feat(gis): restore premium OSM style and fix international routing bounds for Jordan & Syria
This commit is contained in:
@@ -7,55 +7,36 @@ export class MapsService {
|
|||||||
private readonly graphHopperUrl: string;
|
private readonly graphHopperUrl: string;
|
||||||
|
|
||||||
constructor(private configService: ConfigService) {
|
constructor(private configService: ConfigService) {
|
||||||
this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://map-routing:8080');
|
this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://routing:8080');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRoute(from: [number, number], to: [number, number]) {
|
async getRoute(from: [number, number], to: [number, number]) {
|
||||||
// Proxy routing request to GraphHopper
|
|
||||||
// تحويل طلب حساب المسارم إلى GraphHopper
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`${this.graphHopperUrl}/route`, {
|
const payload: any = {
|
||||||
params: {
|
points: [[from[1], from[0]], [to[1], to[0]]],
|
||||||
point: [`${from[0]},${from[1]}`, `${to[0]},${to[1]}`],
|
profile: 'car',
|
||||||
profile: 'car',
|
locale: 'en',
|
||||||
locale: 'en',
|
calc_points: true,
|
||||||
calc_points: true,
|
points_encoded: true,
|
||||||
points_encoded: false,
|
};
|
||||||
elevation: false,
|
|
||||||
type: 'json',
|
console.log(`Routing Request: ${from} -> ${to} via ${this.graphHopperUrl}`);
|
||||||
},
|
const response = await axios.post(`${this.graphHopperUrl}/route`, payload, { timeout: 5000 });
|
||||||
paramsSerializer: (params) => {
|
|
||||||
const searchParams = new URLSearchParams();
|
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach(v => searchParams.append(key, v));
|
|
||||||
} else {
|
|
||||||
searchParams.append(key, value as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return searchParams.toString();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Routing Successful:', response.request.path);
|
|
||||||
|
|
||||||
|
console.log('Routing SUCCESS');
|
||||||
const route = response.data.paths[0];
|
const route = response.data.paths[0];
|
||||||
if (!route) {
|
if (!route) throw new HttpException('No route found', HttpStatus.NOT_FOUND);
|
||||||
throw new HttpException('No route found', HttpStatus.NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
distance: route.distance, // In meters
|
distance: route.distance,
|
||||||
duration: route.time / 1000, // In seconds
|
duration: route.time / 1000,
|
||||||
points: route.points.coordinates, // Array of [lng, lat]
|
points: route.points,
|
||||||
bbox: route.bbox,
|
bbox: route.bbox,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Routing Error:', error.message);
|
const msg = error.response ? `GH Error: ${JSON.stringify(error.response.data)}` : `DNS/Connection Error: ${error.message}`;
|
||||||
if (error.response) {
|
console.error('CRITICAL ROUTING FAILURE:', msg);
|
||||||
console.error('GraphHopper Response:', error.response.data);
|
throw new HttpException(`Routing Failure: ${msg}`, HttpStatus.BAD_GATEWAY);
|
||||||
}
|
|
||||||
throw new HttpException('Error calculating route', HttpStatus.BAD_GATEWAY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ar" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Intaleq Premium Map - خرائط انطلاقة الذكية</title>
|
||||||
|
|
||||||
|
<link href="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css" rel="stylesheet" />
|
||||||
|
<script src="https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body { font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; background: #f8f9fa; overflow: hidden; }
|
||||||
|
#map { width: 100%; height: 100vh; position: absolute; top: 0; left: 0; }
|
||||||
|
.controls {
|
||||||
|
position: absolute; top: 20px; right: 20px; z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(8px);
|
||||||
|
border-radius: 16px; padding: 20px; width: 340px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0,0,0,0.1); border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.branding {
|
||||||
|
position: absolute; bottom: 24px; left: 24px; z-index: 100;
|
||||||
|
background: rgba(255, 255, 255, 0.9); padding: 8px 16px; border-radius: 8px;
|
||||||
|
display: flex; align-items: center; gap: 10px; box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||||
|
pointer-events: none; border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 14px 20px; border: none; border-radius: 10px;
|
||||||
|
font-size: 15px; font-weight: 700; cursor: pointer; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
width: 100%; margin-top: 12px; display: flex; align-items: center; justify-content: center; gap: 10px;
|
||||||
|
}
|
||||||
|
.btn-primary { background: #2563eb; color: white; }
|
||||||
|
.btn-primary:hover { background: #1d4ed8; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); }
|
||||||
|
.info-box {
|
||||||
|
background: #f8fafc; border-radius: 10px; padding: 14px; font-size: 14px; color: #334155; margin-top: 16px;
|
||||||
|
border-right: 4px solid #2563eb; line-height: 1.6; display: none;
|
||||||
|
}
|
||||||
|
#loading { display: none; position: fixed; inset: 0; background: rgba(255,255,255,0.7); z-index: 2000; align-items: center; justify-content: center; backdrop-filter: blur(4px); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
<div id="loading"><strong>جاري معالجة المسار...</strong></div>
|
||||||
|
|
||||||
|
<div class="branding">
|
||||||
|
<img src="/intaleq-logo.png" alt="Logo" style="height: 20px;" onerror="this.style.display='none'">
|
||||||
|
<span style="font-size: 12px; font-weight: 800; color: #0f172a; letter-spacing: 0.5px;">POWERED BY INTALEQ</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<h2 style="font-size: 20px; color: #0f172a; margin-bottom: 6px;">🗺️ خرائط انطلاقة الذكية</h2>
|
||||||
|
<p style="font-size: 13px; color: #64748b; margin-bottom: 16px;">المسار المعتمد: عمان ➔ دمشق</p>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="handleRoute()">
|
||||||
|
<span>🚀</span>
|
||||||
|
<span>حساب المسار الذكي</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="status" class="info-box"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const TILES_URL = 'https://tiles.intaleqapp.com';
|
||||||
|
const API_KEY = 'intaleq_secret_2026';
|
||||||
|
const AMMAN = [35.9106, 31.9539];
|
||||||
|
const DAMASCUS = [36.2765, 33.5138];
|
||||||
|
|
||||||
|
maplibregl.setRTLTextPlugin('https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js', null, true);
|
||||||
|
|
||||||
|
const map = new maplibregl.Map({
|
||||||
|
container: 'map',
|
||||||
|
center: AMMAN,
|
||||||
|
zoom: 12,
|
||||||
|
attributionControl: false,
|
||||||
|
style: './style-mobile.json?v=' + Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
map.on('load', () => {
|
||||||
|
// إضافة مصدر المعالم الخاصة بدمشق (Places Syria)
|
||||||
|
map.addSource('user-landmarks', {
|
||||||
|
type: 'vector',
|
||||||
|
tiles: [`${TILES_URL}/places_syria/{z}/{x}/{y}`],
|
||||||
|
maxzoom: 14
|
||||||
|
});
|
||||||
|
|
||||||
|
// إضافة طبقة الأسماء بدقة عالية تندمج مع الستايل الأصلي
|
||||||
|
map.addLayer({
|
||||||
|
id: 'user-landmarks-labels',
|
||||||
|
type: 'symbol',
|
||||||
|
source: 'user-landmarks',
|
||||||
|
'source-layer': 'places_syria',
|
||||||
|
minzoom: 15, // تظهر فقط عند التقريب الشديد للحفاظ على النظافة
|
||||||
|
layout: {
|
||||||
|
'text-field': '{name_ar}',
|
||||||
|
'text-font': ['Noto Sans Regular'],
|
||||||
|
'text-size': 13,
|
||||||
|
'text-anchor': 'top',
|
||||||
|
'text-offset': [0, 1.2],
|
||||||
|
'text-letter-spacing': 0.05
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'text-color': '#3c4043', // تطابق لون أسماء الشوارع والمعالم الأصلية
|
||||||
|
'text-halo-color': 'rgba(255, 255, 255, 0.9)',
|
||||||
|
'text-halo-width': 2
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// تحميل أيقونة الأسهم
|
||||||
|
map.loadImage('/icons/arrow.svg', (error, image) => {
|
||||||
|
if (error) console.error('Error loading arrow icon:', error);
|
||||||
|
else if (!map.hasImage('arrow')) map.addImage('arrow', image, { sdf: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function decodePoly(str) {
|
||||||
|
let index = 0, lat = 0, lng = 0, coordinates = [];
|
||||||
|
while (index < str.length) {
|
||||||
|
let byte, shift = 0, result = 0;
|
||||||
|
do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
|
||||||
|
lat += ((result & 1) ? ~(result >> 1) : (result >> 1));
|
||||||
|
shift = 0; result = 0;
|
||||||
|
do { byte = str.charCodeAt(index++) - 63; result |= (byte & 0x1f) << shift; shift += 5; } while (byte >= 0x20);
|
||||||
|
lng += ((result & 1) ? ~(result >> 1) : (result >> 1));
|
||||||
|
coordinates.push([lng / 1e5, lat / 1e5]);
|
||||||
|
}
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRoute() {
|
||||||
|
const l = document.getElementById('loading');
|
||||||
|
const s = document.getElementById('status');
|
||||||
|
l.style.display = 'flex';
|
||||||
|
try {
|
||||||
|
const url = `https://map-saas.intaleqapp.com/api/maps/route?fromLat=${AMMAN[1]}&fromLng=${AMMAN[0]}&toLat=${DAMASCUS[1]}&toLng=${DAMASCUS[0]}`;
|
||||||
|
const res = await fetch(url, { headers: { 'x-api-key': API_KEY } });
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
if (data.statusCode >= 400) throw new Error(data.message || 'Routing API Error');
|
||||||
|
|
||||||
|
let coords = typeof data.points === 'string' ? decodePoly(data.points) : data.points;
|
||||||
|
if (!coords || coords.length === 0) throw new Error('No valid route points found');
|
||||||
|
|
||||||
|
if (map.getSource('route')) {
|
||||||
|
map.getSource('route').setData({ type: 'Feature', geometry: { type: 'LineString', coordinates: coords } });
|
||||||
|
} else {
|
||||||
|
map.addSource('route', { type: 'geojson', data: { type: 'Feature', geometry: { type: 'LineString', coordinates: coords } } });
|
||||||
|
map.addLayer({ id: 'route-line', type: 'line', source: 'route', paint: { 'line-color': '#2563eb', 'line-width': 8, 'line-opacity': 0.85 } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const bounds = coords.reduce((b, c) => b.extend(c), new maplibregl.LngLatBounds(coords[0], coords[0]));
|
||||||
|
map.fitBounds(bounds, { padding: 80 });
|
||||||
|
|
||||||
|
s.style.display = 'block';
|
||||||
|
s.innerHTML = `🏁 المسافة: ${(data.distance/1000).toFixed(1)} كم | ⏱ الوقت: ${(data.duration/60).toFixed(0)} دقيقة`;
|
||||||
|
} catch (e) {
|
||||||
|
alert('خطأ في حساب المسارات: ' + e.message);
|
||||||
|
console.error(e);
|
||||||
|
} finally { l.style.display = 'none'; }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+9
-7
@@ -25,9 +25,9 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: map-redis
|
container_name: map-redis
|
||||||
platform: linux/arm64
|
platform: linux/amd64
|
||||||
ports:
|
ports:
|
||||||
- "6380:6379"
|
- "6381:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -46,7 +46,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./infrastructure/osm-data:/data
|
- ./infrastructure/osm-data:/data
|
||||||
- ./infrastructure/docker/graphhopper/config.yml:/graphhopper/config.yml
|
- ./infrastructure/docker/graphhopper/config.yml:/graphhopper/config.yml
|
||||||
command: ["-i", "/data/jordan-latest.osm.pbf", "-c", "config.yml"]
|
command: ["-i", "/data/region.osm.pbf", "-c", "config.yml"]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
|
test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -80,7 +80,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: ./infrastructure/docker/api/Dockerfile
|
dockerfile: ./infrastructure/docker/api/Dockerfile
|
||||||
container_name: map-api
|
container_name: map-api
|
||||||
platform: linux/arm64
|
platform: linux/amd64
|
||||||
ports:
|
ports:
|
||||||
- "3200:3200"
|
- "3200:3200"
|
||||||
environment:
|
environment:
|
||||||
@@ -102,7 +102,7 @@ services:
|
|||||||
container_name: map-martin
|
container_name: map-martin
|
||||||
platform: linux/amd64
|
platform: linux/amd64
|
||||||
ports:
|
ports:
|
||||||
- "3001:3000"
|
- "3202:3000"
|
||||||
environment:
|
environment:
|
||||||
- WATCH_DB=true
|
- WATCH_DB=true
|
||||||
command: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
command: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
@@ -121,7 +121,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
- DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||||
- PGPASSWORD=${POSTGRES_PASSWORD}
|
- PGPASSWORD=${POSTGRES_PASSWORD}
|
||||||
command: ["osm2pgsql", "--create", "--slim", "--cache", "1000", "--database", "${POSTGRES_DB}", "--host", "db", "--user", "${POSTGRES_USER}", "/data/jordan-latest.osm.pbf"]
|
command: ["osm2pgsql", "--create", "--slim", "--cache", "1000", "--database", "${POSTGRES_DB}", "--host", "db", "--user", "${POSTGRES_USER}", "/data/region.osm.pbf"]
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -134,9 +134,11 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: ./infrastructure/docker/web/Dockerfile
|
dockerfile: ./infrastructure/docker/web/Dockerfile
|
||||||
container_name: map-web
|
container_name: map-web
|
||||||
platform: linux/arm64
|
platform: linux/amd64
|
||||||
ports:
|
ports:
|
||||||
- "3201:5173"
|
- "3201:5173"
|
||||||
|
volumes:
|
||||||
|
- ./apps/web/public:/app/public
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
- martin
|
- martin
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Intaleq Map Platform - 10-Day Update Script
|
||||||
|
# سكربت تحديث خرائط انطلاقة - التحديث الدوري (كل 10 أيام)
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
echo "🚀 Starting 10-day map update..."
|
||||||
|
|
||||||
|
# 1. Configuration (From .env or defaults)
|
||||||
|
PBF_FILE="/data/jordan-latest.osm.pbf"
|
||||||
|
SOURCE_URL="https://download.geofabrik.de/asia/jordan-latest.osm.pbf"
|
||||||
|
APP_DIR="/home/hamzadoctor/app"
|
||||||
|
|
||||||
|
cd "$APP_DIR"
|
||||||
|
|
||||||
|
# 2. Download latest OSM data (Running on Host)
|
||||||
|
# تحميل أحدث البيانات للأردن وسوريا
|
||||||
|
echo "🌍 Downloading latest OpenStreetMap data for Jordan & Syria..."
|
||||||
|
wget -O "infrastructure/osm-data/jordan-latest.osm.pbf.new" "https://download.geofabrik.de/asia/jordan-latest.osm.pbf"
|
||||||
|
wget -O "infrastructure/osm-data/syria-latest.osm.pbf.new" "https://download.geofabrik.de/asia/syria-latest.osm.pbf"
|
||||||
|
|
||||||
|
mv "infrastructure/osm-data/jordan-latest.osm.pbf.new" "infrastructure/osm-data/jordan-latest.osm.pbf"
|
||||||
|
mv "infrastructure/osm-data/syria-latest.osm.pbf.new" "infrastructure/osm-data/syria-latest.osm.pbf"
|
||||||
|
|
||||||
|
# 3. Import new data to PostGIS
|
||||||
|
# استيراد البيانات إلى قاعدة البيانات
|
||||||
|
echo "💾 Importing Jordan (Create)..."
|
||||||
|
docker compose --profile import run --rm osm-import osm2pgsql --create --slim --cache 1000 --database mapdb --host db --user mapuser /data/jordan-latest.osm.pbf
|
||||||
|
|
||||||
|
echo "💾 Importing Syria (Append)..."
|
||||||
|
docker compose --profile import run --rm osm-import osm2pgsql --append --slim --cache 1000 --database mapdb --host db --user mapuser /data/syria-latest.osm.pbf
|
||||||
|
|
||||||
|
# 4. Merge Data (Jordan + Syria)
|
||||||
|
OSM_FILE="infrastructure/osm-data/region.osm.pbf"
|
||||||
|
echo "🗺️ Merging Jordan and Syria data into $OSM_FILE..."
|
||||||
|
osmium merge infrastructure/osm-data/jordan-latest.osm.pbf infrastructure/osm-data/syria-latest.osm.pbf -o $OSM_FILE --overwrite
|
||||||
|
|
||||||
|
# 5. Spatial Integrity check for Geocoding (Landmarks)
|
||||||
|
# Purge any legacy landmarks outside the expanded region
|
||||||
|
echo "📍 Syncing user-submitted landmarks and purging invalid coordinates..."
|
||||||
|
DELETED_COUNT=$(docker compose exec -T db psql -U mapuser -d mapdb -t -c "DELETE FROM places_syria WHERE longitude < 34 OR longitude > 43 OR latitude < 29 OR latitude > 38;")
|
||||||
|
echo " ✅ Purged ${DELETED_COUNT//[[:space:]]/} invalid landmarks outside the expanded region."
|
||||||
|
|
||||||
|
# Force Spatial Geometry Update
|
||||||
|
docker compose exec -T db psql -U mapuser -d mapdb -c "UPDATE places_syria SET location = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326) WHERE location IS NULL OR latitude IS NOT NULL;"
|
||||||
|
|
||||||
|
# 6. Rebuild Routing Index (GraphHopper)
|
||||||
|
echo "🚗 Rebuilding GraphHopper routing index (This may take ~5-8 minutes)..."
|
||||||
|
docker compose stop routing
|
||||||
|
# Delete old cache - VERY IMPORTANT to force full re-index
|
||||||
|
rm -rf infrastructure/osm-data/graph-cache infrastructure/osm-data/default-gh
|
||||||
|
docker compose up -d routing
|
||||||
|
|
||||||
|
# 7. Final Cleanup & Cache Flush
|
||||||
|
echo "🧹 Clearing Redis traffic cache..."
|
||||||
|
docker compose exec -T redis redis-cli flushall
|
||||||
|
|
||||||
|
echo "Done! Map platform is now fully synchronized with Jordan & Syria roads (including Damascus)."
|
||||||
Reference in New Issue
Block a user