From c97c961c96de01533ae004522741aea99d81b37c Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 15 Jul 2026 23:01:18 +0300 Subject: [PATCH] feat: implement manual routing sync system, update map tile URLs to HTTPS, and migrate admin session storage to localStorage --- ADDING_NEW_COUNTRY_GUIDE.md | 78 ++++++++++++ apps/api/src/geocoding/geocoding.service.ts | 121 ++++++++++++++++++- apps/api/src/maps/maps.controller.ts | 8 +- apps/api/src/maps/maps.service.ts | 7 ++ apps/web/public/style.json | 2 +- apps/web/src/pages/CompareView.tsx | 2 +- apps/web/src/pages/IntelligenceDashboard.tsx | 30 +++-- infrastructure/scripts/check_routing_sync.sh | 31 +++++ infrastructure/scripts/overture_ingest.sh | 6 + infrastructure/scripts/setup-crons.sh | 6 + packages/flutter-sdk/assets/style.json | 2 +- packages/flutter-sdk/assets/style_dark.json | 2 +- style-dark.json | 2 +- style.json | 2 +- 14 files changed, 281 insertions(+), 18 deletions(-) create mode 100644 ADDING_NEW_COUNTRY_GUIDE.md create mode 100755 infrastructure/scripts/check_routing_sync.sh diff --git a/ADDING_NEW_COUNTRY_GUIDE.md b/ADDING_NEW_COUNTRY_GUIDE.md new file mode 100644 index 0000000..5ae78cd --- /dev/null +++ b/ADDING_NEW_COUNTRY_GUIDE.md @@ -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` الشامل أصبح كافياً الآن). + +**تهانينا! 🎉 بهذا تكون الدولة الجديدة قد أُضيفت بشكل متكامل (شوارع، ملاحة، أماكن، ومناطق إدارية).** diff --git a/apps/api/src/geocoding/geocoding.service.ts b/apps/api/src/geocoding/geocoding.service.ts index 2e28b1c..6e2fcbc 100644 --- a/apps/api/src/geocoding/geocoding.service.ts +++ b/apps/api/src/geocoding/geocoding.service.ts @@ -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((_, 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[] = []; + + 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 => ({ diff --git a/apps/api/src/maps/maps.controller.ts b/apps/api/src/maps/maps.controller.ts index fc23eee..c80e37a 100644 --- a/apps/api/src/maps/maps.controller.ts +++ b/apps/api/src/maps/maps.controller.ts @@ -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) { diff --git a/apps/api/src/maps/maps.service.ts b/apps/api/src/maps/maps.service.ts index 44308f3..fe6efdd 100644 --- a/apps/api/src/maps/maps.service.ts +++ b/apps/api/src/maps/maps.service.ts @@ -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) { diff --git a/apps/web/public/style.json b/apps/web/public/style.json index 467de31..57f0c76 100644 --- a/apps/web/public/style.json +++ b/apps/web/public/style.json @@ -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 diff --git a/apps/web/src/pages/CompareView.tsx b/apps/web/src/pages/CompareView.tsx index 5537450..b95a40e 100644 --- a/apps/web/src/pages/CompareView.tsx +++ b/apps/web/src/pages/CompareView.tsx @@ -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 = { diff --git a/apps/web/src/pages/IntelligenceDashboard.tsx b/apps/web/src/pages/IntelligenceDashboard.tsx index 30d95a8..e83abf0 100644 --- a/apps/web/src/pages/IntelligenceDashboard.tsx +++ b/apps/web/src/pages/IntelligenceDashboard.tsx @@ -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(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 = () => {
Intaleq SaaS v2 · Split Compare
- +
+ + +
diff --git a/infrastructure/scripts/check_routing_sync.sh b/infrastructure/scripts/check_routing_sync.sh new file mode 100755 index 0000000..78daefa --- /dev/null +++ b/infrastructure/scripts/check_routing_sync.sh @@ -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 diff --git a/infrastructure/scripts/overture_ingest.sh b/infrastructure/scripts/overture_ingest.sh index f3cc6f4..9b65b7a 100644 --- a/infrastructure/scripts/overture_ingest.sh +++ b/infrastructure/scripts/overture_ingest.sh @@ -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!" diff --git a/infrastructure/scripts/setup-crons.sh b/infrastructure/scripts/setup-crons.sh index 34fd941..4b6c821 100755 --- a/infrastructure/scripts/setup-crons.sh +++ b/infrastructure/scripts/setup-crons.sh @@ -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 diff --git a/packages/flutter-sdk/assets/style.json b/packages/flutter-sdk/assets/style.json index b4c0ade..7aea78e 100644 --- a/packages/flutter-sdk/assets/style.json +++ b/packages/flutter-sdk/assets/style.json @@ -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 diff --git a/packages/flutter-sdk/assets/style_dark.json b/packages/flutter-sdk/assets/style_dark.json index 6799e83..3863b47 100644 --- a/packages/flutter-sdk/assets/style_dark.json +++ b/packages/flutter-sdk/assets/style_dark.json @@ -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 diff --git a/style-dark.json b/style-dark.json index 0a7490d..465dd15 100644 --- a/style-dark.json +++ b/style-dark.json @@ -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 diff --git a/style.json b/style.json index 2f23ec1..185b76d 100644 --- a/style.json +++ b/style.json @@ -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