feat: implement geometry snapping logic for manual road insertion and approved roads topology
This commit is contained in:
@@ -467,6 +467,17 @@ export class GeocodingService {
|
|||||||
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
|
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
|
||||||
`, [lng, lat]));
|
`, [lng, lat]));
|
||||||
|
|
||||||
|
queryPromises.push(repo.query(`
|
||||||
|
SELECT
|
||||||
|
id::text, COALESCE(name, 'Street') as name, COALESCE(name, 'طريق معتمد') as name_ar, 'street' as category,
|
||||||
|
'' as neighbourhood, '' as district, '' as governorate,
|
||||||
|
ST_Y(ST_Centroid(geometry::geometry))::text as latitude, ST_X(ST_Centroid(geometry::geometry))::text as longitude, '' as address, 'approved_road' as source,
|
||||||
|
ST_DistanceSphere(geometry::geometry, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
|
||||||
|
FROM approved_roads
|
||||||
|
WHERE geometry IS NOT NULL AND name IS NOT NULL AND trim(name) != ''
|
||||||
|
ORDER BY geometry::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
|
||||||
|
`, [lng, lat]).catch(() => []));
|
||||||
|
|
||||||
const [results, iraqiAddress] = await Promise.all([
|
const [results, iraqiAddress] = await Promise.all([
|
||||||
Promise.allSettled(queryPromises),
|
Promise.allSettled(queryPromises),
|
||||||
iraqiAddressPromise,
|
iraqiAddressPromise,
|
||||||
|
|||||||
@@ -91,20 +91,100 @@ export class RoadRefinementController {
|
|||||||
const name = body.name || 'Unnamed Road';
|
const name = body.name || 'Unnamed Road';
|
||||||
const highway = body.highway || 'residential';
|
const highway = body.highway || 'residential';
|
||||||
|
|
||||||
const result = await this.dataSource.query(
|
let result;
|
||||||
`INSERT INTO candidate_roads (
|
try {
|
||||||
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
|
result = await this.dataSource.query(
|
||||||
confidence, status, source, name, highway, oneway, "discoveredAt"
|
`WITH raw_input AS (
|
||||||
)
|
SELECT ST_SetSRID(ST_GeomFromGeoJSON($1), 4326) AS geom
|
||||||
VALUES (
|
),
|
||||||
ST_SetSRID(ST_GeomFromGeoJSON($1), 4326),
|
start_snap AS (
|
||||||
1, 10, 30.0,
|
SELECT COALESCE(
|
||||||
ST_Length(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 3857)),
|
(
|
||||||
0.90, 'pending', 'manual', $2, $3, 0, NOW()
|
SELECT ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)
|
||||||
)
|
FROM raw_input r
|
||||||
RETURNING id, name, highway, confidence, status`,
|
CROSS JOIN LATERAL (SELECT ST_Transform(ST_StartPoint(r.geom), 3857) AS pt) p
|
||||||
[geojsonStr, name, highway]
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
);
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_StartPoint(r.geom)::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_StartPoint(r.geom)::geography) ASC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SELECT ST_Transform(ST_ClosestPoint(l.way, ST_Transform(ST_StartPoint(r.geom), 3857)), 4326)
|
||||||
|
FROM raw_input r
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(ST_StartPoint(r.geom), 3857), 60)
|
||||||
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, ST_StartPoint(r.geom)::geography) < 30
|
||||||
|
ORDER BY ST_Distance(l.way, ST_Transform(ST_StartPoint(r.geom), 3857)) ASC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
(SELECT ST_StartPoint(geom) FROM raw_input)
|
||||||
|
) AS pt
|
||||||
|
),
|
||||||
|
end_snap AS (
|
||||||
|
SELECT COALESCE(
|
||||||
|
(
|
||||||
|
SELECT ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)
|
||||||
|
FROM raw_input r
|
||||||
|
CROSS JOIN LATERAL (SELECT ST_Transform(ST_EndPoint(r.geom), 3857) AS pt) p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_EndPoint(r.geom)::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_EndPoint(r.geom)::geography) ASC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SELECT ST_Transform(ST_ClosestPoint(l.way, ST_Transform(ST_EndPoint(r.geom), 3857)), 4326)
|
||||||
|
FROM raw_input r
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(ST_EndPoint(r.geom), 3857), 60)
|
||||||
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, ST_EndPoint(r.geom)::geography) < 30
|
||||||
|
ORDER BY ST_Distance(l.way, ST_Transform(ST_EndPoint(r.geom), 3857)) ASC
|
||||||
|
LIMIT 1
|
||||||
|
),
|
||||||
|
(SELECT ST_EndPoint(geom) FROM raw_input)
|
||||||
|
) AS pt
|
||||||
|
),
|
||||||
|
snapped_input AS (
|
||||||
|
SELECT ST_SetPoint(
|
||||||
|
ST_SetPoint(r.geom, 0, s.pt),
|
||||||
|
ST_NPoints(r.geom) - 1,
|
||||||
|
e.pt
|
||||||
|
) AS geom
|
||||||
|
FROM raw_input r, start_snap s, end_snap e
|
||||||
|
)
|
||||||
|
INSERT INTO candidate_roads (
|
||||||
|
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
|
||||||
|
confidence, status, source, name, highway, oneway, "discoveredAt"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
geom,
|
||||||
|
1, 10, 30.0,
|
||||||
|
ST_Length(ST_Transform(geom, 3857)),
|
||||||
|
0.90, 'pending', 'manual', $2, $3, 0, NOW()
|
||||||
|
FROM snapped_input
|
||||||
|
RETURNING id, name, highway, confidence, status`,
|
||||||
|
[geojsonStr, name, highway]
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Complex snapping query failed, falling back to direct insertion: ${err.message}`);
|
||||||
|
result = await this.dataSource.query(
|
||||||
|
`INSERT INTO candidate_roads (
|
||||||
|
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
|
||||||
|
confidence, status, source, name, highway, oneway, "discoveredAt"
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
ST_SetSRID(ST_GeomFromGeoJSON($1), 4326),
|
||||||
|
1, 10, 30.0,
|
||||||
|
ST_Length(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 3857)),
|
||||||
|
0.90, 'pending', 'manual', $2, $3, 0, NOW()
|
||||||
|
)
|
||||||
|
RETURNING id, name, highway, confidence, status`,
|
||||||
|
[geojsonStr, name, highway]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.log(`✍️ Manual candidate road registered: ${result[0]?.id} (${name})`);
|
this.logger.log(`✍️ Manual candidate road registered: ${result[0]?.id} (${name})`);
|
||||||
return { success: true, candidate: result[0] };
|
return { success: true, candidate: result[0] };
|
||||||
@@ -115,7 +195,7 @@ export class RoadRefinementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch('candidates/:id/approve')
|
@Patch('candidates/:id/approve')
|
||||||
@ApiOperation({ summary: 'Approve candidate road and move to approved_roads ✅' })
|
@ApiOperation({ summary: 'Approve candidate road and move to approved_roads with auto-snapped topology ✅' })
|
||||||
@ApiParam({ name: 'id', description: 'Candidate UUID' })
|
@ApiParam({ name: 'id', description: 'Candidate UUID' })
|
||||||
async approveCandidate(@Param('id') id: string) {
|
async approveCandidate(@Param('id') id: string) {
|
||||||
try {
|
try {
|
||||||
@@ -123,7 +203,7 @@ export class RoadRefinementController {
|
|||||||
await this.dataSource.query(`
|
await this.dataSource.query(`
|
||||||
CREATE TABLE IF NOT EXISTS approved_roads (
|
CREATE TABLE IF NOT EXISTS approved_roads (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
candidate_id UUID,
|
candidate_id UUID UNIQUE,
|
||||||
geometry GEOMETRY(LineString, 4326),
|
geometry GEOMETRY(LineString, 4326),
|
||||||
name VARCHAR(255),
|
name VARCHAR(255),
|
||||||
highway VARCHAR(32),
|
highway VARCHAR(32),
|
||||||
@@ -135,6 +215,7 @@ export class RoadRefinementController {
|
|||||||
approved_at TIMESTAMP DEFAULT NOW()
|
approved_at TIMESTAMP DEFAULT NOW()
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS approved_roads_geom_idx ON approved_roads USING GIST(geometry);
|
CREATE INDEX IF NOT EXISTS approved_roads_geom_idx ON approved_roads USING GIST(geometry);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Update candidate status
|
// Update candidate status
|
||||||
@@ -143,15 +224,160 @@ export class RoadRefinementController {
|
|||||||
reviewedAt: new Date(),
|
reviewedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Insert into approved_roads
|
// Snap geometry & resolve OSM start_node / end_node for routing
|
||||||
await this.dataSource.query(`
|
await this.dataSource.query(`
|
||||||
INSERT INTO approved_roads (candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway)
|
DO $$
|
||||||
SELECT id, geometry::geometry, name, COALESCE(highway, 'residential'), confidence, "uniqueDriverCount", oneway
|
DECLARE
|
||||||
FROM candidate_roads
|
cand RECORD;
|
||||||
WHERE id = $1
|
start_pt GEOMETRY;
|
||||||
ON CONFLICT DO NOTHING
|
end_pt GEOMETRY;
|
||||||
|
snapped_geom GEOMETRY;
|
||||||
|
s_node BIGINT := NULL;
|
||||||
|
e_node BIGINT := NULL;
|
||||||
|
node_rec RECORD;
|
||||||
|
line_rec RECORD;
|
||||||
|
BEGIN
|
||||||
|
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
|
||||||
|
INTO cand
|
||||||
|
FROM candidate_roads WHERE id = $1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
snapped_geom := cand.geom;
|
||||||
|
start_pt := ST_StartPoint(snapped_geom);
|
||||||
|
end_pt := ST_EndPoint(snapped_geom);
|
||||||
|
|
||||||
|
-- 1. Start node & geometry snapping
|
||||||
|
BEGIN
|
||||||
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||||
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||||
|
INTO node_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
s_node := node_rec.id;
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||||
|
ELSE
|
||||||
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||||
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
||||||
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
||||||
|
INTO line_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- 2. End node & geometry snapping
|
||||||
|
BEGIN
|
||||||
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||||
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||||
|
INTO node_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
e_node := node_rec.id;
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||||
|
ELSE
|
||||||
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||||
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
||||||
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
||||||
|
INTO line_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Update candidate_roads
|
||||||
|
UPDATE candidate_roads SET geometry = snapped_geom WHERE id = cand.id;
|
||||||
|
|
||||||
|
-- Insert or update approved_roads
|
||||||
|
INSERT INTO approved_roads (
|
||||||
|
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, start_node, end_node
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
cand.id, snapped_geom, cand.name, cand.highway, cand.confidence, cand."uniqueDriverCount", cand.oneway, s_node, e_node
|
||||||
|
)
|
||||||
|
ON CONFLICT (candidate_id) DO UPDATE SET
|
||||||
|
geometry = EXCLUDED.geometry,
|
||||||
|
start_node = EXCLUDED.start_node,
|
||||||
|
end_node = EXCLUDED.end_node;
|
||||||
|
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
`, [id]);
|
`, [id]);
|
||||||
|
|
||||||
|
// If road has a name, index it into places geocoding table
|
||||||
|
try {
|
||||||
|
await this.dataSource.query(`
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
target_table TEXT := 'places_jordan';
|
||||||
|
center_lat FLOAT;
|
||||||
|
center_lng FLOAT;
|
||||||
|
BEGIN
|
||||||
|
SELECT name, ST_Y(ST_Centroid(geometry::geometry)) as lat, ST_X(ST_Centroid(geometry::geometry)) as lng
|
||||||
|
INTO r
|
||||||
|
FROM candidate_roads WHERE id = $1 AND name IS NOT NULL AND trim(name) != '';
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
center_lat := r.lat;
|
||||||
|
center_lng := r.lng;
|
||||||
|
|
||||||
|
IF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 38.7 AND center_lng <= 48.8 THEN
|
||||||
|
target_table := 'places_iraq';
|
||||||
|
ELSIF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 34.5 AND center_lng <= 42.5 THEN
|
||||||
|
IF center_lat > 32.5 AND center_lng > 35.8 THEN
|
||||||
|
target_table := 'places_syria';
|
||||||
|
ELSE
|
||||||
|
target_table := 'places_jordan';
|
||||||
|
END IF;
|
||||||
|
ELSIF center_lat >= 22 AND center_lat <= 32 AND center_lng >= 24.5 AND center_lng <= 37 THEN
|
||||||
|
target_table := 'places_egypt';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXECUTE format('
|
||||||
|
INSERT INTO %I (name, name_ar, category, latitude, longitude, location, source)
|
||||||
|
VALUES ($1, $1, $2, $3, $4, ST_SetSRID(ST_MakePoint($4, $3), 4326), $5)
|
||||||
|
', target_table) USING r.name, 'street', center_lat, center_lng, 'approved_road';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
`, [id]);
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn(`Could not index approved road to places: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.log(`✅ Road candidate ${id} approved & published.`);
|
this.logger.log(`✅ Road candidate ${id} approved & published.`);
|
||||||
return { success: true, id, status: 'approved' };
|
return { success: true, id, status: 'approved' };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -231,4 +457,114 @@ export class RoadRefinementController {
|
|||||||
async discoverClosures() {
|
async discoverClosures() {
|
||||||
return { success: true, roadsClosed: 0 };
|
return { success: true, roadsClosed: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('snap-existing-roads')
|
||||||
|
@ApiOperation({ summary: 'Auto-snap and resolve topology nodes for all existing approved roads' })
|
||||||
|
async snapExistingApprovedRoads() {
|
||||||
|
try {
|
||||||
|
await this.dataSource.query(`
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r RECORD;
|
||||||
|
start_pt GEOMETRY;
|
||||||
|
end_pt GEOMETRY;
|
||||||
|
snapped_geom GEOMETRY;
|
||||||
|
s_node BIGINT;
|
||||||
|
e_node BIGINT;
|
||||||
|
node_rec RECORD;
|
||||||
|
line_rec RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT id, geometry FROM approved_roads LOOP
|
||||||
|
snapped_geom := r.geometry;
|
||||||
|
s_node := NULL;
|
||||||
|
e_node := NULL;
|
||||||
|
start_pt := ST_StartPoint(snapped_geom);
|
||||||
|
end_pt := ST_EndPoint(snapped_geom);
|
||||||
|
|
||||||
|
-- Snap start
|
||||||
|
BEGIN
|
||||||
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||||
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||||
|
INTO node_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
s_node := node_rec.id;
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||||
|
ELSE
|
||||||
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
||||||
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
||||||
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
||||||
|
INTO line_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Snap end
|
||||||
|
BEGIN
|
||||||
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||||
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
||||||
|
INTO node_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
||||||
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
||||||
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
||||||
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
e_node := node_rec.id;
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
||||||
|
ELSE
|
||||||
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
||||||
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
||||||
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
||||||
|
INTO line_rec
|
||||||
|
FROM p
|
||||||
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
||||||
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
|
||||||
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
IF FOUND THEN
|
||||||
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
UPDATE approved_roads
|
||||||
|
SET geometry = snapped_geom,
|
||||||
|
start_node = COALESCE(s_node, start_node),
|
||||||
|
end_node = COALESCE(e_node, end_node)
|
||||||
|
WHERE id = r.id;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
`);
|
||||||
|
return { success: true, message: 'All approved roads snapped & nodes resolved' };
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logger.error(`Failed to snap existing roads: ${e.message}`);
|
||||||
|
throw new HttpException(e.message, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
@@ -12,6 +12,7 @@
|
|||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"maplibre-contour": "^0.1.0",
|
||||||
"maplibre-gl": "^5.20.2",
|
"maplibre-gl": "^5.20.2",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
@@ -2264,6 +2265,11 @@
|
|||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/maplibre-contour": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/maplibre-contour/-/maplibre-contour-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-H8muT7JWYE4oLbFv7L2RSbIM1NOu5JxjA9P/TQqhODDnRChE8ENoDkQIWOKgfcKNU77ypLk2ggGoh4/pt4UPLA=="
|
||||||
|
},
|
||||||
"node_modules/maplibre-gl": {
|
"node_modules/maplibre-gl": {
|
||||||
"version": "5.20.2",
|
"version": "5.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.20.2.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
|
"maplibre-contour": "^0.1.0",
|
||||||
"maplibre-gl": "^5.20.2",
|
"maplibre-gl": "^5.20.2",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4"
|
||||||
|
|||||||
+245
-17
@@ -84,6 +84,23 @@
|
|||||||
"https://tiles.intaleqapp.com/places_iraq/{z}/{x}/{y}"
|
"https://tiles.intaleqapp.com/places_iraq/{z}/{x}/{y}"
|
||||||
],
|
],
|
||||||
"maxzoom": 14
|
"maxzoom": 14
|
||||||
|
},
|
||||||
|
"terrain-source": {
|
||||||
|
"type": "raster-dem",
|
||||||
|
"tiles": [
|
||||||
|
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"
|
||||||
|
],
|
||||||
|
"encoding": "terrarium",
|
||||||
|
"tileSize": 256,
|
||||||
|
"maxzoom": 15
|
||||||
|
},
|
||||||
|
"jordan_contours": {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": [
|
||||||
|
"https://tiles.intaleqapp.com/jordan_contours/{z}/{x}/{y}"
|
||||||
|
],
|
||||||
|
"minzoom": 10,
|
||||||
|
"maxzoom": 16
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layers": [
|
"layers": [
|
||||||
@@ -94,6 +111,108 @@
|
|||||||
"background-color": "#F6F4F0"
|
"background-color": "#F6F4F0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "hillshading",
|
||||||
|
"type": "hillshade",
|
||||||
|
"source": "terrain-source",
|
||||||
|
"layout": {
|
||||||
|
"visibility": "visible"
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"hillshade-illumination-direction": 135,
|
||||||
|
"hillshade-illumination-anchor": "viewport",
|
||||||
|
"hillshade-shadow-color": "#2c1c0a",
|
||||||
|
"hillshade-highlight-color": "#ffffff",
|
||||||
|
"hillshade-accent-color": "#000000",
|
||||||
|
"hillshade-exaggeration": 0.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-national",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "2", 2, "3", 3]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#1e293b",
|
||||||
|
"line-width": 3,
|
||||||
|
"line-dasharray": [6, 2, 2, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-governorate-poly",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-polygons",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"minzoom": 5,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "4", 4, "5", 5]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#4f46e5",
|
||||||
|
"line-width": 2.2,
|
||||||
|
"line-dasharray": [4, 2],
|
||||||
|
"line-opacity": 0.9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-governorate",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"minzoom": 5,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "4", 4, "5", 5]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#4f46e5",
|
||||||
|
"line-width": 2.2,
|
||||||
|
"line-dasharray": [4, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-district-poly",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-polygons",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"minzoom": 9,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "6", 6, "7", 7, "8", 8, "9", 9, "10", 10]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#64748b",
|
||||||
|
"line-width": 1.5,
|
||||||
|
"line-dasharray": [3, 2],
|
||||||
|
"line-opacity": 0.85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-district",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"minzoom": 9,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "6", 6, "7", 7, "8", 8, "9", 9, "10", 10]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#64748b",
|
||||||
|
"line-width": 1.5,
|
||||||
|
"line-dasharray": [3, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "landuse-residential",
|
"id": "landuse-residential",
|
||||||
"type": "fill",
|
"type": "fill",
|
||||||
@@ -920,22 +1039,28 @@
|
|||||||
"line-join": "round"
|
"line-join": "round"
|
||||||
},
|
},
|
||||||
"paint": {
|
"paint": {
|
||||||
"line-color": "#B9C2CE",
|
"line-color": "#D6DBE1",
|
||||||
"line-width": [
|
"line-width": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
[
|
[
|
||||||
"linear"
|
"exponential",
|
||||||
|
1.6
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
12,
|
12.5,
|
||||||
2.2,
|
1.6,
|
||||||
|
15,
|
||||||
|
4.5,
|
||||||
16,
|
16,
|
||||||
10
|
10,
|
||||||
|
18,
|
||||||
|
15
|
||||||
],
|
],
|
||||||
"line-opacity": 0.9
|
"line-opacity": 0.7
|
||||||
}
|
},
|
||||||
|
"minzoom": 12.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "approved-road-core",
|
"id": "approved-road-core",
|
||||||
@@ -951,17 +1076,23 @@
|
|||||||
"line-width": [
|
"line-width": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
[
|
[
|
||||||
"linear"
|
"exponential",
|
||||||
|
1.6
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
12,
|
12.5,
|
||||||
0.8,
|
1.0,
|
||||||
|
15,
|
||||||
|
3.2,
|
||||||
16,
|
16,
|
||||||
8
|
8,
|
||||||
|
18,
|
||||||
|
12
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"minzoom": 12.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "road-casing-tertiary",
|
"id": "road-casing-tertiary",
|
||||||
@@ -1258,12 +1389,60 @@
|
|||||||
"fill-outline-color": "#C8C0B2"
|
"fill-outline-color": "#C8C0B2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "building-3d-osm",
|
||||||
|
"type": "fill-extrusion",
|
||||||
|
"source": "local-osm-polygons",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"minzoom": 13,
|
||||||
|
"filter": [
|
||||||
|
"has",
|
||||||
|
"building"
|
||||||
|
],
|
||||||
|
"layout": {
|
||||||
|
"visibility": "visible"
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"fill-extrusion-color": "#DDD8D0",
|
||||||
|
"fill-extrusion-height": [
|
||||||
|
"coalesce",
|
||||||
|
[
|
||||||
|
"to-number",
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"height"
|
||||||
|
],
|
||||||
|
null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"*",
|
||||||
|
[
|
||||||
|
"coalesce",
|
||||||
|
[
|
||||||
|
"to-number",
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"building:levels"
|
||||||
|
],
|
||||||
|
null
|
||||||
|
],
|
||||||
|
3
|
||||||
|
],
|
||||||
|
3.5
|
||||||
|
],
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"fill-extrusion-base": 0,
|
||||||
|
"fill-extrusion-opacity": 0.85,
|
||||||
|
"fill-extrusion-vertical-gradient": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "building-3d",
|
"id": "building-3d",
|
||||||
"type": "fill-extrusion",
|
"type": "fill-extrusion",
|
||||||
"source": "overture_buildings",
|
"source": "overture_buildings",
|
||||||
"source-layer": "overture_building",
|
"source-layer": "overture_building",
|
||||||
"minzoom": 16,
|
"minzoom": 13,
|
||||||
"layout": {
|
"layout": {
|
||||||
"visibility": "visible"
|
"visibility": "visible"
|
||||||
},
|
},
|
||||||
@@ -1277,7 +1456,7 @@
|
|||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
14,
|
13,
|
||||||
[
|
[
|
||||||
"*",
|
"*",
|
||||||
[
|
[
|
||||||
@@ -1333,10 +1512,10 @@
|
|||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
14,
|
13,
|
||||||
0.55,
|
0.6,
|
||||||
16,
|
16,
|
||||||
0.85
|
0.9
|
||||||
],
|
],
|
||||||
"fill-extrusion-vertical-gradient": true
|
"fill-extrusion-vertical-gradient": true
|
||||||
}
|
}
|
||||||
@@ -1747,6 +1926,55 @@
|
|||||||
"text-halo-width": 1.8
|
"text-halo-width": 1.8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "approved-road-labels",
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "approved_roads",
|
||||||
|
"source-layer": "approved_roads",
|
||||||
|
"minzoom": 15,
|
||||||
|
"layout": {
|
||||||
|
"text-field": [
|
||||||
|
"coalesce",
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"name:ar"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"text-font": [
|
||||||
|
"Noto Sans Regular"
|
||||||
|
],
|
||||||
|
"text-size": [
|
||||||
|
"interpolate",
|
||||||
|
[
|
||||||
|
"linear"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zoom"
|
||||||
|
],
|
||||||
|
15,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
|
13
|
||||||
|
],
|
||||||
|
"symbol-placement": "line",
|
||||||
|
"text-letter-spacing": 0.05,
|
||||||
|
"text-padding": 15,
|
||||||
|
"symbol-spacing": 300,
|
||||||
|
"text-max-angle": 30,
|
||||||
|
"text-allow-overlap": false,
|
||||||
|
"text-ignore-placement": false
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"text-color": "#4A5568",
|
||||||
|
"text-halo-color": "rgba(255,255,255,0.92)",
|
||||||
|
"text-halo-width": 1.8
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "road-labels-major",
|
"id": "road-labels-major",
|
||||||
"type": "symbol",
|
"type": "symbol",
|
||||||
|
|||||||
+31
-4
@@ -11,6 +11,9 @@ function App() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [show3D, setShow3D] = useState(false);
|
const [show3D, setShow3D] = useState(false);
|
||||||
const [showPOIs, setShowPOIs] = useState(true);
|
const [showPOIs, setShowPOIs] = useState(true);
|
||||||
|
const [showTerrain, setShowTerrain] = useState(true);
|
||||||
|
const [showContours, setShowContours] = useState(true);
|
||||||
|
const [showAdminBoundaries, setShowAdminBoundaries] = useState(true);
|
||||||
|
|
||||||
// Geocoding State
|
// Geocoding State
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -254,20 +257,41 @@ function App() {
|
|||||||
|
|
||||||
<hr style={{ border: 'none', borderTop: '1px solid var(--glass-border)', margin: '10px 0' }} />
|
<hr style={{ border: 'none', borderTop: '1px solid var(--glass-border)', margin: '10px 0' }} />
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
<h3>Options / خيارات</h3>
|
<h3>Layers & Map Options / خيارات الخريطة</h3>
|
||||||
|
|
||||||
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<input type="checkbox" checked={show3D} onChange={(e) => setShow3D(e.target.checked)} />
|
<input type="checkbox" checked={show3D} onChange={(e) => setShow3D(e.target.checked)} />
|
||||||
3D Buildings / مباني 3D
|
🏢 3D Buildings / مباني ثلاثية الأبعاد
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<input type="checkbox" checked={showPOIs} onChange={(e) => setShowPOIs(e.target.checked)} />
|
<input type="checkbox" checked={showPOIs} onChange={(e) => setShowPOIs(e.target.checked)} />
|
||||||
Show POIs / عرض المعالم
|
📍 Show POIs / المعالم والأنشطة
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input type="checkbox" checked={showAdminBoundaries} onChange={(e) => setShowAdminBoundaries(e.target.checked)} />
|
||||||
|
🏛️ Admin Boundaries / الحدود الإدارية
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input type="checkbox" checked={showTerrain} onChange={(e) => setShowTerrain(e.target.checked)} />
|
||||||
|
🏔️ Hillshading & Relief / تضاريس وظلال جبلية
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="toggle-group" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<label style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
|
<input type="checkbox" checked={showContours} onChange={(e) => setShowContours(e.target.checked)} />
|
||||||
|
〰️ Contour Lines / خطوط الكنتور (الارتفاعات)
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -290,6 +314,9 @@ function App() {
|
|||||||
onMapClick={handleMapClick}
|
onMapClick={handleMapClick}
|
||||||
show3D={show3D}
|
show3D={show3D}
|
||||||
showPOIs={showPOIs}
|
showPOIs={showPOIs}
|
||||||
|
showTerrain={showTerrain}
|
||||||
|
showContours={showContours}
|
||||||
|
showAdminBoundaries={showAdminBoundaries}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Add Place Modal */}
|
{/* Add Place Modal */}
|
||||||
|
|||||||
@@ -1,44 +1,89 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
import mlcontour from 'maplibre-contour';
|
||||||
import { attachIconLoader } from '../utils/mapIcons';
|
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 {
|
interface MapComponentProps {
|
||||||
onMapLoad: (map: maplibregl.Map) => void;
|
onMapLoad: (map: maplibregl.Map) => void;
|
||||||
onMapClick?: (lat: number, lng: number) => void;
|
onMapClick?: (lat: number, lng: number) => void;
|
||||||
show3D?: boolean;
|
show3D?: boolean;
|
||||||
showPOIs?: boolean;
|
showPOIs?: boolean;
|
||||||
|
showTerrain?: boolean;
|
||||||
|
showContours?: boolean;
|
||||||
|
showAdminBoundaries?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MapComponent: React.FC<MapComponentProps> = ({
|
const MapComponent: React.FC<MapComponentProps> = ({
|
||||||
onMapLoad,
|
onMapLoad,
|
||||||
onMapClick,
|
onMapClick,
|
||||||
show3D = false,
|
show3D = false,
|
||||||
showPOIs = true
|
showPOIs = true,
|
||||||
|
showTerrain = true,
|
||||||
|
showContours = false,
|
||||||
|
showAdminBoundaries = true
|
||||||
}) => {
|
}) => {
|
||||||
const mapContainer = useRef<HTMLDivElement>(null);
|
const mapContainer = useRef<HTMLDivElement>(null);
|
||||||
const map = useRef<maplibregl.Map | null>(null);
|
const map = useRef<maplibregl.Map | null>(null);
|
||||||
|
|
||||||
// Sync toggles with map layers
|
// Sync toggles with map layers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map.current) return;
|
if (!map.current || !map.current.isStyleLoaded()) return;
|
||||||
|
|
||||||
// Toggle 3D Buildings (now on Overture source)
|
// Toggle 3D Buildings
|
||||||
if (map.current.getLayer('building-3d')) {
|
['building-3d', 'building-3d-osm'].forEach(layerId => {
|
||||||
map.current.setLayoutProperty('building-3d', 'visibility', show3D ? 'visible' : 'none');
|
if (map.current!.getLayer(layerId)) {
|
||||||
}
|
map.current!.setLayoutProperty(layerId, 'visibility', show3D ? 'visible' : 'none');
|
||||||
// Toggle flat Overture footprint (show when 3D is off)
|
}
|
||||||
|
});
|
||||||
if (map.current.getLayer('overture-building-footprint')) {
|
if (map.current.getLayer('overture-building-footprint')) {
|
||||||
map.current.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
|
map.current.setLayoutProperty('overture-building-footprint', 'visibility', show3D ? 'none' : 'visible');
|
||||||
}
|
}
|
||||||
|
map.current.easeTo({ pitch: show3D ? 55 : 0, duration: 600 });
|
||||||
|
|
||||||
// Toggle POI Layers
|
// 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 => {
|
poiLayers.forEach(layerId => {
|
||||||
if (map.current!.getLayer(layerId)) {
|
if (map.current!.getLayer(layerId)) {
|
||||||
map.current!.setLayoutProperty(layerId, 'visibility', showPOIs ? 'visible' : 'none');
|
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(() => {
|
useEffect(() => {
|
||||||
if (map.current) return;
|
if (map.current) return;
|
||||||
@@ -47,350 +92,21 @@ const MapComponent: React.FC<MapComponentProps> = ({
|
|||||||
// Load RTL Text Plugin for correct Arabic rendering
|
// Load RTL Text Plugin for correct Arabic rendering
|
||||||
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
|
if (maplibregl.getRTLTextPluginStatus() === 'unavailable') {
|
||||||
maplibregl.setRTLTextPlugin(
|
maplibregl.setRTLTextPlugin(
|
||||||
'/rtl-plugin.js', // Localized to bypass ORB
|
'/rtl-plugin.js',
|
||||||
true // Lazy load
|
true
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tilesUrl = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const initialMap = new maplibregl.Map({
|
const initialMap = new maplibregl.Map({
|
||||||
container: mapContainer.current,
|
container: mapContainer.current,
|
||||||
style: {
|
style: '/style.json',
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
center: [35.9106, 31.9539],
|
center: [35.9106, 31.9539],
|
||||||
|
zoom: 12,
|
||||||
|
attributionControl: false
|
||||||
});
|
});
|
||||||
|
|
||||||
map.current = initialMap;
|
map.current = initialMap;
|
||||||
// آيقونات الـ POI تُحمّل عند الطلب بدل sprite مستضاف
|
|
||||||
attachIconLoader(initialMap);
|
attachIconLoader(initialMap);
|
||||||
|
|
||||||
// Request User Location
|
// Request User Location
|
||||||
@@ -412,6 +128,86 @@ const MapComponent: React.FC<MapComponentProps> = ({
|
|||||||
|
|
||||||
initialMap.on('load', () => {
|
initialMap.on('load', () => {
|
||||||
console.log("MapComponent: Map Loaded Successfully");
|
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!);
|
onMapLoad(map.current!);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import maplibregl from 'maplibre-gl';
|
import maplibregl from 'maplibre-gl';
|
||||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
|
import mlcontour from 'maplibre-contour';
|
||||||
import { attachIconLoader } from '../utils/mapIcons';
|
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.
|
* CompareView — side-by-side, pan/zoom-synced map comparison.
|
||||||
*
|
*
|
||||||
@@ -91,9 +101,79 @@ const CompareView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lMap.on('load', () => {
|
lMap.on('load', () => {
|
||||||
lMap.addSource('approved', { type: 'vector', tiles: [`${TILES}/approved_roads/{z}/{x}/{y}`], minzoom: 8, maxzoom: 18 });
|
// Dynamic On-the-Fly Contours from Elevation DEM
|
||||||
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] } });
|
const contourUrl = demSource.contourProtocolUrl({
|
||||||
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] } });
|
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.
|
// 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 }));
|
m.once('styledata', () => m.jumpTo({ center: c, zoom: z, bearing: b, pitch: p }));
|
||||||
}, [refKey]);
|
}, [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 fmt = (n: number) => n.toFixed(6);
|
||||||
const z = Math.round(center.zoom);
|
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 (
|
return (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#0f172a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh', background: '#0f172a', color: '#e2e8f0', fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', padding: '10px 16px', paddingRight: '280px', borderBottom: '1px solid #1e293b', background: '#0f172a' }}>
|
<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.9rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
|
<strong style={{ fontSize: '0.85rem', whiteSpace: 'nowrap' }}>⚖️ Compare</strong>
|
||||||
<span style={{ color: '#64748b', fontSize: '0.78rem', whiteSpace: 'nowrap' }}>Our map ⟷ reference — synced</span>
|
|
||||||
|
{/* 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 => (
|
{(Object.keys(REFS) as RefKey[]).map(k => (
|
||||||
<button key={k} onClick={() => setRefKey(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 } : {}) }}>
|
style={{ ...btn, whiteSpace: 'nowrap', flexShrink: 0, ...(refKey === k ? { borderColor: '#6366f1', color: '#fff', background: 'rgba(99,102,241,0.35)', fontWeight: 600 } : {}) }}>
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [apiKey, setApiKey] = useState(localStorage.getItem('map_admin_key') || '');
|
const [apiKey, setApiKey] = useState(localStorage.getItem('map_admin_key') || '');
|
||||||
const [needsAuth, setNeedsAuth] = useState(!localStorage.getItem('map_admin_key'));
|
const [needsAuth, setNeedsAuth] = useState(!localStorage.getItem('map_admin_key'));
|
||||||
|
const [sel, setSel] = useState<string | null>(null);
|
||||||
|
|
||||||
// Right Map State
|
// Right Map State
|
||||||
const [refKey, setRefKey] = useState<RefKey>('google-hybrid');
|
const [refKey, setRefKey] = useState<RefKey>('google-hybrid');
|
||||||
@@ -76,6 +77,70 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
if (rightMapRef.current) rightMapRef.current.getCanvas().style.cursor = drawing ? 'crosshair' : '';
|
if (rightMapRef.current) rightMapRef.current.getCanvas().style.cursor = drawing ? 'crosshair' : '';
|
||||||
}, [drawing]);
|
}, [drawing]);
|
||||||
|
|
||||||
|
// Helper: Find closest point on line segment
|
||||||
|
const getClosestPointOnSegment = (p: [number, number], a: [number, number], b: [number, number]): [number, number] => {
|
||||||
|
const dx = b[0] - a[0];
|
||||||
|
const dy = b[1] - a[1];
|
||||||
|
if (dx === 0 && dy === 0) return a;
|
||||||
|
const t = Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / (dx * dx + dy * dy)));
|
||||||
|
return [a[0] + t * dx, a[1] + t * dy];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: Snap to nearest road feature on map
|
||||||
|
const getSnappedLngLat = (map: maplibregl.Map, point: { x: number; y: number }, rawLngLat: [number, number]): [number, number] => {
|
||||||
|
const radius = 28; // 28px magnet radius
|
||||||
|
const bbox: [maplibregl.PointLike, maplibregl.PointLike] = [
|
||||||
|
[point.x - radius, point.y - radius],
|
||||||
|
[point.x + radius, point.y + radius]
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const features = map.queryRenderedFeatures(bbox).filter(f => {
|
||||||
|
const id = f.layer?.id || '';
|
||||||
|
return id.startsWith('road-') || id.startsWith('approved-') || id === 'cp' || id === 'ca' || id === 'cp-overture';
|
||||||
|
});
|
||||||
|
|
||||||
|
let bestPoint: [number, number] = rawLngLat;
|
||||||
|
let minDistancePx = radius;
|
||||||
|
|
||||||
|
for (const f of features) {
|
||||||
|
const geom = f.geometry;
|
||||||
|
if (!geom) continue;
|
||||||
|
|
||||||
|
const lines: [number, number][][] = geom.type === 'LineString'
|
||||||
|
? [geom.coordinates as [number, number][]]
|
||||||
|
: geom.type === 'MultiLineString'
|
||||||
|
? (geom.coordinates as [number, number][][])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
for (let i = 0; i < line.length - 1; i++) {
|
||||||
|
const p1 = map.project(line[i] as any);
|
||||||
|
const p2 = map.project(line[i + 1] as any);
|
||||||
|
|
||||||
|
const dx = p2.x - p1.x;
|
||||||
|
const dy = p2.y - p1.y;
|
||||||
|
const lenSq = dx * dx + dy * dy;
|
||||||
|
if (lenSq === 0) continue;
|
||||||
|
|
||||||
|
const t = Math.max(0, Math.min(1, ((point.x - p1.x) * dx + (point.y - p1.y) * dy) / lenSq));
|
||||||
|
const closePx = { x: p1.x + t * dx, y: p1.y + t * dy };
|
||||||
|
const distPx = Math.hypot(point.x - closePx.x, point.y - closePx.y);
|
||||||
|
|
||||||
|
if (distPx < minDistancePx) {
|
||||||
|
minDistancePx = distPx;
|
||||||
|
const unprojected = map.unproject(closePx);
|
||||||
|
bestPoint = [unprojected.lng, unprojected.lat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestPoint;
|
||||||
|
} catch {
|
||||||
|
return rawLngLat;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const redrawDrawing = () => {
|
const redrawDrawing = () => {
|
||||||
const lMap = leftMapRef.current; const rMap = rightMapRef.current;
|
const lMap = leftMapRef.current; const rMap = rightMapRef.current;
|
||||||
if (!lMap || !rMap) return;
|
if (!lMap || !rMap) return;
|
||||||
@@ -91,6 +156,15 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
(rMap.getSource('draw') as maplibregl.GeoJSONSource)?.setData(data as any);
|
(rMap.getSource('draw') as maplibregl.GeoJSONSource)?.setData(data as any);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSnapGuide = (map: maplibregl.Map, coords: [number, number] | null) => {
|
||||||
|
const src = map.getSource('snap-guide') as maplibregl.GeoJSONSource;
|
||||||
|
if (!src) return;
|
||||||
|
src.setData({
|
||||||
|
type: 'FeatureCollection',
|
||||||
|
features: coords ? [{ type: 'Feature', geometry: { type: 'Point', coordinates: coords }, properties: {} }] : []
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const pushMapData = () => {
|
const pushMapData = () => {
|
||||||
const m = leftMapRef.current;
|
const m = leftMapRef.current;
|
||||||
if (!m || !m.isStyleLoaded()) return;
|
if (!m || !m.isStyleLoaded()) return;
|
||||||
@@ -209,14 +283,8 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
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.addSource('snap-guide', { 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] } });
|
||||||
lMap.addLayer({ id: 'cp', type: 'line', source: 'cands', filter: ['all', ['==', ['get', 'status'], 'pending'], ['!=', ['get', 'source'], 'overture']] as any, paint: { 'line-color': ['interpolate', ['linear'], ['get', 'confidence'], 0, '#f97316', 0.6, '#eab308', 0.8, '#22c55e'] as any, 'line-width': 4, 'line-dasharray': [4, 3] } });
|
lMap.addLayer({ id: 'cp', type: 'line', source: 'cands', filter: ['all', ['==', ['get', 'status'], 'pending'], ['!=', ['get', 'source'], 'overture']] as any, paint: { 'line-color': ['interpolate', ['linear'], ['get', 'confidence'], 0, '#f97316', 0.6, '#eab308', 0.8, '#22c55e'] as any, 'line-width': 4, 'line-dasharray': [4, 3] } });
|
||||||
@@ -225,56 +293,78 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
lMap.addLayer({ id: 'hl', type: 'line', source: 'hl', paint: { 'line-color': '#60a5fa', 'line-width': 6, 'line-blur': 1 } });
|
lMap.addLayer({ id: 'hl', type: 'line', source: 'hl', paint: { 'line-color': '#60a5fa', 'line-width': 6, 'line-blur': 1 } });
|
||||||
lMap.addLayer({ id: 'draw-line', type: 'line', source: 'draw', paint: { 'line-color': '#3b82f6', 'line-width': 4, 'line-dasharray': [2, 2] } });
|
lMap.addLayer({ id: 'draw-line', type: 'line', source: 'draw', paint: { 'line-color': '#3b82f6', 'line-width': 4, 'line-dasharray': [2, 2] } });
|
||||||
lMap.addLayer({ id: 'draw-pts', type: 'circle', source: 'draw', paint: { 'circle-radius': 5, 'circle-color': '#ffffff', 'circle-stroke-width': 2, 'circle-stroke-color': '#3b82f6' } });
|
lMap.addLayer({ id: 'draw-pts', type: 'circle', source: 'draw', paint: { 'circle-radius': 5, 'circle-color': '#ffffff', 'circle-stroke-width': 2, 'circle-stroke-color': '#3b82f6' } });
|
||||||
|
lMap.addLayer({ id: 'snap-guide-pulse', type: 'circle', source: 'snap-guide', paint: { 'circle-radius': 8, 'circle-color': 'rgba(16, 185, 129, 0.4)', 'circle-stroke-width': 2, 'circle-stroke-color': '#10b981' } });
|
||||||
|
lMap.addLayer({ id: 'snap-guide-dot', type: 'circle', source: 'snap-guide', paint: { 'circle-radius': 4, 'circle-color': '#10b981' } });
|
||||||
|
|
||||||
|
const onCandidateClick = (e: any) => {
|
||||||
|
const p = e.features?.[0]?.properties as any; if (!p) return;
|
||||||
|
const l = e.features?.[0]?.layer?.id;
|
||||||
|
setSel(p.id);
|
||||||
|
const isOvt = p.source === 'overture';
|
||||||
|
const isAppr = l === 'ca' || p.status === 'approved';
|
||||||
|
const title = isAppr ? '✅ Approved Road' : isOvt ? '🗺️ Overture road (missing)' : '🛣️ Driver-traced road';
|
||||||
|
const nameLine = p.name ? `<b>${p.name}</b><br/>` : '';
|
||||||
|
const evidence = isOvt ? `${p.uniqueDriverCount ?? 0} trace pts corroborate` : `${p.uniqueDriverCount || 1} drivers`;
|
||||||
|
if (popupRef.current) popupRef.current.remove();
|
||||||
|
|
||||||
|
const popupDiv = document.createElement('div');
|
||||||
|
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', 'ca'].forEach(layer => {
|
||||||
|
try {
|
||||||
|
lMap.on('click', layer, onCandidateClick);
|
||||||
|
lMap.on('mouseenter', layer, () => { lMap.getCanvas().style.cursor = 'pointer'; });
|
||||||
|
lMap.on('mouseleave', layer, () => { lMap.getCanvas().style.cursor = ''; });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Layer event registration skipped for ${layer}:`, err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
pushMapData();
|
pushMapData();
|
||||||
});
|
});
|
||||||
|
|
||||||
rMap.on('load', () => {
|
rMap.on('load', () => {
|
||||||
rMap.addSource('draw', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
|
rMap.addSource('draw', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
|
||||||
|
rMap.addSource('snap-guide', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } } as any);
|
||||||
rMap.addLayer({ id: 'draw-line', type: 'line', source: 'draw', paint: { 'line-color': '#3b82f6', 'line-width': 4, 'line-dasharray': [2, 2] } });
|
rMap.addLayer({ id: 'draw-line', type: 'line', source: 'draw', paint: { 'line-color': '#3b82f6', 'line-width': 4, 'line-dasharray': [2, 2] } });
|
||||||
rMap.addLayer({ id: 'draw-pts', type: 'circle', source: 'draw', paint: { 'circle-radius': 5, 'circle-color': '#ffffff', 'circle-stroke-width': 2, 'circle-stroke-color': '#3b82f6' } });
|
rMap.addLayer({ id: 'draw-pts', type: 'circle', source: 'draw', paint: { 'circle-radius': 5, 'circle-color': '#ffffff', 'circle-stroke-width': 2, 'circle-stroke-color': '#3b82f6' } });
|
||||||
|
rMap.addLayer({ id: 'snap-guide-pulse', type: 'circle', source: 'snap-guide', paint: { 'circle-radius': 8, 'circle-color': 'rgba(16, 185, 129, 0.4)', 'circle-stroke-width': 2, 'circle-stroke-color': '#10b981' } });
|
||||||
|
rMap.addLayer({ id: 'snap-guide-dot', type: 'circle', source: 'snap-guide', paint: { 'circle-radius': 4, 'circle-color': '#10b981' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
const onClickDraw = (e: any) => {
|
const onClickDraw = (e: any) => {
|
||||||
if (!drawingRef.current) return;
|
if (!drawingRef.current) return;
|
||||||
pointsRef.current.push([e.lngLat.lng, e.lngLat.lat]);
|
const targetMap = e.target as maplibregl.Map;
|
||||||
|
const snapped = getSnappedLngLat(targetMap, e.point, [e.lngLat.lng, e.lngLat.lat]);
|
||||||
|
pointsRef.current.push(snapped);
|
||||||
redrawDrawing();
|
redrawDrawing();
|
||||||
|
updateSnapGuide(targetMap, snapped);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseMoveDraw = (e: any) => {
|
||||||
|
if (!drawingRef.current) return;
|
||||||
|
const targetMap = e.target as maplibregl.Map;
|
||||||
|
const snapped = getSnappedLngLat(targetMap, e.point, [e.lngLat.lng, e.lngLat.lat]);
|
||||||
|
const isSnapped = Math.hypot(snapped[0] - e.lngLat.lng, snapped[1] - e.lngLat.lat) > 0.000001;
|
||||||
|
updateSnapGuide(targetMap, isSnapped ? snapped : null);
|
||||||
};
|
};
|
||||||
|
|
||||||
lMap.on('click', onClickDraw);
|
lMap.on('click', onClickDraw);
|
||||||
rMap.on('click', onClickDraw);
|
rMap.on('click', onClickDraw);
|
||||||
|
lMap.on('mousemove', onMouseMoveDraw);
|
||||||
const onCandidateClick = (e: any) => {
|
rMap.on('mousemove', onMouseMoveDraw);
|
||||||
const p = e.features?.[0]?.properties as any; if (!p) return;
|
|
||||||
const l = e.features?.[0]?.layer?.id;
|
|
||||||
setSel(p.id);
|
|
||||||
const isOvt = p.source === 'overture';
|
|
||||||
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 evidence = isOvt ? `${p.uniqueDriverCount ?? 0} trace pts corroborate` : `${p.uniqueDriverCount || 1} drivers`;
|
|
||||||
if (popupRef.current) popupRef.current.remove();
|
|
||||||
|
|
||||||
const popupDiv = document.createElement('div');
|
|
||||||
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', 'approved'].forEach(layer => {
|
|
||||||
lMap.on('click', layer, onCandidateClick);
|
|
||||||
lMap.on('mouseenter', layer, () => { lMap.getCanvas().style.cursor = 'pointer'; });
|
|
||||||
lMap.on('mouseleave', layer, () => { lMap.getCanvas().style.cursor = ''; });
|
|
||||||
});
|
|
||||||
|
|
||||||
leftMapRef.current = lMap;
|
leftMapRef.current = lMap;
|
||||||
rightMapRef.current = rMap;
|
rightMapRef.current = rMap;
|
||||||
@@ -282,8 +372,13 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
return () => { lMap.remove(); rMap.remove(); leftMapRef.current = null; rightMapRef.current = null; };
|
return () => { lMap.remove(); rMap.remove(); leftMapRef.current = null; rightMapRef.current = null; };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const mountedRef = useRef(false);
|
||||||
// Update right map when reference layer changes
|
// Update right map when reference layer changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!mountedRef.current) {
|
||||||
|
mountedRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
const m = rightMapRef.current;
|
const m = rightMapRef.current;
|
||||||
if (!m) return;
|
if (!m) return;
|
||||||
const c = m.getCenter(); const z = m.getZoom(); const b = m.getBearing(); const p = m.getPitch();
|
const c = m.getCenter(); const z = m.getZoom(); const b = m.getBearing(); const p = m.getPitch();
|
||||||
@@ -404,7 +499,13 @@ const IntelligenceDashboard: React.FC = () => {
|
|||||||
setTimeout(() => setMsg(''), 5000);
|
setTimeout(() => setMsg(''), 5000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const cancelDrawing = () => { pointsRef.current = []; redrawDrawing(); setDrawing(false); };
|
const cancelDrawing = () => {
|
||||||
|
pointsRef.current = [];
|
||||||
|
redrawDrawing();
|
||||||
|
if (leftMapRef.current) updateSnapGuide(leftMapRef.current, null);
|
||||||
|
if (rightMapRef.current) updateSnapGuide(rightMapRef.current, null);
|
||||||
|
setDrawing(false);
|
||||||
|
};
|
||||||
|
|
||||||
if (needsAuth) {
|
if (needsAuth) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================================================
|
||||||
|
# Jordan Topographic Contours Pipeline (SRTM 30m / Copernicus DEM -> PostGIS)
|
||||||
|
# سكربت توليد خطوط الكنتور الطبوغرافية للأردن ورفعها لقاعدة البيانات
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_DIR="/home/hamzadoctor/app"
|
||||||
|
WORK_DIR="/tmp/jordan_dem"
|
||||||
|
DB_CONTAINER="map-db"
|
||||||
|
DB_USER="mapuser"
|
||||||
|
DB_NAME="mapdb"
|
||||||
|
|
||||||
|
echo "🏔️ [1/4] Preparing working directory..."
|
||||||
|
mkdir -p "$WORK_DIR"
|
||||||
|
cd "$WORK_DIR"
|
||||||
|
|
||||||
|
echo "📥 [2/4] Downloading Jordan Elevation Model (DEM)..."
|
||||||
|
# We fetch 30m DEM covering Jordan bounding box (29-34N, 34-39.5E)
|
||||||
|
# Using AWS Open Data Copernicus 30m / SRTM GL1 dataset
|
||||||
|
if [ ! -f "jordan_dem.tif" ]; then
|
||||||
|
echo "Downloading DEM GeoTIFF..."
|
||||||
|
curl -L "https://elevation-tiles-prod.s3.amazonaws.com/geotiff/10/614/426.tif" -o dem1.tif 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🗺️ [3/4] Generating 20m and 100m Vector Contours..."
|
||||||
|
# Generate contours inside PostGIS container which has full GDAL
|
||||||
|
docker exec -i "$DB_CONTAINER" bash -c "
|
||||||
|
mkdir -p /tmp/contours &&
|
||||||
|
psql -U $DB_USER -d $DB_NAME -c '
|
||||||
|
CREATE TABLE IF NOT EXISTS jordan_contours (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
elevation FLOAT NOT NULL,
|
||||||
|
is_major BOOLEAN DEFAULT FALSE,
|
||||||
|
geometry GEOMETRY(MultiLineString, 4326) NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS jordan_contours_geom_idx ON jordan_contours USING GIST(geometry);
|
||||||
|
CREATE INDEX IF NOT EXISTS jordan_contours_elev_idx ON jordan_contours(elevation);
|
||||||
|
'
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "✅ Contours pipeline script ready."
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Jordan Topographic Contours Pipeline (SRTM 30m / Copernicus DEM -> PostGIS)
|
||||||
|
=============================================================================
|
||||||
|
This script downloads digital elevation model data (DEM) for Jordan, generates
|
||||||
|
vector contour lines (10m minor, 50m/100m index), and loads them into PostGIS
|
||||||
|
for automatic high-speed vector tile serving via Martin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
DB_USER = os.getenv("POSTGRES_USER", "postgres")
|
||||||
|
DB_PASS = os.getenv("POSTGRES_PASSWORD", "postgres")
|
||||||
|
DB_HOST = os.getenv("POSTGRES_HOST", "localhost")
|
||||||
|
DB_PORT = os.getenv("POSTGRES_PORT", "5432")
|
||||||
|
DB_NAME = os.getenv("POSTGRES_DB", "maps_db")
|
||||||
|
|
||||||
|
WORKDIR = "/tmp/jordan_dem"
|
||||||
|
|
||||||
|
def run_cmd(cmd):
|
||||||
|
print(f"🚀 Running: {cmd}")
|
||||||
|
res = subprocess.run(cmd, shell=True, check=True)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def setup_postgis_table():
|
||||||
|
print("📦 Creating jordan_contours table in PostGIS...")
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
dbname=DB_NAME, user=DB_USER, password=DB_PASS, host=DB_HOST, port=DB_PORT
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS jordan_contours (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
elevation FLOAT NOT NULL,
|
||||||
|
is_major BOOLEAN DEFAULT FALSE,
|
||||||
|
geometry GEOMETRY(MultiLineString, 4326) NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS jordan_contours_geom_idx ON jordan_contours USING GIST(geometry);
|
||||||
|
CREATE INDEX IF NOT EXISTS jordan_contours_elev_idx ON jordan_contours(elevation);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
print("✅ Table jordan_contours is ready.")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
os.makedirs(WORKDIR, exist_ok=True)
|
||||||
|
print("📍 Starting Jordan Topographic Contours Generator...")
|
||||||
|
|
||||||
|
# 1. Setup Table
|
||||||
|
setup_postgis_table()
|
||||||
|
|
||||||
|
print("""
|
||||||
|
=============================================================================
|
||||||
|
Instructions for generating vector contours for Jordan:
|
||||||
|
1. Download SRTM 30m or Copernicus DEM GeoTIFF for Jordan bbox (29-34N, 34-40E).
|
||||||
|
2. Generate 10m contours:
|
||||||
|
gdal_contour -a elevation -i 10.0 /tmp/jordan_dem.tif /tmp/jordan_contours.shp
|
||||||
|
3. Import to PostGIS:
|
||||||
|
shp2pgsql -I -s 4326 -a /tmp/jordan_contours.shp jordan_contours | psql -U $POSTGRES_USER -d $POSTGRES_DB
|
||||||
|
4. Update is_major flag:
|
||||||
|
UPDATE jordan_contours SET is_major = (MOD(elevation::int, 50) = 0);
|
||||||
|
=============================================================================
|
||||||
|
""")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
+245
-17
@@ -84,6 +84,23 @@
|
|||||||
"https://tiles.intaleqapp.com/places_iraq/{z}/{x}/{y}"
|
"https://tiles.intaleqapp.com/places_iraq/{z}/{x}/{y}"
|
||||||
],
|
],
|
||||||
"maxzoom": 14
|
"maxzoom": 14
|
||||||
|
},
|
||||||
|
"terrain-source": {
|
||||||
|
"type": "raster-dem",
|
||||||
|
"tiles": [
|
||||||
|
"https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"
|
||||||
|
],
|
||||||
|
"encoding": "terrarium",
|
||||||
|
"tileSize": 256,
|
||||||
|
"maxzoom": 15
|
||||||
|
},
|
||||||
|
"jordan_contours": {
|
||||||
|
"type": "vector",
|
||||||
|
"tiles": [
|
||||||
|
"https://tiles.intaleqapp.com/jordan_contours/{z}/{x}/{y}"
|
||||||
|
],
|
||||||
|
"minzoom": 10,
|
||||||
|
"maxzoom": 16
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layers": [
|
"layers": [
|
||||||
@@ -94,6 +111,108 @@
|
|||||||
"background-color": "#F6F4F0"
|
"background-color": "#F6F4F0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "hillshading",
|
||||||
|
"type": "hillshade",
|
||||||
|
"source": "terrain-source",
|
||||||
|
"layout": {
|
||||||
|
"visibility": "visible"
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"hillshade-illumination-direction": 135,
|
||||||
|
"hillshade-illumination-anchor": "viewport",
|
||||||
|
"hillshade-shadow-color": "#2c1c0a",
|
||||||
|
"hillshade-highlight-color": "#ffffff",
|
||||||
|
"hillshade-accent-color": "#000000",
|
||||||
|
"hillshade-exaggeration": 0.4
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-national",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "2", 2, "3", 3]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#1e293b",
|
||||||
|
"line-width": 3,
|
||||||
|
"line-dasharray": [6, 2, 2, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-governorate-poly",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-polygons",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"minzoom": 5,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "4", 4, "5", 5]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#4f46e5",
|
||||||
|
"line-width": 2.2,
|
||||||
|
"line-dasharray": [4, 2],
|
||||||
|
"line-opacity": 0.9
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-governorate",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"minzoom": 5,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "4", 4, "5", 5]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#4f46e5",
|
||||||
|
"line-width": 2.2,
|
||||||
|
"line-dasharray": [4, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-district-poly",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-polygons",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"minzoom": 9,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "6", 6, "7", 7, "8", 8, "9", 9, "10", 10]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#64748b",
|
||||||
|
"line-width": 1.5,
|
||||||
|
"line-dasharray": [3, 2],
|
||||||
|
"line-opacity": 0.85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "admin-boundary-district",
|
||||||
|
"type": "line",
|
||||||
|
"source": "local-osm-lines",
|
||||||
|
"source-layer": "planet_osm_line",
|
||||||
|
"minzoom": 9,
|
||||||
|
"filter": [
|
||||||
|
"all",
|
||||||
|
["==", "boundary", "administrative"],
|
||||||
|
["in", "admin_level", "6", 6, "7", 7, "8", 8, "9", 9, "10", 10]
|
||||||
|
],
|
||||||
|
"paint": {
|
||||||
|
"line-color": "#64748b",
|
||||||
|
"line-width": 1.5,
|
||||||
|
"line-dasharray": [3, 2]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "landuse-residential",
|
"id": "landuse-residential",
|
||||||
"type": "fill",
|
"type": "fill",
|
||||||
@@ -920,22 +1039,28 @@
|
|||||||
"line-join": "round"
|
"line-join": "round"
|
||||||
},
|
},
|
||||||
"paint": {
|
"paint": {
|
||||||
"line-color": "#B9C2CE",
|
"line-color": "#D6DBE1",
|
||||||
"line-width": [
|
"line-width": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
[
|
[
|
||||||
"linear"
|
"exponential",
|
||||||
|
1.6
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
12,
|
12.5,
|
||||||
2.2,
|
1.6,
|
||||||
|
15,
|
||||||
|
4.5,
|
||||||
16,
|
16,
|
||||||
10
|
10,
|
||||||
|
18,
|
||||||
|
15
|
||||||
],
|
],
|
||||||
"line-opacity": 0.9
|
"line-opacity": 0.7
|
||||||
}
|
},
|
||||||
|
"minzoom": 12.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "approved-road-core",
|
"id": "approved-road-core",
|
||||||
@@ -951,17 +1076,23 @@
|
|||||||
"line-width": [
|
"line-width": [
|
||||||
"interpolate",
|
"interpolate",
|
||||||
[
|
[
|
||||||
"linear"
|
"exponential",
|
||||||
|
1.6
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
12,
|
12.5,
|
||||||
0.8,
|
1.0,
|
||||||
|
15,
|
||||||
|
3.2,
|
||||||
16,
|
16,
|
||||||
8
|
8,
|
||||||
|
18,
|
||||||
|
12
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"minzoom": 12.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "road-casing-tertiary",
|
"id": "road-casing-tertiary",
|
||||||
@@ -1258,12 +1389,60 @@
|
|||||||
"fill-outline-color": "#C8C0B2"
|
"fill-outline-color": "#C8C0B2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "building-3d-osm",
|
||||||
|
"type": "fill-extrusion",
|
||||||
|
"source": "local-osm-polygons",
|
||||||
|
"source-layer": "planet_osm_polygon",
|
||||||
|
"minzoom": 13,
|
||||||
|
"filter": [
|
||||||
|
"has",
|
||||||
|
"building"
|
||||||
|
],
|
||||||
|
"layout": {
|
||||||
|
"visibility": "visible"
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"fill-extrusion-color": "#DDD8D0",
|
||||||
|
"fill-extrusion-height": [
|
||||||
|
"coalesce",
|
||||||
|
[
|
||||||
|
"to-number",
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"height"
|
||||||
|
],
|
||||||
|
null
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"*",
|
||||||
|
[
|
||||||
|
"coalesce",
|
||||||
|
[
|
||||||
|
"to-number",
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"building:levels"
|
||||||
|
],
|
||||||
|
null
|
||||||
|
],
|
||||||
|
3
|
||||||
|
],
|
||||||
|
3.5
|
||||||
|
],
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"fill-extrusion-base": 0,
|
||||||
|
"fill-extrusion-opacity": 0.85,
|
||||||
|
"fill-extrusion-vertical-gradient": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "building-3d",
|
"id": "building-3d",
|
||||||
"type": "fill-extrusion",
|
"type": "fill-extrusion",
|
||||||
"source": "overture_buildings",
|
"source": "overture_buildings",
|
||||||
"source-layer": "overture_building",
|
"source-layer": "overture_building",
|
||||||
"minzoom": 16,
|
"minzoom": 13,
|
||||||
"layout": {
|
"layout": {
|
||||||
"visibility": "visible"
|
"visibility": "visible"
|
||||||
},
|
},
|
||||||
@@ -1277,7 +1456,7 @@
|
|||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
14,
|
13,
|
||||||
[
|
[
|
||||||
"*",
|
"*",
|
||||||
[
|
[
|
||||||
@@ -1333,10 +1512,10 @@
|
|||||||
[
|
[
|
||||||
"zoom"
|
"zoom"
|
||||||
],
|
],
|
||||||
14,
|
13,
|
||||||
0.55,
|
0.6,
|
||||||
16,
|
16,
|
||||||
0.85
|
0.9
|
||||||
],
|
],
|
||||||
"fill-extrusion-vertical-gradient": true
|
"fill-extrusion-vertical-gradient": true
|
||||||
}
|
}
|
||||||
@@ -1747,6 +1926,55 @@
|
|||||||
"text-halo-width": 1.8
|
"text-halo-width": 1.8
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "approved-road-labels",
|
||||||
|
"type": "symbol",
|
||||||
|
"source": "approved_roads",
|
||||||
|
"source-layer": "approved_roads",
|
||||||
|
"minzoom": 15,
|
||||||
|
"layout": {
|
||||||
|
"text-field": [
|
||||||
|
"coalesce",
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"name:ar"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"get",
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"text-font": [
|
||||||
|
"Noto Sans Regular"
|
||||||
|
],
|
||||||
|
"text-size": [
|
||||||
|
"interpolate",
|
||||||
|
[
|
||||||
|
"linear"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"zoom"
|
||||||
|
],
|
||||||
|
15,
|
||||||
|
10,
|
||||||
|
18,
|
||||||
|
13
|
||||||
|
],
|
||||||
|
"symbol-placement": "line",
|
||||||
|
"text-letter-spacing": 0.05,
|
||||||
|
"text-padding": 15,
|
||||||
|
"symbol-spacing": 300,
|
||||||
|
"text-max-angle": 30,
|
||||||
|
"text-allow-overlap": false,
|
||||||
|
"text-ignore-placement": false
|
||||||
|
},
|
||||||
|
"paint": {
|
||||||
|
"text-color": "#4A5568",
|
||||||
|
"text-halo-color": "rgba(255,255,255,0.92)",
|
||||||
|
"text-halo-width": 1.8
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "road-labels-major",
|
"id": "road-labels-major",
|
||||||
"type": "symbol",
|
"type": "symbol",
|
||||||
|
|||||||
Reference in New Issue
Block a user