feat: implement geometry snapping logic for manual road insertion and approved roads topology

This commit is contained in:
Hamza-Ayed
2026-08-16 17:34:06 +03:00
parent b3ffa7c71d
commit 03cdc6fe9c
12 changed files with 1431 additions and 453 deletions
+140 -344
View File
@@ -1,44 +1,89 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import mlcontour from 'maplibre-contour';
import { attachIconLoader } from '../utils/mapIcons';
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);
interface MapComponentProps {
onMapLoad: (map: maplibregl.Map) => void;
onMapClick?: (lat: number, lng: number) => void;
show3D?: boolean;
showPOIs?: boolean;
showTerrain?: boolean;
showContours?: boolean;
showAdminBoundaries?: boolean;
}
const MapComponent: React.FC<MapComponentProps> = ({
onMapLoad,
onMapClick,
show3D = false,
showPOIs = true
showPOIs = true,
showTerrain = true,
showContours = false,
showAdminBoundaries = true
}) => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<maplibregl.Map | null>(null);
// Sync toggles with map layers
useEffect(() => {
if (!map.current) return;
if (!map.current || !map.current.isStyleLoaded()) return;
// Toggle 3D Buildings (now on Overture source)
if (map.current.getLayer('building-3d')) {
map.current.setLayoutProperty('building-3d', 'visibility', show3D ? 'visible' : 'none');
}
// Toggle flat Overture footprint (show when 3D is off)
// Toggle 3D Buildings
['building-3d', 'building-3d-osm'].forEach(layerId => {
if (map.current!.getLayer(layerId)) {
map.current!.setLayoutProperty(layerId, 'visibility', show3D ? 'visible' : 'none');
}
});
if (map.current.getLayer('overture-building-footprint')) {
map.current.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
}
map.current.easeTo({ pitch: show3D ? 55 : 0, duration: 600 });
// Toggle POI Layers
const poiLayers = ['poi-icons', 'place-labels', 'overture-building-names'];
const poiLayers = ['poi-icons', 'place-labels', 'overture-building-names', 'places-jordan-labels'];
poiLayers.forEach(layerId => {
if (map.current!.getLayer(layerId)) {
map.current!.setLayoutProperty(layerId, 'visibility', showPOIs ? 'visible' : 'none');
}
});
}, [show3D, showPOIs]);
// Toggle Hillshading / Terrain
if (map.current.getLayer('hillshading')) {
map.current.setLayoutProperty('hillshading', 'visibility', showTerrain ? 'visible' : 'none');
}
// Toggle Contours
const contourLayers = ['contour-lines-minor', 'contour-lines-major', 'contour-labels'];
contourLayers.forEach(layerId => {
if (map.current!.getLayer(layerId)) {
map.current!.setLayoutProperty(layerId, 'visibility', showContours ? 'visible' : 'none');
}
});
// Toggle Admin Boundaries
const adminLayers = [
'admin-boundary-national',
'admin-boundary-governorate-poly',
'admin-boundary-governorate',
'admin-boundary-district-poly',
'admin-boundary-district'
];
adminLayers.forEach(layerId => {
if (map.current!.getLayer(layerId)) {
map.current!.setLayoutProperty(layerId, 'visibility', showAdminBoundaries ? 'visible' : 'none');
}
});
}, [show3D, showPOIs, showTerrain, showContours, showAdminBoundaries]);
useEffect(() => {
if (map.current) return;
@@ -47,350 +92,21 @@ const MapComponent: React.FC<MapComponentProps> = ({
// Load RTL Text Plugin for correct Arabic rendering
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
maplibregl.setRTLTextPlugin(
'/rtl-plugin.js', // Localized to bypass ORB
true // Lazy load
'/rtl-plugin.js',
true
);
}
const tilesUrl = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
try {
const initialMap = new maplibregl.Map({
container: mapContainer.current,
style: {
version: 8,
glyphs: 'https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf',
sources: {
'local-osm-polygons': {
type: 'vector',
tiles: [`${tilesUrl}/planet_osm_polygon/{z}/{x}/{y}`],
maxzoom: 14
},
'local-osm-lines': {
type: 'vector',
tiles: [`${tilesUrl}/planet_osm_line/{z}/{x}/{y}`],
maxzoom: 14
},
'local-osm-points': {
type: 'vector',
tiles: [`${tilesUrl}/planet_osm_point/{z}/{x}/{y}`],
maxzoom: 14
},
'approved_roads': {
type: 'vector',
tiles: [`${tilesUrl}/approved_roads/{z}/{x}/{y}`],
minzoom: 8,
maxzoom: 18
}
},
layers: [
{
id: 'background',
type: 'background',
paint: { 'background-color': '#f8f9fa' }
},
{
id: 'water-layer',
type: 'fill',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
filter: ['in', 'natural', 'water', 'lake', 'riverbank'],
paint: { 'fill-color': '#a3ccff' }
},
{
id: 'park-layer',
type: 'fill',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
filter: ['in', 'leisure', 'park', 'garden', 'nature_reserve', 'pitch'],
paint: { 'fill-color': '#dcedc8' }
},
{
id: 'landuse-layer',
type: 'fill',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
filter: ["in", "landuse", "residential", "commercial", "industrial", "cemetery"],
paint: {
"fill-color": [
"match",
["get", "landuse"],
"residential", "#f1f3f4",
"commercial", "#f8f9fa",
"industrial", "#f1f3f4",
"cemetery", "#dcedc8",
"#f1f3f4"
]
}
},
{
id: 'building-3d',
type: 'fill-extrusion',
source: 'local-osm-polygons',
'source-layer': 'planet_osm_polygon',
minzoom: 15,
filter: ['has', 'building'],
layout: {
visibility: show3D ? 'visible' : 'none'
},
paint: {
'fill-extrusion-color': '#e8eaed',
'fill-extrusion-height': 20,
'fill-extrusion-base': 0,
'fill-extrusion-opacity': 0.8
}
},
{
id: 'road-casing-minor',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'residential', 'service', 'unclassified', 'living_street', 'pedestrian', 'path', 'track'],
paint: {
'line-color': '#d4d4d4',
'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',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'residential', 'service', 'unclassified', 'living_street', 'pedestrian', 'path', 'track'],
paint: {
'line-color': '#ffffff',
'line-width': ['interpolate', ['linear'], ['zoom'], 13, 0.5, 16, 6] as any
}
},
{
id: 'road-casing-tertiary',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'tertiary', 'tertiary_link'],
paint: {
'line-color': '#e0e0e0',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 1.5, 16, 12] as any
}
},
{
id: 'road-core-tertiary',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'tertiary', 'tertiary_link'],
paint: {
'line-color': '#ffffff',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 1, 16, 9] as any
}
},
{
id: 'road-casing-secondary',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'secondary', 'secondary_link'],
paint: {
'line-color': '#cfd8dc',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 2, 16, 14] as any
}
},
{
id: 'road-core-secondary',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'secondary', 'secondary_link'],
paint: {
'line-color': '#f1f5f9',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 1.5, 16, 11] as any
}
},
{
id: 'road-casing-primary',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'primary', 'primary_link'],
paint: {
'line-color': '#facc15',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 3, 16, 16] as any
}
},
{
id: 'road-core-primary',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'primary', 'primary_link'],
paint: {
'line-color': '#fefce8',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 2, 16, 12] as any
}
},
{
id: 'road-casing-motorway-trunk',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'motorway', 'motorway_link', 'trunk', 'trunk_link'],
paint: {
'line-color': '#fb923c',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 4, 16, 18] as any
}
},
{
id: 'road-core-motorway-trunk',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
filter: ['in', 'highway', 'motorway', 'motorway_link', 'trunk', 'trunk_link'],
paint: {
'line-color': '#ffedd5',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 2.5, 16, 14] as any
}
},
{
id: 'route-line',
type: 'line',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': '#3b82f6', 'line-width': 6, 'line-opacity': 0.8 },
filter: ['==', 'id', -1]
},
{
id: 'road-labels',
type: 'symbol',
source: 'local-osm-lines',
'source-layer': 'planet_osm_line',
minzoom: 15,
layout: {
'text-field': '{name}',
'text-font': ['Noto Sans Regular'],
'text-size': 13,
'symbol-placement': 'line',
'text-letter-spacing': 0.05,
'text-padding': 5,
'text-allow-overlap': false,
'text-ignore-placement': false
},
paint: {
'text-color': '#3c4043',
'text-halo-color': 'rgba(255, 255, 255, 0.8)',
'text-halo-width': 2
}
},
{
id: 'poi-icons',
type: 'symbol',
source: 'local-osm-points',
'source-layer': 'planet_osm_point',
minzoom: 13,
layout: {
'visibility': showPOIs ? 'visible' : 'none',
'icon-image': [
'coalesce',
[
'match', ['get', 'shop'],
'supermarket', 'grocery-15',
'convenience', 'grocery-15',
'pharmacy', 'pharmacy-15',
'clothes', 'clothing_store-15',
''
],
[
'match', ['get', 'amenity'],
'restaurant', 'restaurant-15',
'cafe', 'cafe-15',
'bank', 'bank-15',
'pharmacy', 'pharmacy-15',
'grave_yard', 'cemetery-15',
'school', 'school-15',
'university', 'college-15',
'place_of_worship', 'religious-christian-15',
'hospital', 'hospital-15',
'fuel', 'fuel-15',
''
],
[
'match', ['get', 'historic'],
'monument', 'monument-15',
'ruins', 'attraction-15',
'archaeological_site', 'attraction-15',
''
],
[
'match', ['get', 'tourism'],
'museum', 'museum-15',
'hotel', 'hotel-15',
'attraction', 'attraction-15',
''
],
[
'match', ['get', 'landuse'],
'cemetery', 'cemetery-15',
''
],
'marker-15'
],
'icon-size': 1.1,
'icon-padding': 2,
'icon-allow-overlap': false
},
paint: {
'icon-opacity': 1,
'icon-halo-color': '#ffffff',
'icon-halo-width': 1
}
},
{
id: 'place-labels',
type: 'symbol',
source: 'local-osm-points',
'source-layer': 'planet_osm_point',
minzoom: 13,
layout: {
'text-field': '{name}',
'text-font': ['Noto Sans Regular'],
'text-size': ['interpolate', ['linear'], ['zoom'], 13, 12, 16, 16] as any,
'text-offset': [0, 1.5],
'text-anchor': 'top',
'visibility': showPOIs ? 'visible' : 'none'
},
paint: {
'text-color': '#222',
'text-halo-color': 'rgba(255, 255, 255, 0.9)',
'text-halo-width': 2
}
}
]
},
style: '/style.json',
center: [35.9106, 31.9539],
zoom: 12,
attributionControl: false
});
map.current = initialMap;
// آيقونات الـ POI تُحمّل عند الطلب بدل sprite مستضاف
attachIconLoader(initialMap);
// Request User Location
@@ -412,6 +128,86 @@ const MapComponent: React.FC<MapComponentProps> = ({
initialMap.on('load', () => {
console.log("MapComponent: Map Loaded Successfully");
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 (!initialMap.getSource('contour-source')) {
initialMap.addSource('contour-source', {
type: 'vector',
tiles: [contourUrl],
maxzoom: 18,
});
initialMap.addLayer({
id: 'contour-lines-minor',
type: 'line',
source: 'contour-source',
'source-layer': 'contours',
minzoom: 10,
layout: {
visibility: showContours ? 'visible' : 'none',
},
filter: ['==', ['get', 'level'], 0],
paint: {
'line-color': '#a86324',
'line-width': 1.1,
'line-opacity': 0.9,
},
});
initialMap.addLayer({
id: 'contour-lines-major',
type: 'line',
source: 'contour-source',
'source-layer': 'contours',
minzoom: 9,
layout: {
visibility: showContours ? 'visible' : 'none',
},
filter: ['>', ['get', 'level'], 0],
paint: {
'line-color': '#703800',
'line-width': 2.0,
'line-opacity': 1.0,
},
});
initialMap.addLayer({
id: 'contour-labels',
type: 'symbol',
source: 'contour-source',
'source-layer': 'contours',
minzoom: 12,
layout: {
visibility: showContours ? 'visible' : 'none',
'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,
},
});
}
onMapLoad(map.current!);
});