feat: implement manual routing sync system, update map tile URLs to HTTPS, and migrate admin session storage to localStorage

This commit is contained in:
Hamza-Ayed
2026-07-15 23:01:18 +03:00
parent 78fe6c48cc
commit c97c961c96
14 changed files with 281 additions and 18 deletions
+78
View File
@@ -0,0 +1,78 @@
# دليل المستتخدم: إضافة دولة جديدة على نظام خرائط انطلاقة
هذا الدليل يوضح لك الخطوات البرمجية والإدارية اللازمة بالترتيب لإضافة دولة جديدة (مثل مصر أو السعودية) إلى نظام الخرائط الخاص بك، وتهيئتها بالكامل (ملاحة، أماكن، وحدود إدارية).
---
## المرحلة الأولى: إعداد بيانات الملاحة (OSM & Routing)
لإضافة شوارع الدولة ومبانيها لتعمل عليها الملاحة (GraphHopper):
1. **تعديل سكربت التحديث `update-data.sh`:**
- قم بالدخول إلى الملف `infrastructure/scripts/update-data.sh`.
- أضف رابط تحميل الدولة من موقع [Geofabrik](https://download.geofabrik.de/).
- أضف أمر حقن الدولة الجديدة في قاعدة البيانات `PostGIS` باستخدام أمر `osm2pgsql --append`.
- أضف مسار الملف الجديد إلى أمر الدمج `osmium merge` لكي يُدمج مع الخريطة الأساسية `master_map.osm.pbf`.
2. **تشغيل السكربت:**
بمجرد تشغيل السكربت، سيتم تحميل الشوارع والمباني، وسيُعاد بناء محرك التوجيه (GraphHopper) ليتعرف على مسارات الدولة الجديدة تلقائياً.
---
## المرحلة الثانية: جلب الأماكن والحدود الإدارية (Overture Maps)
لجلب المحلات التجارية، المطاعم، والحدود الجغرافية للدولة الجديدة:
1. **تعديل سكربت الأماكن `overture_ingest.sh`:**
- قم بالدخول إلى الملف `infrastructure/scripts/overture_ingest.sh`.
- حدد الإحداثيات المربعة (BBOX) للدولة الجديدة (مثال: `BBOX_EGYPT="22.0,24.0,31.5,37.0"`).
- أضف أمر استدعاء الدالة أسفل الملف:
```bash
process_city "egypt" "$BBOX_EGYPT" "division_area"
process_city "egypt" "$BBOX_EGYPT" "place"
```
2. **تشغيل السكربت:**
- سيقوم السكربت بحقن كل المحلات التجارية في جدول `overture_place` لتصبح قابلة للبحث مباشرة.
- **ملاحظة:** السكربت سيقوم بتحميل ملف الحدود الإدارية ويتركه بصيغة GeoJSON (مثال: `overture_egypt_division_area.geojson`) لكي نستخدمه في المرحلة الثالثة.
---
## المرحلة الثالثة: التأسيس الإداري (عبر الـ Postman)
> [!IMPORTANT]
> هذه الروابط هي أدوات لمرة واحدة فقط (Setup Tools). لا تستخدمها بشكل دوري لكي لا تفقد أي تعديلات يدوية قمت بها على مسميات الأحياء لاحقاً.
هنا نستخدم الروابط التي سألت عنها لتأسيس (المحافظات، الألوية، والأحياء) للدولة الجديدة بالترتيب:
### 1. استيراد الحدود الإدارية الكبرى
هذا الرابط يقرأ ملف الـ GeoJSON الذي تم تحميله في المرحلة السابقة، ويقوم بإنشاء المحافظات والألوية/المقاطعات في جدول `admin_boundaries`.
- **Method:** `POST`
- **URL:** `https://map-saas.intaleqapp.com/api/geocoding/import-boundaries`
- **Query Params:**
- `country` = `EG` (رمز الدولة)
- `filePath` = `/data/overture_egypt_division_area.geojson`
### 2. مزامنة نقاط الأحياء من OSM
هذا الرابط يتصل بسيرفرات الخرائط العالمية لجلب مراكز الأحياء ونقاطها (Neighborhoods) ووضعها في جدول `neighborhood_polygons`.
- **Method:** `POST`
- **URL:** `https://map-saas.intaleqapp.com/api/geocoding/admin/sync-neighborhoods`
- **Query Params:**
- `bbox` = `22.0,24.0,31.5,37.0` (إحداثيات الدولة الجديدة)
- `country` = `egypt`
### 3. توليد مضلعات الأحياء هندسياً (Voronoi Polygons)
نقاط الأحياء لوحدها غير كافية لمعرفة أين يبدأ وأين ينتهي الحي. هذا الرابط الأخير يقوم بعملية حسابية هندسية لتقسيم المساحات بين نقاط الأحياء (بناءً على حدود المحافظات التي جلبناها في الخطوة 1) لتوليد مضلعات حقيقية.
- **Method:** `POST`
- **URL:** `https://map-saas.intaleqapp.com/api/geocoding/admin/generate-voronoi`
- **Query Params:** (لا يوجد، ستقوم الوظيفة بحساب المضلعات لكل النقاط الناقصة).
---
## المرحلة الرابعة: ربط محرك البحث (الجي كودينج)
إذا كنت لا تزال تستخدم جداول قديمة مخصصة لكل دولة مثل (`places_syria` و `places_jordan`) في الـ API الخاص بك:
1. اذهب إلى ملف `apps/api/src/geocoding/geocoding.service.ts`.
2. تأكد من إضافة دالة أو `UNION ALL` لجلب البيانات من `places_egypt` أو الجدول المخصص للدولة الجديدة (رغم أن الاعتماد على `overture_place` الشامل أصبح كافياً الآن).
**تهانينا! 🎉 بهذا تكون الدولة الجديدة قد أُضيفت بشكل متكامل (شوارع، ملاحة، أماكن، ومناطق إدارية).**
+117 -4
View File
@@ -112,6 +112,58 @@ export class GeocodingService {
ORDER BY (o.name <-> $1) ASC LIMIT 10
`, [cleanQuery, lat || null, lon || null, radius]));
queryPromises.push(this.osmPointsRepository.query(`
SELECT
id, name_ar as name, name_ar, name_en, 'admin' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(geom))::text as latitude, ST_X(ST_Centroid(geom))::text as longitude, '' as address, 'admin_boundary' as region, 'admin' as source,
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(geom, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
similarity(COALESCE(name_ar, ''), $1) as relevance
FROM admin_boundaries
WHERE name_ar % $1
AND ($2::float IS NULL OR ST_DistanceSphere(geom, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4)
ORDER BY (name_ar <-> $1) ASC LIMIT 5
`, [cleanQuery, lat || null, lon || null, radius]));
queryPromises.push(this.osmPointsRepository.query(`
SELECT
id::text, COALESCE(names->>'primary', names->>'common', 'Street') as name, COALESCE(names->>'primary', names->>'common', 'Street') as name_ar, '' as name_en, 'street' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(location))::text as latitude, ST_X(ST_Centroid(location))::text as longitude, '' as address, 'overture' as region, 'overture_global' as source,
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
GREATEST(similarity(COALESCE(names->>'primary', ''), $1), similarity(COALESCE(names->>'common', ''), $1)) as relevance
FROM overture_segment
WHERE (names->>'primary' % $1 OR names->>'common' % $1)
AND ($2::float IS NULL OR ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4)
ORDER BY (COALESCE(names->>'primary', '') <-> $1) ASC LIMIT 10
`, [cleanQuery, lat || null, lon || null, radius]));
queryPromises.push(this.osmPointsRepository.query(`
SELECT
id::text, COALESCE(names->>'primary', names->>'common', 'Building') as name, COALESCE(names->>'primary', names->>'common', 'Building') as name_ar, '' as name_en, 'building' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(location))::text as latitude, ST_X(ST_Centroid(location))::text as longitude, '' as address, 'overture' as region, 'overture_global' as source,
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
GREATEST(similarity(COALESCE(names->>'primary', ''), $1), similarity(COALESCE(names->>'common', ''), $1)) as relevance
FROM overture_building
WHERE (names->>'primary' % $1 OR names->>'common' % $1)
AND ($2::float IS NULL OR ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4)
ORDER BY (COALESCE(names->>'primary', '') <-> $1) ASC LIMIT 10
`, [cleanQuery, lat || null, lon || null, radius]));
queryPromises.push(this.osmPointsRepository.query(`
SELECT
id::text, COALESCE(names->>'primary', names->>'common', 'Place') as name, COALESCE(names->>'primary', names->>'common', 'Place') as name_ar, '' as name_en, 'place' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(location))::text as latitude, ST_X(ST_Centroid(location))::text as longitude, '' as address, 'overture' as region, 'overture_global' as source,
CASE WHEN $2::float IS NOT NULL THEN ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) ELSE 0 END as distance,
GREATEST(similarity(COALESCE(names->>'primary', ''), $1), similarity(COALESCE(names->>'common', ''), $1)) as relevance
FROM overture_place
WHERE (names->>'primary' % $1 OR names->>'common' % $1)
AND ($2::float IS NULL OR ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($3::float, $2::float), 4326)) <= $4)
ORDER BY (COALESCE(names->>'primary', '') <-> $1) ASC LIMIT 10
`, [cleanQuery, lat || null, lon || null, radius]));
const executionResults = await Promise.race([
Promise.allSettled(queryPromises),
new Promise<any>((_, reject) => setTimeout(() => reject(new Error('QUERY_TIMEOUT')), this.DB_TIMEOUT_MS))
@@ -184,7 +236,9 @@ export class GeocodingService {
try {
const repo = this.getRepositoryForCoords(lat, lng);
const tableName = this.getTableNameForRepo(repo);
const query = `
const queryPromises: Promise<any[]>[] = [];
queryPromises.push(repo.query(`
SELECT
p.id, p.name, p.name_ar, p.category,
n.name_ar as neighbourhood,
@@ -197,9 +251,67 @@ export class GeocodingService {
LEFT JOIN admin_boundaries d ON p.sub_district_id = d.id
LEFT JOIN admin_boundaries g ON p.governorate_id = g.id
WHERE p.location IS NOT NULL
ORDER BY p.location::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 3
`;
return await repo.query(query, [lng, lat]);
ORDER BY p.location::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]));
queryPromises.push(repo.query(`
SELECT
o.osm_id::text as id, o.name, o.name_ar, COALESCE(o.amenity, o.shop, 'place') as category,
'' as neighbourhood, '' as district, '' as governorate,
o.latitude, o.longitude, o.addr_street as address, 'osm_global' as source,
ST_DistanceSphere(o.geom, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
FROM osm_points_with_area o
WHERE o.geom IS NOT NULL AND (o.name IS NOT NULL OR o.name_ar IS NOT NULL)
ORDER BY o.geom <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]));
queryPromises.push(repo.query(`
SELECT
id::text, COALESCE(names->>'primary', names->>'common', 'Street') as name, COALESCE(names->>'primary', names->>'common', 'Street') as name_ar, 'street' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(location))::text as latitude, ST_X(ST_Centroid(location))::text as longitude, '' as address, 'overture_global' as source,
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
FROM overture_segment
WHERE location IS NOT NULL AND (names->>'primary' IS NOT NULL OR names->>'common' IS NOT NULL)
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]));
queryPromises.push(repo.query(`
SELECT
id::text, COALESCE(names->>'primary', names->>'common', 'Building') as name, COALESCE(names->>'primary', names->>'common', 'Building') as name_ar, 'building' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(location))::text as latitude, ST_X(ST_Centroid(location))::text as longitude, '' as address, 'overture_global' as source,
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
FROM overture_building
WHERE location IS NOT NULL AND (names->>'primary' IS NOT NULL OR names->>'common' IS NOT NULL)
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]));
queryPromises.push(repo.query(`
SELECT
id::text, COALESCE(names->>'primary', names->>'common', 'Place') as name, COALESCE(names->>'primary', names->>'common', 'Place') as name_ar, 'place' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(location))::text as latitude, ST_X(ST_Centroid(location))::text as longitude, '' as address, 'overture_global' as source,
ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
FROM overture_place
WHERE location IS NOT NULL AND (names->>'primary' IS NOT NULL OR names->>'common' IS NOT NULL)
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]));
const results = await Promise.allSettled(queryPromises);
let allResults: any[] = [];
results.forEach(res => {
if (res.status === 'fulfilled' && res.value) allResults.push(...res.value);
});
return allResults
.sort((a, b) => Number(a.distance) - Number(b.distance))
.slice(0, 5)
.map(r => ({
...r,
latitude: parseFloat(r.latitude),
longitude: parseFloat(r.longitude)
}));
} catch (error) {
this.logger.error('Reverse geocoding error:', error);
return [];
@@ -252,6 +364,7 @@ export class GeocodingService {
UNION ALL SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_egypt
UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Building') as name, 'building' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_building WHERE names->>'primary' IS NOT NULL LIMIT 500
UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Street') as name, 'street' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_segment WHERE names->>'primary' IS NOT NULL LIMIT 500
UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Place') as name, 'place' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_place WHERE names->>'primary' IS NOT NULL LIMIT 500
`;
const res = await this.placesSyriaRepository.query(q);
const features = res.map(p => ({
+7 -1
View File
@@ -1,4 +1,4 @@
import { Controller, Get, Query, UseGuards, Res } from '@nestjs/common';
import { Controller, Get, Post, Query, UseGuards, Res } from '@nestjs/common';
import { ApiTags, ApiOperation } from '@nestjs/swagger';
import type { Response } from 'express';
import * as fs from 'fs';
@@ -12,6 +12,12 @@ import { ApiKeyGuard } from '../common/guards/api-key.guard';
export class MapsController {
constructor(private readonly mapsService: MapsService) { }
@Post('sync-routes')
@ApiOperation({ summary: 'Request GraphHopper routing sync 🔄' })
async syncRoutes() {
return this.mapsService.requestRoutingSync();
}
@Get('style.json')
@ApiOperation({ summary: 'Get MapLibre style JSON 🎨' })
async getStyleJson(@Query('theme') theme: string, @Res() res: Response) {
+7
View File
@@ -7,6 +7,7 @@ import axios from 'axios';
import { RoadSegmentStat } from './road-stat.entity';
import { TrafficGridService } from './traffic-grid.service';
import { GeocodingService } from '../geocoding/geocoding.service';
import { RedisService } from '../common/redis.service';
@Injectable()
export class MapsService {
@@ -19,10 +20,16 @@ export class MapsService {
private trafficGrid: TrafficGridService,
private geocodingService: GeocodingService,
private dataSource: DataSource,
private redisService: RedisService,
) {
this.graphHopperUrl = this.configService.get('GRAPH_HOPPER_URL', 'http://routing:8080');
}
async requestRoutingSync() {
await this.redisService.set('routing_sync_requested', '1');
return { success: true, message: 'Routing sync requested and scheduled for the next minute.' };
}
async getRoute(waypoints: [number, number][], profile: string = 'car', steps: boolean = false, locale: string = 'en', alternatives: boolean = false) {
if (waypoints.length < 2) {
+1 -1
View File
@@ -74,7 +74,7 @@
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
+1 -1
View File
@@ -18,7 +18,7 @@ import 'maplibre-gl/dist/maplibre-gl.css';
// Martin vector-tile host (serves planet_osm_*, approved_roads, …). This is a
// DIFFERENT host from where the app + its style.json are served, so it must be
// absolute — an empty default resolved to the app host, which serves no tiles.
const TILES = (import.meta as any).env.VITE_TILES_URL || 'http://188.68.36.205:3202';
const TILES = (import.meta as any).env.VITE_TILES_URL || 'https://tiles.intaleqapp.com';
type RefKey = 'google-sat' | 'esri-sat' | 'esri-streets' | 'osm';
const REFS: Record<RefKey, { label: string; tiles: string; attribution: string; maxzoom: number }> = {
+23 -7
View File
@@ -30,8 +30,8 @@ const IntelligenceDashboard: React.FC = () => {
const [msg, setMsg] = useState('');
const [tab, setTab] = useState<'summary' | 'candidates' | 'closures'>('candidates');
const [loading, setLoading] = useState(true);
const [apiKey, setApiKey] = useState(sessionStorage.getItem('map_admin_key') || '');
const [needsAuth, setNeedsAuth] = useState(!sessionStorage.getItem('map_admin_key'));
const [apiKey, setApiKey] = 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
@@ -111,8 +111,8 @@ const IntelligenceDashboard: React.FC = () => {
if (!sr.ok || !cr.ok || !clr.ok) {
if (sr.status === 401 || sr.status === 403) {
localStorage.removeItem('map_admin_key');
setNeedsAuth(true);
sessionStorage.removeItem('map_admin_key');
setApiKey('');
throw new Error('Invalid or unauthorized API Key');
}
@@ -371,6 +371,17 @@ const IntelligenceDashboard: React.FC = () => {
setTimeout(() => setMsg(''), 5000);
};
const syncRoutes = async () => {
if (!confirm('This will request a full routing rebuild on the server which takes 5-10 minutes. Continue?')) return;
try {
setMsg('Requesting route sync...');
const r = await fetch(`${API}/maps/sync-routes`, { method: 'POST', headers: { 'x-api-key': apiKey } });
if (!r.ok) throw new Error('Failed');
setMsg('✅ Routing sync requested! Check back in 5-10 min.');
} catch (e: any) { setMsg(e.message || 'Error'); }
setTimeout(() => setMsg(''), 5000);
};
const cancelDrawing = () => { pointsRef.current = []; redrawDrawing(); setDrawing(false); };
if (needsAuth) {
@@ -389,7 +400,7 @@ const IntelligenceDashboard: React.FC = () => {
if (e.key === 'Enter') {
const val = e.currentTarget.value;
if (val) {
sessionStorage.setItem('map_admin_key', val);
localStorage.setItem('map_admin_key', val);
setApiKey(val);
}
}
@@ -416,9 +427,14 @@ const IntelligenceDashboard: React.FC = () => {
<div style={{ fontSize: '0.7rem', color: '#64748b' }}>Intaleq SaaS v2 · Split Compare</div>
</div>
</div>
<button onClick={runAI} style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid #6366f1', color: '#818cf8', padding: '6px 10px', borderRadius: '8px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.78rem' }}>
<RefreshCw size={13} /> Run
</button>
<div style={{ display: 'flex', gap: '6px' }}>
<button onClick={syncRoutes} style={{ background: 'rgba(16,185,129,0.15)', border: '1px solid #10b981', color: '#34d399', padding: '6px 10px', borderRadius: '8px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.78rem' }} title="Sync Approved Roads to Router">
<RefreshCw size={13} /> Sync Routes
</button>
<button onClick={runAI} style={{ background: 'rgba(99,102,241,0.15)', border: '1px solid #6366f1', color: '#818cf8', padding: '6px 10px', borderRadius: '8px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.78rem' }}>
<RefreshCw size={13} /> Run
</button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px', padding: '1rem' }}>
+31
View File
@@ -0,0 +1,31 @@
#!/bin/bash
# --------------------------------------------------------------------------
# check_routing_sync.sh
# Runs via cron every minute to check if the admin dashboard requested
# a routing graph rebuild (due to a newly approved road).
# --------------------------------------------------------------------------
APP_DIR="/home/hamzadoctor/app"
cd "${APP_DIR}"
# Read flag from Redis using the redis container
SYNC_REQ=$(docker compose exec -T redis redis-cli get routing_sync_requested | tr -d '\r')
if [ "$SYNC_REQ" = "1" ] || [ "$SYNC_REQ" = "\"1\"" ]; then
echo "$(date): Sync requested! Triggering delta apply and GH rebuild..."
# 1. Delete the flag immediately so we don't trigger it again
docker compose exec -T redis redis-cli del routing_sync_requested
# 2. Apply delta
if [ -f "${APP_DIR}/infrastructure/scripts/apply-delta.sh" ]; then
bash "${APP_DIR}/infrastructure/scripts/apply-delta.sh"
fi
# 3. Restart GraphHopper to rebuild index
docker compose stop routing
rm -rf "${APP_DIR}/infrastructure/osm-data/graph-cache" "${APP_DIR}/infrastructure/osm-data/default-gh"
docker compose up -d routing
echo "$(date): Routing rebuild triggered successfully."
fi
@@ -21,6 +21,9 @@ BBOX_SYRIA="35.7,32.3,42.4,37.3"
echo "🚀 Starting Overture Data Pipeline (Jordan & Syria)..."
echo "🧹 Clearing old Overture tables to prevent duplication..."
docker compose -f /home/hamzadoctor/app/docker-compose.yml exec -T db psql -U $DB_USER -d $DB_NAME -c "DROP TABLE IF EXISTS overture_place, overture_building, overture_segment;"
# 1. Setup Python Environment
# ... (rest of setup)
if [ ! -d "venv_overture" ]; then
@@ -68,9 +71,12 @@ process_city() {
# 3. Execution (Jordan & Syria)
# Jordan
process_city "jordan_north" "$BBOX_JORDAN_NORTH" "division_area"
process_city "jordan_north" "$BBOX_JORDAN_NORTH" "place"
process_city "jordan_south" "$BBOX_JORDAN_SOUTH" "division_area"
process_city "jordan_south" "$BBOX_JORDAN_SOUTH" "place"
# Syria
process_city "syria" "$BBOX_SYRIA" "division_area"
process_city "syria" "$BBOX_SYRIA" "place"
echo "🎉 Administrative boundary GeoJSONs ready for import!"
+6
View File
@@ -15,6 +15,12 @@ echo "0 3 */10 * * /home/hamzadoctor/app/infrastructure/scripts/update-data.sh >
# 2. Discover road closures daily at 4:00 AM
echo "0 4 * * * curl -X POST -H \"x-api-key: intaleq_secret_2026\" http://localhost:3200/api/map-refinement/roads/discover-closures >> /home/hamzadoctor/app/infrastructure/logs/closures.log 2>&1" >> $CRON_FILE
# 3. Check for manual routing sync requests every minute
echo "* * * * * /home/hamzadoctor/app/infrastructure/scripts/check_routing_sync.sh >> /home/hamzadoctor/app/infrastructure/logs/routing_sync.log 2>&1" >> $CRON_FILE
# 4. Update Overture Maps Data on the 1st of every month at 2:00 AM
echo "0 2 1 * * /home/hamzadoctor/app/infrastructure/scripts/overture_ingest.sh >> /home/hamzadoctor/app/infrastructure/logs/overture_ingest.log 2>&1" >> $CRON_FILE
# Install crontab
crontab $CRON_FILE
rm $CRON_FILE
+1 -1
View File
@@ -74,7 +74,7 @@
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
+1 -1
View File
@@ -74,7 +74,7 @@
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
+1 -1
View File
@@ -74,7 +74,7 @@
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18
+1 -1
View File
@@ -74,7 +74,7 @@
"approved_roads": {
"type": "vector",
"tiles": [
"http://188.68.36.205:3202/approved_roads/{z}/{x}/{y}"
"https://tiles.intaleqapp.com/approved_roads/{z}/{x}/{y}"
],
"minzoom": 8,
"maxzoom": 18