update tile endpoints to the Martin host, refine map styling and snapping logic, and add geolocation-based map centering.

This commit is contained in:
Hamza-Ayed
2026-07-15 21:56:41 +03:00
parent 7a64be5833
commit 78fe6c48cc
9 changed files with 544 additions and 89 deletions
+255 -34
View File
@@ -59,13 +59,25 @@
}, },
"overture_buildings": { "overture_buildings": {
"type": "vector", "type": "vector",
"tiles": ["https://tiles.intaleqapp.com/overture_building/{z}/{x}/{y}"], "tiles": [
"https://tiles.intaleqapp.com/overture_building/{z}/{x}/{y}"
],
"maxzoom": 14 "maxzoom": 14
}, },
"overture_segments": { "overture_segments": {
"type": "vector", "type": "vector",
"tiles": ["https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}"], "tiles": [
"https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}"
],
"maxzoom": 14 "maxzoom": 14
},
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
} }
}, },
"layers": [ "layers": [
@@ -599,7 +611,7 @@
"track", "track",
"path", "path",
"footway", "footway",
"cycleway", "cycleway",
"steps" "steps"
], ],
"minzoom": 14, "minzoom": 14,
@@ -687,6 +699,59 @@
] ]
} }
}, },
{
"id": "approved-road-casing",
"type": "line",
"source": "approved_roads",
"source-layer": "approved_roads",
"layout": {
"line-cap": "round",
"line-join": "round"
},
"paint": {
"line-color": "#B9C2CE",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
2.2,
16,
10
],
"line-opacity": 0.9
}
},
{
"id": "approved-road-core",
"type": "line",
"source": "approved_roads",
"source-layer": "approved_roads",
"layout": {
"line-cap": "round",
"line-join": "round"
},
"paint": {
"line-color": "#FFFFFF",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
0.8,
16,
8
]
}
},
{ {
"id": "road-casing-tertiary", "id": "road-casing-tertiary",
"type": "line", "type": "line",
@@ -927,15 +992,44 @@
"minzoom": 15, "minzoom": 15,
"filter": [ "filter": [
"all", "all",
["in", "highway", "motorway", "trunk", "primary", "secondary", "tertiary", "residential", "unclassified", "living_street"], [
["==", "oneway", "yes"] "in",
"highway",
"motorway",
"trunk",
"primary",
"secondary",
"tertiary",
"residential",
"unclassified",
"living_street"
],
[
"==",
"oneway",
"yes"
]
], ],
"layout": { "layout": {
"symbol-placement": "line", "symbol-placement": "line",
"symbol-spacing": 80, "symbol-spacing": 80,
"text-field": "▸", "text-field": "▸",
"text-font": ["Noto Sans Regular"], "text-font": [
"text-size": ["interpolate", ["linear"], ["zoom"], 15, 12, 18, 18], "Noto Sans Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
15,
12,
18,
18
],
"text-keep-upright": false, "text-keep-upright": false,
"text-rotation-alignment": "map", "text-rotation-alignment": "map",
"text-allow-overlap": true, "text-allow-overlap": true,
@@ -943,7 +1037,19 @@
"text-padding": 0 "text-padding": 0
}, },
"paint": { "paint": {
"text-color": ["interpolate", ["linear"], ["zoom"], 15, "rgba(100,116,139,0.4)", 18, "rgba(100,116,139,0.7)"], "text-color": [
"interpolate",
[
"linear"
],
[
"zoom"
],
15,
"rgba(100,116,139,0.4)",
18,
"rgba(100,116,139,0.7)"
],
"text-halo-color": "rgba(255,255,255,0.3)", "text-halo-color": "rgba(255,255,255,0.3)",
"text-halo-width": 0.5 "text-halo-width": 0.5
} }
@@ -977,21 +1083,54 @@
"fill-extrusion-color": "#DDD8D0", "fill-extrusion-color": "#DDD8D0",
"fill-extrusion-height": [ "fill-extrusion-height": [
"interpolate", "interpolate",
["linear"], [
["zoom"], "linear"
],
[
"zoom"
],
14, 14,
[ [
"*", "*",
["coalesce", ["to-number", ["get", "num_floors"], null], 3], [
"coalesce",
[
"to-number",
[
"get",
"num_floors"
],
null
],
3
],
2.5 2.5
], ],
17, 17,
[ [
"coalesce", "coalesce",
["to-number", ["get", "height"], null], [
"to-number",
[
"get",
"height"
],
null
],
[ [
"*", "*",
["coalesce", ["to-number", ["get", "num_floors"], null], 3], [
"coalesce",
[
"to-number",
[
"get",
"num_floors"
],
null
],
3
],
3.5 3.5
], ],
12 12
@@ -1000,10 +1139,16 @@
"fill-extrusion-base": 0, "fill-extrusion-base": 0,
"fill-extrusion-opacity": [ "fill-extrusion-opacity": [
"interpolate", "interpolate",
["linear"], [
["zoom"], "linear"
14, 0.55, ],
16, 0.85 [
"zoom"
],
14,
0.55,
16,
0.85
], ],
"fill-extrusion-vertical-gradient": true "fill-extrusion-vertical-gradient": true
} }
@@ -1178,12 +1323,32 @@
"layout": { "layout": {
"text-field": [ "text-field": [
"coalesce", "coalesce",
["get", "name:ar"], [
["get", "name"], "get",
"name:ar"
],
[
"get",
"name"
],
"" ""
], ],
"text-font": ["Noto Sans Regular"], "text-font": [
"text-size": ["interpolate", ["linear"], ["zoom"], 15, 10, 18, 13], "Noto Sans Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
15,
10,
18,
13
],
"symbol-placement": "line", "symbol-placement": "line",
"text-letter-spacing": 0.05, "text-letter-spacing": 0.05,
"text-padding": 15, "text-padding": 15,
@@ -1216,12 +1381,34 @@
"layout": { "layout": {
"text-field": [ "text-field": [
"coalesce", "coalesce",
["get", "name:ar"], [
["get", "name"], "get",
"name:ar"
],
[
"get",
"name"
],
"" ""
], ],
"text-font": ["Noto Sans Bold"], "text-font": [
"text-size": ["interpolate", ["linear"], ["zoom"], 12, 10, 14, 13, 18, 16], "Noto Sans Bold"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
10,
14,
13,
18,
16
],
"symbol-placement": "line", "symbol-placement": "line",
"text-letter-spacing": 0.06, "text-letter-spacing": 0.06,
"text-padding": 20, "text-padding": 20,
@@ -1445,7 +1632,7 @@
"" ""
], ],
"text-font": [ "text-font": [
"Noto Sans Regular" "Noto Sans Regular"
], ],
"text-size": 11, "text-size": 11,
"text-offset": [ "text-offset": [
@@ -1550,7 +1737,7 @@
"" ""
], ],
"text-font": [ "text-font": [
"Noto Sans Regular" "Noto Sans Regular"
], ],
"text-size": [ "text-size": [
"interpolate", "interpolate",
@@ -1730,7 +1917,7 @@
"text-size": [ "text-size": [
"interpolate", "interpolate",
[ [
"linear" "linear"
], ],
[ [
"zoom" "zoom"
@@ -1760,7 +1947,11 @@
"source": "places_syria", "source": "places_syria",
"source-layer": "places_syria", "source-layer": "places_syria",
"minzoom": 10, "minzoom": 10,
"filter": ["!in", "category", "street"], "filter": [
"!in",
"category",
"street"
],
"layout": { "layout": {
"text-field": [ "text-field": [
"coalesce", "coalesce",
@@ -1810,7 +2001,11 @@
"source": "places_jordan", "source": "places_jordan",
"source-layer": "places_jordan", "source-layer": "places_jordan",
"minzoom": 10, "minzoom": 10,
"filter": ["!in", "category", "street"], "filter": [
"!in",
"category",
"street"
],
"layout": { "layout": {
"text-field": [ "text-field": [
"coalesce", "coalesce",
@@ -1905,11 +2100,37 @@
"source": "overture_segments", "source": "overture_segments",
"source-layer": "overture_segment", "source-layer": "overture_segment",
"minzoom": 14, "minzoom": 14,
"filter": ["has", "names"], "filter": [
"has",
"names"
],
"layout": { "layout": {
"text-field": ["coalesce", ["get", "names"], ""], "text-field": [
"text-font": ["Noto Sans Regular"], "coalesce",
"text-size": ["interpolate", ["linear"], ["zoom"], 14, 9, 16, 12, 18, 14], [
"get",
"names"
],
""
],
"text-font": [
"Noto Sans Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
14,
9,
16,
12,
18,
14
],
"symbol-placement": "line", "symbol-placement": "line",
"text-rotation-alignment": "map", "text-rotation-alignment": "map",
"text-pitch-alignment": "viewport", "text-pitch-alignment": "viewport",
+47 -3
View File
@@ -54,7 +54,7 @@ const MapComponent: React.FC<MapComponentProps> = ({
const tilesUrl = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com'; const tilesUrl = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
try { try {
map.current = new maplibregl.Map({ const initialMap = new maplibregl.Map({
container: mapContainer.current, container: mapContainer.current,
style: { style: {
version: 8, version: 8,
@@ -75,6 +75,12 @@ const MapComponent: React.FC<MapComponentProps> = ({
type: 'vector', type: 'vector',
tiles: [`${tilesUrl}/planet_osm_point/{z}/{x}/{y}`], tiles: [`${tilesUrl}/planet_osm_point/{z}/{x}/{y}`],
maxzoom: 14 maxzoom: 14
},
'approved_roads': {
type: 'vector',
tiles: [`${tilesUrl}/approved_roads/{z}/{x}/{y}`],
minzoom: 8,
maxzoom: 18
} }
}, },
layers: [ layers: [
@@ -145,6 +151,26 @@ const MapComponent: React.FC<MapComponentProps> = ({
'line-width': ['interpolate', ['linear'], ['zoom'], 13, 1, 16, 8] as any 'line-width': ['interpolate', ['linear'], ['zoom'], 13, 1, 16, 8] as any
} }
}, },
{
id: 'approved-casing',
type: 'line',
source: 'approved_roads',
'source-layer': 'approved_roads',
paint: {
'line-color': '#d4d4d4',
'line-width': ['interpolate', ['linear'], ['zoom'], 13, 1, 16, 8] as any
}
},
{
id: 'approved-core',
type: 'line',
source: 'approved_roads',
'source-layer': 'approved_roads',
paint: {
'line-color': '#ffffff',
'line-width': ['interpolate', ['linear'], ['zoom'], 13, 0.5, 16, 6] as any
}
},
{ {
id: 'road-core-minor', id: 'road-core-minor',
type: 'line', type: 'line',
@@ -361,10 +387,28 @@ const MapComponent: React.FC<MapComponentProps> = ({
] ]
}, },
center: [35.9106, 31.9539], center: [35.9106, 31.9539],
zoom: 12,
}); });
map.current.on('load', () => { map.current = initialMap;
// Request User Location
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
if (initialMap) {
initialMap.flyTo({
center: [position.coords.longitude, position.coords.latitude],
zoom: 14,
essential: true
});
}
},
(error) => console.warn('Geolocation denied or failed', error),
{ timeout: 5000 }
);
}
initialMap.on('load', () => {
console.log("MapComponent: Map Loaded Successfully"); console.log("MapComponent: Map Loaded Successfully");
onMapLoad(map.current!); onMapLoad(map.current!);
}); });
+19 -3
View File
@@ -15,7 +15,10 @@ import 'maplibre-gl/dist/maplibre-gl.css';
* Purpose: visually confirm whether a street/place exists before adding or approving it. * Purpose: visually confirm whether a street/place exists before adding or approving it.
*/ */
const TILES = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com'; // Martin vector-tile host (serves planet_osm_*, approved_roads, …). This is a
// DIFFERENT host from where the app + its style.json are served, so it must be
// absolute — an empty default resolved to the app host, which serves no tiles.
const TILES = (import.meta as any).env.VITE_TILES_URL || 'http://188.68.36.205:3202';
type RefKey = 'google-sat' | 'esri-sat' | 'esri-streets' | 'osm'; type RefKey = 'google-sat' | 'esri-sat' | 'esri-streets' | 'osm';
const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = { const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = {
@@ -48,14 +51,27 @@ const CompareView: React.FC = () => {
const start: [number, number] = [35.91, 31.95]; const start: [number, number] = [35.91, 31.95];
const startZoom = 15; const startZoom = 15;
const lMap = new maplibregl.Map({ container: leftDiv.current, style: TILES + '/style.json', center: start, zoom: startZoom, attributionControl: false }); // Base style is served by the APP host as a static file (its internal sources
// already point at the absolute Martin host), so load it relatively — never
// prefixed with the tile host, which does not serve /style.json.
const lMap = new maplibregl.Map({ container: leftDiv.current, style: '/style.json', center: start, zoom: startZoom, attributionControl: false });
const rMap = new maplibregl.Map({ container: rightDiv.current, style: rasterStyle(REFS['esri-sat']), center: start, zoom: startZoom, attributionControl: false }); const rMap = new maplibregl.Map({ container: rightDiv.current, style: rasterStyle(REFS['esri-sat']), center: start, zoom: startZoom, attributionControl: false });
lMap.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left'); lMap.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
rMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right'); rMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition((pos) => {
const coords: [number, number] = [pos.coords.longitude, pos.coords.latitude];
lMap.flyTo({ center: coords, zoom: 14, essential: true });
rMap.flyTo({ center: coords, zoom: 14, essential: true });
setCenter({ lng: coords[0], lat: coords[1], zoom: 14 });
});
}
lMap.on('load', () => { lMap.on('load', () => {
lMap.addSource('approved', { type: 'vector', tiles: [`${TILES}/approved_roads/{z}/{x}/{y}`], minzoom: 8, maxzoom: 18 }); lMap.addSource('approved', { type: 'vector', tiles: [`${TILES}/approved_roads/{z}/{x}/{y}`], minzoom: 8, maxzoom: 18 });
lMap.addLayer({ id: 'approved', type: 'line', source: 'approved', 'source-layer': 'approved_roads', paint: { 'line-color': '#22c55e', 'line-width': ['interpolate', ['linear'], ['zoom'], 10, 1.5, 16, 8] } }); 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] } });
}); });
// Keep the two views locked together. The guard stops the move→jumpTo→move loop. // Keep the two views locked together. The guard stops the move→jumpTo→move loop.
+72 -20
View File
@@ -59,15 +59,18 @@ const IntelligenceDashboard: React.FC = () => {
}, [drawing]); }, [drawing]);
const redrawDrawing = () => { const redrawDrawing = () => {
const lm = leftMapRef.current; const lMap = leftMapRef.current; const rMap = rightMapRef.current;
const rm = rightMapRef.current; if (!lMap || !rMap) return;
const pts = pointsRef.current; const pts = pointsRef.current;
const feats: any[] = []; const data = {
pts.forEach(p => feats.push({ type: 'Feature', geometry: { type: 'Point', coordinates: p } })); type: 'FeatureCollection',
if (pts.length > 1) feats.push({ type: 'Feature', geometry: { type: 'LineString', coordinates: pts } }); features: [
const col = { type: 'FeatureCollection', features: feats }; ...(pts.length >= 2 ? [{ type: 'Feature' as const, geometry: { type: 'LineString' as const, coordinates: pts }, properties: {} }] : []),
if (lm && lm.isStyleLoaded()) (lm.getSource('draw') as maplibregl.GeoJSONSource)?.setData(col as any); ...pts.map(p => ({ type: 'Feature' as const, geometry: { type: 'Point' as const, coordinates: p }, properties: {} }))
if (rm && rm.isStyleLoaded()) (rm.getSource('draw') as maplibregl.GeoJSONSource)?.setData(col as any); ]
};
(lMap.getSource('draw') as maplibregl.GeoJSONSource)?.setData(data as any);
(rMap.getSource('draw') as maplibregl.GeoJSONSource)?.setData(data as any);
}; };
const pushMapData = () => { const pushMapData = () => {
@@ -163,6 +166,15 @@ const IntelligenceDashboard: React.FC = () => {
}); });
lMap.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left'); lMap.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
rMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-right');
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition((pos) => {
const coords: [number, number] = [pos.coords.longitude, pos.coords.latitude];
lMap.flyTo({ center: coords, zoom: 14, essential: true });
rMap.flyTo({ center: coords, zoom: 14, essential: true });
});
}
// Sync logic // Sync logic
const link = (from: maplibregl.Map, to: maplibregl.Map) => { const link = (from: maplibregl.Map, to: maplibregl.Map) => {
@@ -178,6 +190,14 @@ const IntelligenceDashboard: React.FC = () => {
lMap.addSource('cands', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any); lMap.addSource('cands', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
lMap.addSource('closures', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any); lMap.addSource('closures', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
lMap.addSource('hl', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any); lMap.addSource('hl', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
// approved_roads is served by Martin on the tile host — NOT the app host. An
// empty default made this resolve to /approved_roads on the app host (404), so
// approved roads never drew. Point it at the real Martin host.
const TILES = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
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] } });
lMap.addSource('draw', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any); lMap.addSource('draw', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
lMap.addLayer({ id: 'cl', type: 'line', source: 'closures', paint: { 'line-color': '#ef4444', 'line-width': 4, 'line-dasharray': [3, 2] } }); lMap.addLayer({ id: 'cl', type: 'line', source: 'closures', paint: { 'line-color': '#ef4444', 'line-width': 4, 'line-dasharray': [3, 2] } });
@@ -208,18 +228,31 @@ const IntelligenceDashboard: React.FC = () => {
const onCandidateClick = (e: any) => { const onCandidateClick = (e: any) => {
const p = e.features?.[0]?.properties as any; if (!p) return; const p = e.features?.[0]?.properties as any; if (!p) return;
const l = e.features?.[0]?.layer?.id;
setSel(p.id); setSel(p.id);
const isOvt = p.source === 'overture'; const isOvt = p.source === 'overture';
const title = isOvt ? '🗺️ Overture road (missing)' : '🛣️ Driver-traced road'; const isAppr = l === 'approved';
const title = isAppr ? '✅ Approved Road' : isOvt ? '🗺️ Overture road (missing)' : '🛣️ Driver-traced road';
const nameLine = p.name ? `<b>${p.name}</b><br/>` : ''; const nameLine = p.name ? `<b>${p.name}</b><br/>` : '';
const evidence = isOvt ? `${p.uniqueDriverCount ?? 0} trace pts corroborate` : `${p.uniqueDriverCount} drivers`; const evidence = isOvt ? `${p.uniqueDriverCount ?? 0} trace pts corroborate` : `${p.uniqueDriverCount || 1} drivers`;
if (popupRef.current) popupRef.current.remove(); if (popupRef.current) popupRef.current.remove();
popupRef.current = new maplibregl.Popup({ offset: 12 }).setLngLat(e.lngLat)
.setHTML(`<div style="font:12px sans-serif;color:#1e293b;line-height:1.6">${nameLine}<b>${title}</b><br/>Conf: <b style="color:${cc(p.confidence)}">${Math.round(p.confidence * 100)}%</b><br/>${Math.round(p.lengthMeters)}m | ${evidence}</div>`) const popupDiv = document.createElement('div');
.addTo(lMap); popupDiv.style.font = '12px sans-serif';
popupDiv.style.color = '#1e293b';
popupDiv.style.lineHeight = '1.6';
popupDiv.innerHTML = `${nameLine}<b>${title}</b><br/>Conf: <b style="color:${cc(p.confidence)}">${Math.round(p.confidence * 100)}%</b><br/>${Math.round(p.lengthMeters || 0)}m | ${evidence}
<br/><button id="btn-del-${p.id}" style="margin-top:8px;background:#ef4444;color:white;border:none;border-radius:4px;padding:4px 8px;cursor:pointer;">🗑️ Delete Road</button>`;
popupDiv.querySelector(`#btn-del-${p.id}`)?.addEventListener('click', () => {
deleteRoad(p.id, isAppr ? 'approved' : 'candidate');
popupRef.current?.remove();
});
popupRef.current = new maplibregl.Popup({ offset: 12 }).setLngLat(e.lngLat).setDOMContent(popupDiv).addTo(lMap);
}; };
['cp', 'cp-overture'].forEach(layer => { ['cp', 'cp-overture', 'approved'].forEach(layer => {
lMap.on('click', layer, onCandidateClick); lMap.on('click', layer, onCandidateClick);
lMap.on('mouseenter', layer, () => { lMap.getCanvas().style.cursor = 'pointer'; }); lMap.on('mouseenter', layer, () => { lMap.getCanvas().style.cursor = 'pointer'; });
lMap.on('mouseleave', layer, () => { lMap.getCanvas().style.cursor = ''; }); lMap.on('mouseleave', layer, () => { lMap.getCanvas().style.cursor = ''; });
@@ -278,6 +311,25 @@ const IntelligenceDashboard: React.FC = () => {
setTimeout(() => setMsg(''), 5000); setTimeout(() => setMsg(''), 5000);
}; };
const deleteRoad = async (id: string, type: 'approved' | 'candidate') => {
try {
const res = await fetch(`${API}/map-refinement/roads/${type}/${id}`, {
method: 'DELETE',
headers: { 'x-api-key': apiKey }
});
if (!res.ok) throw new Error('Failed to delete');
setMsg(`🗑️ Deleted ${type} road successfully.`);
setSel(null);
(leftMapRef.current?.getSource('hl') as maplibregl.GeoJSONSource)?.setData({ type: 'FeatureCollection', features: [] });
load();
// Force MapLibre to drop cached vector tiles completely for approved roads
if (type === 'approved') {
setTimeout(() => window.location.reload(), 1500);
}
} catch { setMsg('Delete failed.'); }
setTimeout(() => setMsg(''), 5000);
};
const runAI = async () => { const runAI = async () => {
setMsg('🧠 Deep analysis started...'); setMsg('🧠 Deep analysis started...');
try { try {
@@ -298,18 +350,18 @@ const IntelligenceDashboard: React.FC = () => {
setTimeout(() => setMsg(''), 6000); setTimeout(() => setMsg(''), 6000);
}; };
const submitDrawing = async () => { const submitRoad = async () => {
if (pointsRef.current.length < 2) return setMsg('Draw at least 2 points!'); if (pointsRef.current.length < 2) return setMsg('Draw at least 2 points!');
const roadName = window.prompt('Enter a name for this new road:', 'Unnamed Road'); const roadName = window.prompt('Enter a name for this new road:', 'Unnamed Road');
if (roadName === null) return; // User cancelled if (roadName === null) return;
const highway = window.prompt('Enter road category (primary, secondary, residential, unclassified, service):', 'unclassified');
if (highway === null) return;
try { try {
const geojson = { type: 'LineString', coordinates: pointsRef.current }; const geojson = { type: 'LineString', coordinates: pointsRef.current };
const res = await fetch(`${API}/map-refinement/roads/candidates/manual`, { const res = await fetch(`${API}/map-refinement/roads/candidates/manual`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey }, headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({ geojson, name: roadName }), body: JSON.stringify({ geojson, name: roadName, highway }),
}); });
if (!res.ok) throw new Error('Failed to submit road'); if (!res.ok) throw new Error('Failed to submit road');
setMsg('Road submitted for intelligence analysis!'); setMsg('Road submitted for intelligence analysis!');
@@ -400,7 +452,7 @@ const IntelligenceDashboard: React.FC = () => {
<div style={{ background: '#1e3a8a30', border: '1px solid #3b82f6', padding: '10px', borderRadius: '8px', fontSize: '0.75rem', color: '#bfdbfe' }}> <div style={{ background: '#1e3a8a30', border: '1px solid #3b82f6', padding: '10px', borderRadius: '8px', fontSize: '0.75rem', color: '#bfdbfe' }}>
Click on <b>either map</b> to trace the street.<br /> Click on <b>either map</b> to trace the street.<br />
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}> <div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button onClick={submitDrawing} style={{ flex: 1, background: '#3b82f6', color: 'white', border: 'none', padding: '6px', borderRadius: '6px', cursor: 'pointer', fontWeight: 600 }}>Submit Road</button> <button onClick={submitRoad} style={{ flex: 1, background: '#3b82f6', color: 'white', border: 'none', padding: '6px', borderRadius: '6px', cursor: 'pointer', fontWeight: 600 }}>Submit Road</button>
<button onClick={() => { pointsRef.current = []; redrawDrawing(); }} style={{ flex: 1, background: 'transparent', color: '#94a3b8', border: '1px solid #334155', padding: '6px', borderRadius: '6px', cursor: 'pointer', fontWeight: 600 }}>Clear</button> <button onClick={() => { pointsRef.current = []; redrawDrawing(); }} style={{ flex: 1, background: 'transparent', color: '#94a3b8', border: '1px solid #334155', padding: '6px', borderRadius: '6px', cursor: 'pointer', fontWeight: 600 }}>Clear</button>
</div> </div>
</div> </div>
+61
View File
@@ -70,6 +70,14 @@
"https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}" "https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}"
], ],
"maxzoom": 14 "maxzoom": 14
},
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
} }
}, },
"layers": [ "layers": [
@@ -691,6 +699,59 @@
] ]
} }
}, },
{
"id": "approved-road-casing",
"type": "line",
"source": "approved_roads",
"source-layer": "approved_roads",
"layout": {
"line-cap": "round",
"line-join": "round"
},
"paint": {
"line-color": "#D4D8DF",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
1.5,
16,
10
],
"line-opacity": 0.7
}
},
{
"id": "approved-road-core",
"type": "line",
"source": "approved_roads",
"source-layer": "approved_roads",
"layout": {
"line-cap": "round",
"line-join": "round"
},
"paint": {
"line-color": "#FFFFFF",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
0.8,
16,
8
]
}
},
{ {
"id": "road-casing-tertiary", "id": "road-casing-tertiary",
"type": "line", "type": "line",
@@ -70,6 +70,14 @@
"https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}" "https://tiles.intaleqapp.com/overture_segment/{z}/{x}/{y}"
], ],
"maxzoom": 14 "maxzoom": 14
},
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
} }
}, },
"layers": [ "layers": [
@@ -691,6 +699,59 @@
] ]
} }
}, },
{
"id": "approved-road-casing",
"type": "line",
"source": "approved_roads",
"source-layer": "approved_roads",
"layout": {
"line-cap": "round",
"line-join": "round"
},
"paint": {
"line-color": "#D4D8DF",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
1.5,
16,
10
],
"line-opacity": 0.7
}
},
{
"id": "approved-road-core",
"type": "line",
"source": "approved_roads",
"source-layer": "approved_roads",
"layout": {
"line-cap": "round",
"line-join": "round"
},
"paint": {
"line-color": "#FFFFFF",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
12,
0.8,
16,
8
]
}
},
{ {
"id": "road-casing-tertiary", "id": "road-casing-tertiary",
"type": "line", "type": "line",
+4 -4
View File
@@ -74,7 +74,7 @@
"approved_roads": { "approved_roads": {
"type": "vector", "type": "vector",
"tiles": [ "tiles": [
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}" "http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
], ],
"minzoom": 8, "minzoom": 8,
"maxzoom": 18 "maxzoom": 18
@@ -709,7 +709,7 @@
"line-join": "round" "line-join": "round"
}, },
"paint": { "paint": {
"line-color": "#B9C2CE", "line-color": "#D4D8DF",
"line-width": [ "line-width": [
"interpolate", "interpolate",
[ [
@@ -719,11 +719,11 @@
"zoom" "zoom"
], ],
12, 12,
2.2, 1.5,
16, 16,
10 10
], ],
"line-opacity": 0.9 "line-opacity": 0.7
} }
}, },
{ {
+4 -4
View File
@@ -74,7 +74,7 @@
"approved_roads": { "approved_roads": {
"type": "vector", "type": "vector",
"tiles": [ "tiles": [
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}" "http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
], ],
"minzoom": 8, "minzoom": 8,
"maxzoom": 18 "maxzoom": 18
@@ -709,7 +709,7 @@
"line-join": "round" "line-join": "round"
}, },
"paint": { "paint": {
"line-color": "#B9C2CE", "line-color": "#D4D8DF",
"line-width": [ "line-width": [
"interpolate", "interpolate",
[ [
@@ -719,11 +719,11 @@
"zoom" "zoom"
], ],
12, 12,
2.2, 1.5,
16, 16,
10 10
], ],
"line-opacity": 0.9 "line-opacity": 0.7
} }
}, },
{ {
+1 -1
View File
@@ -41,7 +41,7 @@ if [ $? -eq 0 ]; then
echo "------------------------------------------------" echo "------------------------------------------------"
echo "Now run the following on your SERVER terminal:" echo "Now run the following on your SERVER terminal:"
echo "cd $REMOTE_PATH" echo "cd $REMOTE_PATH"
echo "docker compose up -d --build api dashboard" echo "docker compose up -d --build api dashboard web"
echo "Dashboard will be live at: http://$SERVER_IP:3204" echo "Dashboard will be live at: http://$SERVER_IP:3204"
echo "------------------------------------------------" echo "------------------------------------------------"
else else