feat: implement geometry snapping logic for manual road insertion and approved roads topology
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import mlcontour from 'maplibre-contour';
|
||||
import { attachIconLoader } from '../utils/mapIcons';
|
||||
|
||||
// Initialize DEM source for on-the-fly global contours (Jordan, Syria, Iraq, Egypt, etc.)
|
||||
const demSource = new mlcontour.DemSource({
|
||||
url: 'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png',
|
||||
encoding: 'terrarium',
|
||||
maxzoom: 14,
|
||||
worker: true,
|
||||
});
|
||||
demSource.setupMaplibre(maplibregl);
|
||||
|
||||
/**
|
||||
* CompareView — side-by-side, pan/zoom-synced map comparison.
|
||||
*
|
||||
@@ -91,9 +101,79 @@ const CompareView: React.FC = () => {
|
||||
}
|
||||
|
||||
lMap.on('load', () => {
|
||||
lMap.addSource('approved', { type: 'vector', tiles: [`${TILES}/approved_roads/{z}/{x}/{y}`], minzoom: 8, maxzoom: 18 });
|
||||
lMap.addLayer({ id: 'approved-casing', type: 'line', source: 'approved', 'source-layer': 'approved_roads', paint: { 'line-color': '#d4d4d4', 'line-width': ['interpolate', ['linear'], ['zoom'], 13, 1, 16, 8] } });
|
||||
lMap.addLayer({ id: 'approved', type: 'line', source: 'approved', 'source-layer': 'approved_roads', paint: { 'line-color': '#ffffff', 'line-width': ['interpolate', ['linear'], ['zoom'], 13, 0.5, 16, 6] } });
|
||||
// Dynamic On-the-Fly Contours from Elevation DEM
|
||||
const contourUrl = demSource.contourProtocolUrl({
|
||||
thresholds: {
|
||||
10: [100, 500],
|
||||
11: [50, 250],
|
||||
12: [25, 100],
|
||||
13: [20, 100],
|
||||
14: [10, 50],
|
||||
15: [10, 50],
|
||||
16: [10, 50],
|
||||
17: [10, 50],
|
||||
18: [10, 50],
|
||||
},
|
||||
elevationKey: 'ele',
|
||||
levelKey: 'level',
|
||||
contourLayer: 'contours',
|
||||
});
|
||||
|
||||
if (!lMap.getSource('contour-source')) {
|
||||
lMap.addSource('contour-source', {
|
||||
type: 'vector',
|
||||
tiles: [contourUrl],
|
||||
maxzoom: 18,
|
||||
});
|
||||
|
||||
lMap.addLayer({
|
||||
id: 'contour-lines-minor',
|
||||
type: 'line',
|
||||
source: 'contour-source',
|
||||
'source-layer': 'contours',
|
||||
minzoom: 10,
|
||||
filter: ['==', ['get', 'level'], 0],
|
||||
paint: {
|
||||
'line-color': '#a86324',
|
||||
'line-width': 1.1,
|
||||
'line-opacity': 0.9,
|
||||
},
|
||||
});
|
||||
|
||||
lMap.addLayer({
|
||||
id: 'contour-lines-major',
|
||||
type: 'line',
|
||||
source: 'contour-source',
|
||||
'source-layer': 'contours',
|
||||
minzoom: 9,
|
||||
filter: ['>', ['get', 'level'], 0],
|
||||
paint: {
|
||||
'line-color': '#703800',
|
||||
'line-width': 2.0,
|
||||
'line-opacity': 1.0,
|
||||
},
|
||||
});
|
||||
|
||||
lMap.addLayer({
|
||||
id: 'contour-labels',
|
||||
type: 'symbol',
|
||||
source: 'contour-source',
|
||||
'source-layer': 'contours',
|
||||
minzoom: 12,
|
||||
filter: ['>', ['get', 'level'], 0],
|
||||
layout: {
|
||||
'symbol-placement': 'line',
|
||||
'text-field': ['concat', ['to-string', ['get', 'ele']], ' m'],
|
||||
'text-size': 10,
|
||||
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
|
||||
},
|
||||
paint: {
|
||||
'text-color': '#5c2d00',
|
||||
'text-halo-color': 'rgba(255, 255, 255, 0.95)',
|
||||
'text-halo-width': 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Keep the two views locked together. The guard stops the move→jumpTo→move loop.
|
||||
@@ -122,18 +202,69 @@ const CompareView: React.FC = () => {
|
||||
m.once('styledata', () => m.jumpTo({ center: c, zoom: z, bearing: b, pitch: p }));
|
||||
}, [refKey]);
|
||||
|
||||
const [showTerrain, setShowTerrain] = useState(true);
|
||||
const [showContours, setShowContours] = useState(true);
|
||||
const [showAdmin, setShowAdmin] = useState(true);
|
||||
const [show3D, setShow3D] = useState(false);
|
||||
|
||||
// Sync toggles with leftMap layers
|
||||
useEffect(() => {
|
||||
const m = leftMap.current;
|
||||
if (!m) return;
|
||||
|
||||
// Wait if style is not loaded yet
|
||||
const applyToggles = () => {
|
||||
if (m.getLayer('hillshading')) {
|
||||
m.setLayoutProperty('hillshading', 'visibility', showTerrain ? 'visible' : 'none');
|
||||
}
|
||||
['contour-lines-minor', 'contour-lines-major', 'contour-labels'].forEach(l => {
|
||||
if (m.getLayer(l)) m.setLayoutProperty(l, 'visibility', showContours ? 'visible' : 'none');
|
||||
});
|
||||
['admin-boundary-national', 'admin-boundary-governorate-poly', 'admin-boundary-governorate', 'admin-boundary-district-poly', 'admin-boundary-district'].forEach(l => {
|
||||
if (m.getLayer(l)) m.setLayoutProperty(l, 'visibility', showAdmin ? 'visible' : 'none');
|
||||
});
|
||||
['building-3d', 'building-3d-osm'].forEach(l => {
|
||||
if (m.getLayer(l)) m.setLayoutProperty(l, 'visibility', show3D ? 'visible' : 'none');
|
||||
});
|
||||
|
||||
// Tilt camera for 3D perspective
|
||||
m.easeTo({ pitch: show3D ? 55 : 0, duration: 600 });
|
||||
};
|
||||
|
||||
if (m.isStyleLoaded()) {
|
||||
applyToggles();
|
||||
} else {
|
||||
m.once('styledata', applyToggles);
|
||||
}
|
||||
}, [showTerrain, showContours, showAdmin, show3D]);
|
||||
|
||||
const fmt = (n: number) => n.toFixed(6);
|
||||
const z = Math.round(center.zoom);
|
||||
const btn: React.CSSProperties = { background: '#1e293b', border: '1px solid #334155', color: '#cbd5e1', padding: '6px 10px', borderRadius: '8px', cursor: 'pointer', fontSize: '0.75rem', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: '5px' };
|
||||
const btn: React.CSSProperties = { background: '#1e293b', border: '1px solid #334155', color: '#cbd5e1', padding: '5px 8px', borderRadius: '8px', cursor: 'pointer', fontSize: '0.72rem', textDecoration: 'none', display: 'inline-flex', alignItems: 'center', gap: '4px' };
|
||||
const toggleBtn = (active: boolean): React.CSSProperties => ({
|
||||
...btn,
|
||||
borderColor: active ? '#10b981' : '#334155',
|
||||
background: active ? 'rgba(16,185,129,0.2)' : '#1e293b',
|
||||
color: active ? '#34d399' : '#94a3b8',
|
||||
fontWeight: active ? 600 : 400
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#0f172a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||
{/* Toolbar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px', paddingRight: '280px', borderBottom: '1px solid #1e293b', background: '#0f172a' }}>
|
||||
<strong style={{ fontSize: '0.9rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
|
||||
<span style={{ color: '#64748b', fontSize: '0.78rem', whiteSpace: 'nowrap' }}>Our map ⟷ reference — synced</span>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', padding: '8px 14px', paddingRight: '280px', borderBottom: '1px solid #1e293b', background: '#0f172a', flexWrap: 'wrap' }}>
|
||||
<strong style={{ fontSize: '0.85rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
|
||||
|
||||
{/* Left Map Layer Toggles */}
|
||||
<div style={{ display: 'flex', gap: '5px', alignItems: 'center', background: 'rgba(30,41,59,0.7)', padding: '2px 6px', borderRadius: '8px', border: '1px solid #334155' }}>
|
||||
<span style={{ fontSize: '0.68rem', color: '#94a3b8' }}>Our Map:</span>
|
||||
<button onClick={() => setShowTerrain(!showTerrain)} style={toggleBtn(showTerrain)}>🏔️ Relief</button>
|
||||
<button onClick={() => setShowContours(!showContours)} style={toggleBtn(showContours)}>〰️ Contours</button>
|
||||
<button onClick={() => setShowAdmin(!showAdmin)} style={toggleBtn(showAdmin)}>🏛️ Admin</button>
|
||||
<button onClick={() => setShow3D(!show3D)} style={toggleBtn(show3D)}>🏢 3D</button>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '6px', marginInlineStart: 'auto', overflowX: 'auto', paddingBottom: '2px' }}>
|
||||
<div style={{ display: 'flex', gap: '5px', marginInlineStart: 'auto', overflowX: 'auto', paddingBottom: '2px' }}>
|
||||
{(Object.keys(REFS) as RefKey[]).map(k => (
|
||||
<button key={k} onClick={() => setRefKey(k)}
|
||||
style={{ ...btn, whiteSpace: 'nowrap', flexShrink: 0, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.35)', fontWeight: 600 } : {}) }}>
|
||||
|
||||
Reference in New Issue
Block a user