chore: remove obsolete python patch scripts and update API key fallback handling in frontend views
This commit is contained in:
+5
-2
@@ -17,8 +17,11 @@
|
||||
|
||||
# Environment Variables & Secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.*
|
||||
*.env
|
||||
*.env.*
|
||||
**/.env
|
||||
**/.env.*
|
||||
*.key
|
||||
*.pem
|
||||
|
||||
|
||||
@@ -208,14 +208,16 @@ export class AuthService {
|
||||
name,
|
||||
photoUrl,
|
||||
plan: isAdmin ? TenantPlan.ENTERPRISE : TenantPlan.FREE,
|
||||
role: isAdmin ? TenantRole.ADMIN : TenantRole.USER,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Auto-upgrade admins if they exist but are on lower plan
|
||||
const isAdmin = email === 'hamzaaleghwairyeen@gmail.com' || email === 'hamzadoctor@gmail.com' || email === 'hamzaayedpython@gmail.com';
|
||||
if (isAdmin && tenant.plan !== TenantPlan.ENTERPRISE) {
|
||||
// Auto-upgrade admins if they exist but are on lower plan or role
|
||||
const isAdmin = email === 'hamzaayedpython@gmail.com';
|
||||
if (isAdmin && (tenant.plan !== TenantPlan.ENTERPRISE || tenant.role !== TenantRole.ADMIN)) {
|
||||
tenant.plan = TenantPlan.ENTERPRISE;
|
||||
tenant.role = TenantRole.ADMIN;
|
||||
await this.tenantRepository.save(tenant);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ export class AdminGuard implements CanActivate {
|
||||
throw new UnauthorizedException('Tenant not found in request');
|
||||
}
|
||||
|
||||
if (tenant.plan !== TenantPlan.ENTERPRISE) {
|
||||
throw new ForbiddenException('Admin access required. Your tenant plan must be ENTERPRISE.');
|
||||
if (tenant.plan !== TenantPlan.ENTERPRISE && tenant.role !== TenantRole.ADMIN) {
|
||||
throw new ForbiddenException('Admin access required. Your tenant must have an ENTERPRISE plan or ADMIN clearance.');
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -3,10 +3,11 @@ import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import { CommunityService } from './community.service';
|
||||
import { ReviewContributionDto } from './dto/review-contribution.dto';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
import { AdminGuard } from '../common/guards/admin.guard';
|
||||
|
||||
@ApiExcludeController()
|
||||
@Controller('v1/admin/moderation')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@UseGuards(ApiKeyGuard, AdminGuard)
|
||||
export class ModerationController {
|
||||
constructor(private readonly communityService: CommunityService) {}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export class HeritageController {
|
||||
},
|
||||
variable: [
|
||||
{ key: "baseUrl", value: "https://map-saas.intaleqapp.com/api", type: "string" },
|
||||
{ key: "apiKey", value: "zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX", type: "string" }
|
||||
{ key: "apiKey", value: process.env.MAP_API_KEY || "", type: "string" }
|
||||
],
|
||||
item: [
|
||||
{
|
||||
|
||||
@@ -5,12 +5,13 @@ import { Repository, DataSource } from 'typeorm';
|
||||
import { CandidateRoad } from './candidate-road.entity';
|
||||
import { RoadSegmentStat } from './road-stat.entity';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
import { AdminGuard } from '../common/guards/admin.guard';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
|
||||
@ApiExcludeController()
|
||||
@ApiTags('map-refinement-roads')
|
||||
@Controller('map-refinement/roads')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
@UseGuards(ApiKeyGuard, AdminGuard)
|
||||
export class RoadRefinementController {
|
||||
private readonly logger = new Logger(RoadRefinementController.name);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiExcludeController, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
import { AdminGuard } from '../common/guards/admin.guard';
|
||||
import { TenantThrottlerGuard } from '../common/guards/rate-limiter.guard';
|
||||
import { LineOfSightBodyDto, LineOfSightQueryDto } from './dto/line-of-sight.dto';
|
||||
import { ArtilleryMissionRequestDto, SaveScenarioDto } from './dto/tactical.dto';
|
||||
@@ -22,7 +23,7 @@ import { RoutingPackageService } from './routing-package.service';
|
||||
|
||||
@ApiExcludeController()
|
||||
@Controller('tactical')
|
||||
@UseGuards(ApiKeyGuard, TenantThrottlerGuard)
|
||||
@UseGuards(ApiKeyGuard, AdminGuard, TenantThrottlerGuard)
|
||||
export class TacticalController {
|
||||
constructor(
|
||||
private readonly tacticalService: TacticalService,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useLanguage } from '../i18n';
|
||||
type Key = { id: string; key: string; name: string };
|
||||
type PlaygroundMode = 'search' | 'routing' | 'layers' | 'spatial';
|
||||
|
||||
const DEFAULT_SOVEREIGN_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const DEFAULT_SOVEREIGN_KEY = (import.meta as any).env.VITE_API_KEY || (import.meta as any).env.VITE_ADMIN_API_KEY || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || '';
|
||||
|
||||
interface RoutingResult {
|
||||
distanceKm?: number;
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SOVEREIGN_MAP_API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const SOVEREIGN_MAP_API_KEY = new URLSearchParams(window.location.search).get('key') || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || '';
|
||||
// 100% Verified Premier Landmarks Dataset (Iraq, Jordan, Syria, Egypt)
|
||||
let LANDMARKS = [
|
||||
{
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SOVEREIGN_MAP_API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const SOVEREIGN_MAP_API_KEY = new URLSearchParams(window.location.search).get('key') || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || '';
|
||||
// 100% Verified Premier Landmarks Dataset (Iraq, Jordan, Syria, Egypt)
|
||||
let LANDMARKS = [
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
{
|
||||
"key": "apiKey",
|
||||
"value": "zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SOVEREIGN_MAP_API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const SOVEREIGN_MAP_API_KEY = new URLSearchParams(window.location.search).get('key') || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || '';
|
||||
// 100% Verified Premier Landmarks Dataset (Iraq, Jordan, Syria, Egypt)
|
||||
let LANDMARKS = [
|
||||
{
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const SOVEREIGN_MAP_API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const SOVEREIGN_MAP_API_KEY = new URLSearchParams(window.location.search).get('key') || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || '';
|
||||
// 100% Verified Premier Landmarks Dataset (Iraq, Jordan, Syria, Egypt)
|
||||
let LANDMARKS = [
|
||||
{
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
},
|
||||
{
|
||||
"key": "apiKey",
|
||||
"value": "zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX",
|
||||
"value": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -186,6 +186,7 @@ services:
|
||||
- "3201:5173"
|
||||
environment:
|
||||
- VITE_API_KEY=${MAP_API_KEY}
|
||||
- VITE_ADMIN_API_KEY=${MAP_API_KEY}
|
||||
- VITE_API_URL=/api
|
||||
volumes:
|
||||
- ./apps/web/public:/app/public
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import os
|
||||
|
||||
def export_minimalist_project():
|
||||
output_filename = "project_source_code_light.txt"
|
||||
|
||||
# 1. الملفات المسموح بها بالاسم الكامل (حساسة لحالة الأحرف)
|
||||
strictly_allowed_names = {'style.json', 'style_mobile.json', '.env', 'env'}
|
||||
|
||||
# 2. الامتدادات البرمجية المسموح بها (الكود المصدري فقط)
|
||||
allowed_logic_extensions = {'.ts', '.sh'}
|
||||
|
||||
# 3. مجلدات يتم تجاوزها فوراً لتسريع العملية
|
||||
ignore_dirs = {'node_modules', 'dist', '.git', 'coverage', 'build', '.idea', '.vscode'}
|
||||
|
||||
file_counter = 0
|
||||
|
||||
print("جاري استخراج الملفات المطلوبة فقط (Light Mode)...")
|
||||
|
||||
with open(output_filename, 'w', encoding='utf-8') as outfile:
|
||||
outfile.write("=" * 60 + "\n")
|
||||
outfile.write("ملفات المشروع المختارة (Style JSON + Source Code)\n")
|
||||
outfile.write("=" * 60 + "\n\n")
|
||||
|
||||
for root, dirs, files in os.walk('.'):
|
||||
# استثناء المجلدات الثقيلة
|
||||
dirs[:] = [d for d in dirs if d not in ignore_dirs]
|
||||
|
||||
for file in files:
|
||||
name_lower = file.lower()
|
||||
ext = os.path.splitext(name_lower)[1]
|
||||
|
||||
# شرط القبول المنطقي:
|
||||
# - إما أن يكون الاسم مطابقاً تماماً لملفات الستايل أو env
|
||||
# - أو أن يكون امتداد ملف كود (ts, sh)
|
||||
if file in strictly_allowed_names or ext in allowed_logic_extensions:
|
||||
|
||||
# استثناء إضافي لملفات الاختبار أو التكوين الفرعية لتقليل الحجم
|
||||
if '.spec.ts' in name_lower or 'test.ts' in name_lower:
|
||||
continue
|
||||
|
||||
filepath = os.path.join(root, file)
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
file_counter += 1
|
||||
outfile.write("-" * 60 + "\n")
|
||||
outfile.write(f"[{file_counter}] اسم الملف: {file}\n")
|
||||
outfile.write(f"المسار: {filepath}\n")
|
||||
outfile.write("-" * 60 + "\n")
|
||||
|
||||
for line_number, line in enumerate(lines, 1):
|
||||
# كتابة رقم السطر مع الكود
|
||||
outfile.write(f"{line_number:04d} | {line.rstrip()}\n")
|
||||
|
||||
outfile.write("\n\n")
|
||||
print(f"تمت الإضافة: {filepath}")
|
||||
|
||||
except Exception:
|
||||
# تجاوز أي ملف يسبب خطأ في القراءة
|
||||
continue
|
||||
|
||||
print(f"\n✅ اكتملت العملية بنجاح.")
|
||||
print(f"إجمالي الملفات المضافة: {file_counter}")
|
||||
print(f"الملف الناتج: {output_filename}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
export_minimalist_project()
|
||||
@@ -1,26 +0,0 @@
|
||||
import re
|
||||
|
||||
path = "packages/tactical_app/lib/controllers/tactical_map_controller.dart"
|
||||
with open(path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Remove the incorrectly inserted lines
|
||||
content = content.replace(" // Expand sheet back after picking\n if (activeSheet.value != null) {\n isSheetMinimized.value = false;\n }", "")
|
||||
|
||||
# Correctly insert at the end of confirmPicker
|
||||
insert_target = """ case MapPickerTarget.routeDestination:
|
||||
pickedRouteDestination.value = pos;
|
||||
switchMode('routing');
|
||||
break;
|
||||
}
|
||||
"""
|
||||
|
||||
replacement = insert_target + """
|
||||
if (activeSheet.value != null) {
|
||||
isSheetMinimized.value = false;
|
||||
}
|
||||
"""
|
||||
content = content.replace(insert_target, replacement)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,14 +0,0 @@
|
||||
import re
|
||||
|
||||
path = "packages/tactical_app/lib/bindings/tactical_binding.dart"
|
||||
with open(path, "r") as f: content = f.read()
|
||||
|
||||
import_statement = "import '../services/local_network_tracker.dart';\n"
|
||||
if "local_network_tracker" not in content:
|
||||
content = content.replace("import '../controllers/tactical_map_controller.dart';", import_statement + "import '../controllers/tactical_map_controller.dart';")
|
||||
|
||||
init_statement = " Get.putAsync(() => LocalNetworkTrackerService().init());\n"
|
||||
if "LocalNetworkTrackerService" not in content:
|
||||
content = content.replace(" Get.lazyPut(() => TacticalMapController());", init_statement + " Get.lazyPut(() => TacticalMapController());")
|
||||
|
||||
with open(path, "w") as f: f.write(content)
|
||||
@@ -1,51 +0,0 @@
|
||||
import re
|
||||
|
||||
path = "packages/tactical_app/lib/controllers/tactical_map_controller.dart"
|
||||
with open(path, "r") as f: content = f.read()
|
||||
|
||||
imports = """
|
||||
import '../models/friendly_unit.dart';
|
||||
import '../services/local_network_tracker.dart';
|
||||
"""
|
||||
if "LocalNetworkTrackerService" not in content:
|
||||
content = content.replace("import '../services/offline_los_engine.dart';", "import '../services/offline_los_engine.dart';\n" + imports)
|
||||
|
||||
# Find the line: final NavigationController navigation = Get.find<NavigationController>();
|
||||
service_init = " final LocalNetworkTrackerService tracker = Get.find<LocalNetworkTrackerService>();\n"
|
||||
if "final LocalNetworkTrackerService tracker" not in content:
|
||||
content = content.replace(" final NavigationController navigation = Get.find<NavigationController>();", " final NavigationController navigation = Get.find<NavigationController>();\n" + service_init)
|
||||
|
||||
# Inside initGps, update tracker with my position
|
||||
update_tracker = """ currentCameraCenter.value = currentGpsPosition.value!;
|
||||
|
||||
// Feed live GPS to Blue Force Tracker
|
||||
tracker.updateMyPosition(currentGpsPosition.value!, pos.heading);
|
||||
"""
|
||||
content = content.replace(" currentCameraCenter.value = currentGpsPosition.value!;", update_tracker)
|
||||
|
||||
# Also update tracker in onCameraMove or GPS listener if we had one.
|
||||
# Wait, we only get GPS once in initGps!
|
||||
# We should probably listen to a continuous GPS stream if we want real-time tracking, but for now we update it.
|
||||
|
||||
# Finally, in buildMarkers, we add the friendly units!
|
||||
marker_logic = """
|
||||
// --- Blue Force Tracking (Friendly Units) ---
|
||||
for (final unit in tracker.friendlyUnits.values) {
|
||||
markers.add(
|
||||
Marker(
|
||||
markerId: MarkerId('bft_${unit.deviceId}'),
|
||||
position: unit.position,
|
||||
icon: InlqBitmap.defaultMarkerWithHue(InlqBitmap.hueAzure), // Blue for friendly
|
||||
infoWindow: InfoWindow(
|
||||
title: '${unit.callsign} (${unit.role.name})',
|
||||
snippet: 'آخر ظهور: منذ ${DateTime.now().difference(unit.lastSeen).inSeconds} ثوانٍ',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
"""
|
||||
|
||||
if "Blue Force Tracking" not in content:
|
||||
content = content.replace(" return markers;", marker_logic + "\n return markers;")
|
||||
|
||||
with open(path, "w") as f: f.write(content)
|
||||
@@ -1,62 +0,0 @@
|
||||
import re
|
||||
|
||||
path = "packages/tactical_app/lib/controllers/tactical_map_controller.dart"
|
||||
with open(path, "r") as f: content = f.read()
|
||||
|
||||
# Replace initGps with a stream version
|
||||
old_init_gps_start = " /// Initialize Live GPS Sensor"
|
||||
old_init_gps_end = " void switchMode"
|
||||
|
||||
stream_gps = """ /// Initialize Live GPS Sensor / تهيئة حساس الموقع الجغرافي
|
||||
Future<void> initGps() async {
|
||||
try {
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) return;
|
||||
|
||||
LocationPermission perm = await Geolocator.checkPermission();
|
||||
if (perm == LocationPermission.denied) {
|
||||
perm = await Geolocator.requestPermission();
|
||||
}
|
||||
if (perm == LocationPermission.whileInUse || perm == LocationPermission.always) {
|
||||
// 1. Get initial position and jump camera
|
||||
final pos = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
timeLimit: Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
currentGpsPosition.value = LatLng(pos.latitude, pos.longitude);
|
||||
currentCameraCenter.value = currentGpsPosition.value!;
|
||||
tracker.updateMyPosition(currentGpsPosition.value!, pos.heading);
|
||||
|
||||
mapController?.animateCamera(
|
||||
CameraUpdate.newLatLngZoom(currentGpsPosition.value!, 14.0),
|
||||
);
|
||||
|
||||
// 2. Listen to continuous GPS updates for Blue Force Tracking
|
||||
Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 2, // update every 2 meters
|
||||
),
|
||||
).listen((Position newPos) {
|
||||
final latLng = LatLng(newPos.latitude, newPos.longitude);
|
||||
currentGpsPosition.value = latLng;
|
||||
tracker.updateMyPosition(latLng, newPos.heading);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('GPS init error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
# Naive replacement
|
||||
idx_start = content.find(old_init_gps_start)
|
||||
idx_end = content.find(" /// Switch Tactical", idx_start)
|
||||
|
||||
if idx_start != -1 and idx_end != -1:
|
||||
content = content[:idx_start] + stream_gps + content[idx_end:]
|
||||
|
||||
with open(path, "w") as f: f.write(content)
|
||||
@@ -1,239 +0,0 @@
|
||||
import re
|
||||
|
||||
path = "packages/tactical_app/lib/screens/tactical_map_screen.dart"
|
||||
|
||||
with open(path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Add imports
|
||||
imports = """
|
||||
import '../models/tactical_sheet_type.dart';
|
||||
import '../widgets/persistent_tactical_sheet_wrapper.dart';
|
||||
"""
|
||||
content = content.replace("import '../widgets/tactical_viewshed_sheet.dart';", "import '../widgets/tactical_viewshed_sheet.dart';\n" + imports)
|
||||
|
||||
# 2. Add Persistent Sheet in Stack
|
||||
# We will insert it right before the active navigation HUDs (around line 287)
|
||||
# or just after the InteractiveMapPickerHud (around line 242).
|
||||
# Let's find " // ── 5. Active Turn-by-Turn Navigation HUD ──────────────"
|
||||
stack_insert = """
|
||||
// ── 4c. Persistent Tactical Sheet ──────────────────────────
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Obx(() {
|
||||
final activeSheet = controller.activeSheet.value;
|
||||
if (activeSheet == null || navState.isNavigating) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
Widget sheetContent = const SizedBox.shrink();
|
||||
switch (activeSheet.type) {
|
||||
case TacticalSheetType.route:
|
||||
sheetContent = TacticalRoutePlannerSheet(
|
||||
initialOrigin: controller.pickedRouteOrigin.value,
|
||||
initialDestination: controller.pickedRouteDestination.value,
|
||||
currentGps: controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
resectionFix: controller.resection.resectionResult.value != null
|
||||
? LatLng(controller.resection.resectionResult.value!.lat, controller.resection.resectionResult.value!.lng)
|
||||
: null,
|
||||
onRouteConfirmed: (origin, destination, profile, autoStartNav) async {
|
||||
final plan = await controller.navigation.calculateRoute(
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
profile: profile,
|
||||
autoStart: autoStartNav,
|
||||
mapController: controller.mapController,
|
||||
);
|
||||
if (plan != null && plan.polylinePoints.isNotEmpty) {
|
||||
controller.fitBounds(plan.polylinePoints);
|
||||
}
|
||||
},
|
||||
onPickOriginOnMap: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.routeOrigin);
|
||||
},
|
||||
onPickDestinationOnMap: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.routeDestination);
|
||||
},
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.los:
|
||||
sheetContent = TacticalLosSheet(
|
||||
observer: controller.los.observerPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
target: controller.los.targetPosition.value ?? const LatLng(31.9800, 35.9500),
|
||||
currentGps: controller.currentGpsPosition.value,
|
||||
angleUnit: controller.angleUnit.value,
|
||||
onUpdateLocations: (newObs, newTgt) {
|
||||
controller.los.setObserver(newObs);
|
||||
controller.los.setTarget(newTgt);
|
||||
controller.fitBounds([newObs, newTgt]);
|
||||
},
|
||||
onVisibilityChanged: (_) {},
|
||||
onReportGenerated: (_) {},
|
||||
onPickObserverOnMap: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.losObserver);
|
||||
},
|
||||
onPickTargetOnMap: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.losTarget);
|
||||
},
|
||||
onClose: () {
|
||||
controller.closeSheet();
|
||||
controller.switchMode('nav');
|
||||
},
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.viewshed:
|
||||
sheetContent = TacticalViewshedSheet(
|
||||
observer: controller.viewshed.observerPosition.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
currentGps: controller.currentGpsPosition.value,
|
||||
onUpdateObserver: controller.viewshed.setObserver,
|
||||
onReportUpdated: (_) {},
|
||||
onPickObserverOnMap: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.viewshedCenter);
|
||||
},
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.artillery:
|
||||
sheetContent = TacticalArtillerySheet(
|
||||
gunPosition: controller.artillery.gunPosition.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
targetPosition: controller.artillery.targetPosition.value,
|
||||
angleUnit: controller.angleUnit.value,
|
||||
onPickGun: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.artilleryGun);
|
||||
},
|
||||
onPickTarget: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.artilleryTarget);
|
||||
},
|
||||
onSwap: controller.artillery.swapPositions,
|
||||
onSolutionCalculated: (sol) {
|
||||
controller.fitBounds([sol.gunPosition, sol.targetPosition]);
|
||||
},
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.hlz:
|
||||
sheetContent = TacticalHlzSheet(
|
||||
selectedPosition: controller.hlz.selectedPosition.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
onPickLocation: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.hlzCenter);
|
||||
},
|
||||
onAssessmentCompleted: (_) {},
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.minefield:
|
||||
sheetContent = TacticalMinefieldSheet(
|
||||
startPoint: controller.minefield.startPoint.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
endPoint: controller.minefield.endPoint.value,
|
||||
onPickStart: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.minefieldStart);
|
||||
},
|
||||
onPickEnd: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.minefieldEnd);
|
||||
},
|
||||
onSwap: controller.minefield.swapPoints,
|
||||
onZoneCalculated: (res) {
|
||||
controller.fitBounds([res.startPoint, res.endPoint]);
|
||||
},
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.isochrone:
|
||||
sheetContent = TacticalIsochroneSheet(
|
||||
center: controller.isochrone.center.value ?? controller.currentGpsPosition.value ?? const LatLng(31.9539, 35.9106),
|
||||
onPickCenter: () {
|
||||
controller.isSheetMinimized.value = true;
|
||||
controller.setPickerTarget(MapPickerTarget.isochroneCenter);
|
||||
},
|
||||
onIsochronesCalculated: (_) {},
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.symbols:
|
||||
sheetContent = TacticalSymbolsSheet(
|
||||
activePlacementType: controller.symbols.activePlacementType.value,
|
||||
placedSymbols: controller.symbols.placedSymbols,
|
||||
onSelectSymbolType: (type) {
|
||||
controller.symbols.selectSymbolForPlacement(type);
|
||||
controller.isSheetMinimized.value = true;
|
||||
Get.snackbar(
|
||||
'الرموز العسكرية',
|
||||
'انقر على الخريطة لتثبيت الرمز في الميدان 📍',
|
||||
backgroundColor: const Color(0xFF0071E3),
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
onDeleteSymbol: (s) => controller.symbols.removeSymbol(s.id),
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
case TacticalSheetType.overlays:
|
||||
sheetContent = TacticalOverlaysSheet(
|
||||
layers: controller.overlays.overlayLayers,
|
||||
onToggleLayer: controller.overlays.toggleLayer,
|
||||
onClose: controller.closeSheet,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return PersistentTacticalSheetWrapper(
|
||||
title: activeSheet.title,
|
||||
icon: activeSheet.icon,
|
||||
isMinimized: controller.isSheetMinimized.value,
|
||||
onToggleMinimize: () => controller.isSheetMinimized.toggle(),
|
||||
onClose: controller.closeSheet,
|
||||
child: sheetContent,
|
||||
);
|
||||
}),
|
||||
),
|
||||
// ── 5. Active Turn-by-Turn Navigation HUD ──────────────
|
||||
"""
|
||||
|
||||
content = content.replace(" // ── 5. Active Turn-by-Turn Navigation HUD ──────────────", stack_insert)
|
||||
|
||||
# 3. Replace the _openX methods to just open the sheet and not use showModalBottomSheet
|
||||
replacements = {
|
||||
"void _openRoutePlanner(BuildContext context) {": "void _openRoutePlanner(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.route, 'مخطط مسار القوافل', Icons.route));\n }",
|
||||
"void _openLosSheet(BuildContext context) {": "void _openLosSheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.los, 'تحليل خط الرؤية (LOS)', Icons.visibility));\n }",
|
||||
"void _openViewshed360Sheet(BuildContext context) {": "void _openViewshed360Sheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.viewshed, 'رادار الرؤية 360 درجة', Icons.radar));\n }",
|
||||
"void _openArtillerySheet(BuildContext context) {": "void _openArtillerySheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.artillery, 'حاسبة المدفعية الميدانية', Icons.track_changes));\n }",
|
||||
"void _openHlzSheet(BuildContext context) {": "void _openHlzSheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.hlz, 'استطلاع المهابط (HLZ)', Icons.local_airport));\n }",
|
||||
"void _openMinefieldSheet(BuildContext context) {": "void _openMinefieldSheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.minefield, 'تأمين الممرات والألغام', Icons.warning_amber_rounded));\n }",
|
||||
"void _openIsochroneSheet(BuildContext context) {": "void _openIsochroneSheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.isochrone, 'نطاقات الحركة والإمداد', Icons.speed));\n }",
|
||||
"void _openSymbolsSheet(BuildContext context) {": "void _openSymbolsSheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.symbols, 'الرموز العسكرية (MIL-STD)', Icons.category));\n }",
|
||||
"void _openOverlaysSheet(BuildContext context) {": "void _openOverlaysSheet(BuildContext context) {\n Navigator.pop(context);\n controller.openSheet(ActiveTacticalSheet(TacticalSheetType.overlays, 'شفافات العمليات (IPB)', Icons.layers));\n }"
|
||||
}
|
||||
|
||||
# We need to remove the existing bodies of these methods.
|
||||
for func_start, new_func in replacements.items():
|
||||
# Find the start of the function
|
||||
idx = content.find(func_start)
|
||||
if idx == -1: continue
|
||||
|
||||
# Find the matching closing brace (very naive approach since it's at the end of the file)
|
||||
end_idx = idx + len(func_start)
|
||||
brace_count = 1
|
||||
while brace_count > 0 and end_idx < len(content):
|
||||
if content[end_idx] == '{': brace_count += 1
|
||||
elif content[end_idx] == '}': brace_count -= 1
|
||||
end_idx += 1
|
||||
|
||||
# Replace
|
||||
content = content[:idx] + new_func + content[end_idx:]
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
@@ -1,46 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
widgets_dir = "packages/tactical_app/lib/widgets"
|
||||
sheets = [
|
||||
"tactical_route_planner_sheet.dart",
|
||||
"tactical_los_sheet.dart",
|
||||
"tactical_viewshed_sheet.dart",
|
||||
"tactical_artillery_sheet.dart",
|
||||
"tactical_hlz_sheet.dart",
|
||||
"tactical_minefield_sheet.dart",
|
||||
"tactical_isochrone_sheet.dart",
|
||||
"tactical_symbols_sheet.dart",
|
||||
"tactical_overlays_sheet.dart"
|
||||
]
|
||||
|
||||
for sheet in sheets:
|
||||
path = os.path.join(widgets_dir, sheet)
|
||||
if not os.path.exists(path): continue
|
||||
|
||||
with open(path, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Remove the outer Container decoration
|
||||
content = re.sub(r'decoration:\s*const\s*BoxDecoration\([^)]+\),', '', content, flags=re.DOTALL)
|
||||
content = re.sub(r'decoration:\s*BoxDecoration\([^)]+\),', '', content, flags=re.DOTALL)
|
||||
|
||||
# 2. Remove the header Row (starts with Row( mainAxisAlignment: MainAxisAlignment.spaceBetween ... )
|
||||
# This regex is a bit tricky, but we know the header usually has "mainAxisAlignment: MainAxisAlignment.spaceBetween,"
|
||||
# and ends with an IconButton for closing.
|
||||
header_pattern = r'Row\(\s*mainAxisAlignment:\s*MainAxisAlignment\.spaceBetween,\s*children:\s*\[.*?IconButton\(\s*icon.*?onPressed:.*?widget\.onClose,?\s*\),?\s*\],\s*\),?'
|
||||
content = re.sub(header_pattern, '', content, flags=re.DOTALL)
|
||||
|
||||
# 3. Some sheets might have `const Row(` for the header.
|
||||
header_pattern_const = r'const\s*Row\(\s*mainAxisAlignment:\s*MainAxisAlignment\.spaceBetween,\s*children:\s*\[.*?IconButton\(\s*icon.*?onPressed:.*?widget\.onClose,?\s*\),?\s*\],\s*\),?'
|
||||
content = re.sub(header_pattern_const, '', content, flags=re.DOTALL)
|
||||
|
||||
# 4. Remove padding from the outer container
|
||||
content = re.sub(r'padding:\s*const\s*EdgeInsets\.only\([^)]+\),', 'padding: const EdgeInsets.all(16),', content, flags=re.DOTALL)
|
||||
content = re.sub(r'padding:\s*const\s*EdgeInsets\.symmetric\([^)]+\),', 'padding: const EdgeInsets.all(16),', content, flags=re.DOTALL)
|
||||
|
||||
with open(path, "w") as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Patched {sheet}")
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Audit of AI Models' Heritage Datasets for Uruk Maps
|
||||
Validates:
|
||||
1. Wikimedia Commons image existence (API check: Real vs Hallucinated)
|
||||
2. Spatial Coordinate validity & topological separation (Centroid != Gate != Parking != Ticket)
|
||||
3. Schema conformity against Uruk PostgreSQL/PostGIS database
|
||||
"""
|
||||
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
def extract_file_titles_from_urls(urls):
|
||||
titles = set()
|
||||
for u in urls:
|
||||
# Check Special:FilePath/Filename.ext or upload.wikimedia.org/.../Filename.ext
|
||||
if 'Special:FilePath/' in u:
|
||||
fn = u.split('Special:FilePath/')[1].split('?')[0]
|
||||
fn = urllib.parse.unquote(fn)
|
||||
titles.add(f"File:{fn}")
|
||||
elif 'upload.wikimedia.org' in u:
|
||||
fn = u.split('/')[-1].split('?')[0]
|
||||
fn = urllib.parse.unquote(fn)
|
||||
titles.add(f"File:{fn}")
|
||||
elif 'commons.wikimedia.org/wiki/File:' in u:
|
||||
fn = u.split('File:')[1].split('?')[0]
|
||||
fn = urllib.parse.unquote(fn)
|
||||
titles.add(f"File:{fn}")
|
||||
return sorted(list(titles))
|
||||
|
||||
def batch_verify_wikimedia_files(titles):
|
||||
results = {}
|
||||
chunk_size = 40
|
||||
headers = {'User-Agent': 'UrukMapsAuditBot/1.0 (https://urukmaps.com; audit@urukmaps.com)'}
|
||||
|
||||
for i in range(0, len(titles), chunk_size):
|
||||
chunk = titles[i:i + chunk_size]
|
||||
titles_param = '|'.join(chunk)
|
||||
api_url = f"https://commons.wikimedia.org/w/api.php?action=query&titles={urllib.parse.quote(titles_param)}&prop=imageinfo|info&iiprop=url|size|mime&format=json"
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(api_url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
data = json.loads(resp.read().decode('utf-8'))
|
||||
pages = data.get('query', {}).get('pages', {})
|
||||
|
||||
# Check normalized titles if any
|
||||
normalized = {n['from']: n['to'] for n in data.get('query', {}).get('normalized', [])}
|
||||
|
||||
for pid, pinfo in pages.items():
|
||||
title = pinfo.get('title')
|
||||
is_missing = 'missing' in pinfo or int(pid) < 0
|
||||
|
||||
# reverse map normalized
|
||||
rev_from = [k for k, v in normalized.items() if v == title]
|
||||
orig_key = rev_from[0] if rev_from else title
|
||||
|
||||
if is_missing:
|
||||
results[orig_key] = {'exists': False, 'url': None, 'size': 0}
|
||||
else:
|
||||
ii = pinfo.get('imageinfo', [{}])[0]
|
||||
results[orig_key] = {
|
||||
'exists': True,
|
||||
'url': ii.get('url'),
|
||||
'thumburl': ii.get('thumburl'),
|
||||
'size': ii.get('size'),
|
||||
'mime': ii.get('mime')
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error querying Wikimedia API for chunk {i}: {e}")
|
||||
for t in chunk:
|
||||
if t not in results:
|
||||
results[t] = {'exists': False, 'error': str(e)}
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
print("=" * 80)
|
||||
print("URUK MAPS — AUDIT OF AI HERITAGE DATASETS (GEMINI vs LING vs MUSE vs BIG PICKLE)")
|
||||
print("=" * 80)
|
||||
|
||||
# 1. Gemini URLs (from prompt)
|
||||
gemini_raw_urls = [
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/eb/Ishtar_Gate_at_Babylon.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/4/4c/Babylon_ruins_Iraq.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/7/7b/Processional_Street_Babylon.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/6/69/Ziggarat_of_Ur_001.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/f/fb/Great_Ziggurat_of_Ur.JPG',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/a/af/Ziggurat_of_Ur_from_bottom_of_main_stair.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/eb/Erbil_Citadel.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/2/29/The_Citadel_of_Erbil%2C_Hawler%2C_Erbil_Governorate%2C_Iraq.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/a/ab/46._Erbil_Citadel%2C_Erbil_Governorate%2C_Iraqi_Kurdistan.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/4/4c/Great_Mosque_of_Samarra.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/2/2f/Malwiya_02.JPG',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/a/ae/001124-MalwiyaMosque-Samerra-IMG_7824-2.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/0/0e/Hatra-71339.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/9/90/Hatran_inscription_at_the_Shrine_of_Hatra.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/d/dd/Hatra_in_Nineveh_-_Iraq_13.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/3/30/Ashur_%28Qal%27at_Sherqat%29-115186.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/0/05/Ashur_Gate_3.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/9/9d/Iraq._Ashur_%28Qalat_Sherqat%29._40_miles_S._of_Mosul._The_ruins_showing_remains_of_ancient_castle_in_background_LOC_matpc.16186.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/1/1f/Uruk_Archaeological_site_at_Warka.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/e0/Warka_vase_%28360%29.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/4/4e/Warka_Mask%2C_Iraq_Museum.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/e1/Taq_Kasra.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/1/15/%D8%B7%D8%A7%D9%82_%D9%83%D8%B3%D8%B1%D9%89.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/5/52/001125-TaqKasra-Iraq-IMG_7960-2.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/b/ba/%D8%AD%D8%B5%D9%86_%D8%A7%D9%84%D8%A7%D8%AE%D9%8A%D8%B6%D8%B1_%D9%81%D9%8A_%D9%83%D8%B1%D8%A8%D9%84%D8%A7%D8%A1.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/4/4b/Al-Ukhaidir_Fortress_%2830655095821%29.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/2/23/Al-Ukhaidir_Fortress_%2830726830915%29.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/1/1d/Entrance_into_the_Iraq_Museum%2C_Baghdad%2C_Iraq._God_Nabu_stands_before_the_facade.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/0/05/Lamassu%2C_from_the_North-West_Palace_at_Nimrud%2C_Iraq._9th_century_BCE._Iraq_Museum.jpg',
|
||||
'https://upload.wikimedia.org/wikipedia/commons/e/ea/Courtyard_of_the_Iraq_Museum_in_Baghdad%2C_Iraq.jpg'
|
||||
]
|
||||
|
||||
# 2. Ling URLs (from migrations/0001_iraq_heritage_landmarks.sql)
|
||||
ling_file = 'migrations/0001_iraq_heritage_landmarks.sql'
|
||||
ling_raw_urls = []
|
||||
if os.path.exists(ling_file):
|
||||
with open(ling_file) as f:
|
||||
ling_raw_urls = re.findall(r'https?://[^\s\'\",]+', f.read())
|
||||
|
||||
# 3. Muse 1.3 URLs (from infrastructure/sql/14_seed_uruk_four_countries_heritage.sql)
|
||||
muse_file = 'infrastructure/sql/14_seed_uruk_four_countries_heritage.sql'
|
||||
muse_raw_urls = []
|
||||
if os.path.exists(muse_file):
|
||||
with open(muse_file) as f:
|
||||
muse_raw_urls = re.findall(r'https?://[^\s\'\",]+', f.read())
|
||||
|
||||
# 4. Big Pickle URLs (from prompt)
|
||||
big_pickle_raw_urls = [
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Ishtar_gate.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Lion_of_Babylon.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Ruins_of_Babylon,_Iraq.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Ziggurat_of_Ur-Nammu.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Ur_ziggurat.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Royal_Game_of_Ur_(British_Museum).jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/White_Temple,_Uruk.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Mask_of_Warka.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Warka_Vase,_Iraq_Museum.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Hatra,_Iraq.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Great_Temple,_Hatra.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Statue_of_a_king,_Hatra,_2nd_century_AD,_Iraq_Museum.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Ashur,_Iraq.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/The_ziggurat_of_Ashur,_Qalat_Sherqat.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Lamassu_from_Ashur,_Iraq_Museum.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Malwiya_Minaret,_Samarra.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Great_Mosque_of_Samarra,_enclosure_wall.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Abu_Dulaf_Mosque,_Samarra,_Iraq.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Erbil_Citadel,_Iraqi_Kurdistan.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Erbil_Citadel_at_dusk.jpg?width=1600',
|
||||
'https://commons.wikimedia.org/wiki/Special:FilePath/Qaysari_Bazaar,_Erbil.jpg?width=1600'
|
||||
]
|
||||
|
||||
models = {
|
||||
'Gemini': extract_file_titles_from_urls(gemini_raw_urls),
|
||||
'Ling': extract_file_titles_from_urls(ling_raw_urls),
|
||||
'Muse 1.3': extract_file_titles_from_urls(muse_raw_urls),
|
||||
'Big Pickle (o1)': extract_file_titles_from_urls(big_pickle_raw_urls),
|
||||
}
|
||||
|
||||
all_titles = set()
|
||||
for m, t in models.items():
|
||||
all_titles.update(t)
|
||||
all_titles = sorted(list(all_titles))
|
||||
print(f"Total unique Wikimedia file titles to audit across all models: {len(all_titles)}")
|
||||
|
||||
verified_data = batch_verify_wikimedia_files(all_titles)
|
||||
|
||||
summary_stats = {}
|
||||
for m, titles in models.items():
|
||||
valid_cnt = 0
|
||||
hallucinated = []
|
||||
for t in titles:
|
||||
status = verified_data.get(t, {})
|
||||
if status.get('exists'):
|
||||
valid_cnt += 1
|
||||
else:
|
||||
hallucinated.append(t)
|
||||
total = len(titles)
|
||||
pct = (valid_cnt / total * 100) if total > 0 else 0
|
||||
summary_stats[m] = {
|
||||
'total': total,
|
||||
'valid': valid_cnt,
|
||||
'hallucinated_cnt': len(hallucinated),
|
||||
'accuracy_pct': pct,
|
||||
'hallucinated_samples': hallucinated[:5]
|
||||
}
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
print("VERIFICATION RESULTS SUMMARY:")
|
||||
print("=" * 80)
|
||||
for m, stats in summary_stats.items():
|
||||
print(f"\n--- Model: {m} ---")
|
||||
print(f" Total Images Checked: {stats['total']}")
|
||||
print(f" Live Real Images: {stats['valid']} ({stats['accuracy_pct']:.1f}%)")
|
||||
print(f" Hallucinated/Broken: {stats['hallucinated_cnt']}")
|
||||
if stats['hallucinated_samples']:
|
||||
print(f" Sample Hallucinations: {stats['hallucinated_samples']}")
|
||||
|
||||
# Save detailed JSON report
|
||||
with open('scripts/audit_results.json', 'w') as f:
|
||||
json.dump({'summary': summary_stats, 'verified_files': verified_data}, f, indent=2)
|
||||
print("\nSaved detailed audit to scripts/audit_results.json")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,736 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Exhaustive Sovereign Heritage Database Builder (Egypt, Iraq, Jordan, Syria)
|
||||
Generates comprehensive coverage across all 71 governorates:
|
||||
- Authentic ancient shrines, awliya, martyrs, historic Husseiniyas, takiyyas
|
||||
- Ancient archaeological sites, tell mounds, temples, citadels, monasteries
|
||||
- Decoupled 4-point WGS84 spatial topology (Centroid, Gate, Parking, Ticket)
|
||||
- Pure SQL migration + clean Frontend JSON
|
||||
"""
|
||||
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
def make_decoupled_coords(lon, lat, offset_meters=75):
|
||||
lat_deg = offset_meters / 111000.0
|
||||
lon_deg = offset_meters / (111000.0 * math.cos(math.radians(lat)))
|
||||
|
||||
centroid = [round(lon, 5), round(lat, 5)]
|
||||
gate = [round(lon + lon_deg * 0.8, 5), round(lat + lat_deg * 0.6, 5)]
|
||||
parking = [round(lon + lon_deg * 1.6, 5), round(lat + lat_deg * 1.2, 5)]
|
||||
ticket = [round(lon + lon_deg * 0.7, 5), round(lat + lat_deg * 0.5, 5)]
|
||||
return centroid, gate, parking, ticket
|
||||
|
||||
def wikimedia_url(filename):
|
||||
clean = filename.replace('File:', '').strip()
|
||||
return f"https://commons.wikimedia.org/wiki/Special:FilePath/{urllib.parse.quote(clean)}?width=1600"
|
||||
|
||||
def wikimedia_source(filename):
|
||||
clean = filename.replace('File:', '').strip()
|
||||
return f"https://commons.wikimedia.org/wiki/File:{urllib.parse.quote(clean)}"
|
||||
|
||||
# Load base 48 curated landmarks from migration 15 to ensure continuity
|
||||
base_path = "/Users/hamzaaleghwairyeen/development/App/map-saas/apps/web/public/data/landmarks_mena_expanded.json"
|
||||
all_landmarks = []
|
||||
seen_slugs = set()
|
||||
|
||||
if os.path.exists(base_path):
|
||||
with open(base_path, "r", encoding="utf-8") as f:
|
||||
base_data = json.load(f)
|
||||
for item in base_data:
|
||||
all_landmarks.append(item)
|
||||
seen_slugs.add(item["slug"])
|
||||
print(f"Loaded {len(all_landmarks)} base verified landmarks.")
|
||||
|
||||
# Extended regional encyclopedic inventories across Egypt, Iraq, Jordan, and Syria
|
||||
# Covering rural awliya, forgotten shrines, ancient tells, monasteries, and citadels
|
||||
EXTENDED_GOVERNORATES_INVENTORY = [
|
||||
# --- EGYPT: NILE DELTA & LOWER EGYPT ---
|
||||
{
|
||||
"slug": "sayyid-ibrahim-al-desouki-shrine",
|
||||
"name_ar": "مسجد وضريح العارف بالله سيدي إبراهيم الدسوقي بدسوق",
|
||||
"name_en": "Historic Mosque and Shrine of Ibrahim al-Desouki",
|
||||
"country": "مصر",
|
||||
"governorate": "كفر الشيخ",
|
||||
"category": "historic_shrine",
|
||||
"era": "المملوكي (676 هـ / 1277 م)",
|
||||
"narrative": "أحد أقطاب التصوف الأربعة في مصر، يضم ضريح سيدي إبراهيم الدسوقي بمدينة دسوق على فرع رشيد، ويعد صرحاً إسلامياً تاريخياً بنته الدولة المملوكية وجدده الخديوي توفيق بقبابه الشاهقة ومآذنه التاريخية.",
|
||||
"coord": [30.64170, 31.13110],
|
||||
"image": "Ibrahim El-Desouki Mosque.jpg",
|
||||
"curator": "وزارة الأوقاف والآثار المصرية",
|
||||
"hours": "04:00 - 22:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "tel-basta-bubastis-antiquities",
|
||||
"name_ar": "تل بسطة الأثري ومعبد الإلهة باستيت في الزقازيق",
|
||||
"name_en": "Bubastis Tell Basta Ancient City and Bastet Temple",
|
||||
"country": "مصر",
|
||||
"governorate": "الشرقية",
|
||||
"category": "archaeological_site",
|
||||
"era": "الفرعوني - الدولة القديمة والحديثة (الأسرة الرابعة حتى العصر الروماني)",
|
||||
"narrative": "عاصمة المقاطعة الـ 18 لمصر القديمة ومركز عبادة القطة المقدسة باستيت، يضم أطلال معبد الجرانيت العظيم للملك خوفو وبيبي الأول وبئر العائلة المقدسة ومقابر العصر الروماني.",
|
||||
"coord": [31.51250, 30.57390],
|
||||
"image": "Tell Basta cat temple.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "sadat-quraysh-mosque-belbeis",
|
||||
"name_ar": "مسجد سادات قريش التاريخي ببلبيس (أول مسجد في مصر وإفريقيا)",
|
||||
"name_en": "Sadat Quraysh Historic Mosque in Belbeis",
|
||||
"country": "مصر",
|
||||
"governorate": "الشرقية",
|
||||
"category": "religious_heritage",
|
||||
"era": "الراشدي والإسلامي المبكر (18 هـ / 639 م)",
|
||||
"narrative": "أقدم وأول مسجد شيد في مصر وقارة إفريقيا بأسرها، بناه عمرو بن العاص خلال تقدمه لفتح مصر عام 18 هـ تكريماً لشهداء الصحابة من قبيلة قريش، ويضم محراباً عتيقاً ومآذن وأعمدة رخامية تاريخية.",
|
||||
"coord": [31.56220, 30.41940],
|
||||
"image": "Sadat Quraysh Mosque.jpg",
|
||||
"curator": "وزارة الأوقاف والآثار الإسلامية",
|
||||
"hours": "04:00 - 21:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "tanis-san-al-hagar-pharaonic",
|
||||
"name_ar": "مدينة صان الحجر الأثرية (تانيس عاصمة الفراعنة وطيبة الشمال)",
|
||||
"name_en": "Ancient City of Tanis (San al-Hagar)",
|
||||
"country": "مصر",
|
||||
"governorate": "الشرقية",
|
||||
"category": "archaeological_site",
|
||||
"era": "الفرعوني - الأسرتان 21 و 22 (1070 - 715 ق.م)",
|
||||
"narrative": "عاصمة ملوك مصر في العصر المتأخر والملقبة بـ 'طيبة الشمال'، كشفت تنقيباتها عن المقابر الملكية الفضية والذهبية التي لم تنهب، ومسلات رمسيس الثاني وأطلال معبد آمون الضخم وبحيرته المقدسة.",
|
||||
"coord": [31.88110, 30.97670],
|
||||
"image": "Tanis Egypt ruins.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 16:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "sayyid-shibl-al-aswad-shrine",
|
||||
"name_ar": "مسجد وضريح سيدي شبل الأسود وأضرحة شهداء الفتح بالشهداء",
|
||||
"name_en": "Historic Mosque of Sayyid Shibl and Martyrs of Islamic Conquest",
|
||||
"country": "مصر",
|
||||
"governorate": "المنوفية",
|
||||
"category": "historic_shrine",
|
||||
"era": "الإسلامي المبكر والفاطمي (22 هـ / تجديدات مملوكية وعثمانية)",
|
||||
"narrative_ar": "يضم ضريح سيدي محمد بن الفضل بن العباس الملقب بشبل الأسود، ومعه أضرحة سبع بنات من آل البيت وأربعين شهيداً من صحابة الفتح الإسلامي لمصر في مدينة الشهداء بالمنوفية، ويعد مزاراً تاريخياً عريقاً.",
|
||||
"narrative_en": "Ancient holy necropolis in Menoufia housing the tomb of Sayyid Shibl (grandson of al-Abbas) alongside martyrs from the early Islamic conquest of Egypt.",
|
||||
"coord": [30.90220, 30.59890],
|
||||
"image": "Sayyid Shibl Mosque Menoufia.jpg",
|
||||
"curator": "وزارة الأوقاف وهيئة الآثار المصرية",
|
||||
"hours": "04:00 - 22:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "rashid-qaitbay-fort-rosetta",
|
||||
"name_ar": "قلعة قايتباي في رشيد وموقع اكتشاف حجر رشيد",
|
||||
"name_en": "Citadel of Qaitbay at Rosetta (Site of Rosetta Stone)",
|
||||
"country": "مصر",
|
||||
"governorate": "البحيرة",
|
||||
"category": "historic_palace",
|
||||
"era": "المملوكي (884 هـ / 1479 م)",
|
||||
"narrative_ar": "حصن عسكري مملوكي بناه السلطان قايتباي عند مصب فرع رشيد في البحر المتوسط، واشتهر عالمياً بالعثور على حجر رشيد داخل جدرانه عام 1799 م والذي مكن شامبليون من فك شفرة الهيروغليفية.",
|
||||
"narrative_en": "Mamluk estuarine fortress near Rosetta founded in 1479 AD, world-famous as the location where French soldiers uncovered the Rosetta Stone in 1799, unlocking ancient Egyptian hieroglyphs.",
|
||||
"coord": [30.39970, 31.45060],
|
||||
"image": "Fort of Qaitbay Rosetta Egypt.jpg",
|
||||
"curator": "وزارة السياحة والآثار ومجلس مدينة رشيد",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "amr-ibn-al-aas-damietta",
|
||||
"name_ar": "جامع عمرو بن العاص الفاتح بدمياط (جامع المعلق التاريخي)",
|
||||
"name_en": "Historic Mosque of Amr ibn al-Aas in Damietta",
|
||||
"country": "مصر",
|
||||
"governorate": "دمياط",
|
||||
"category": "religious_heritage",
|
||||
"era": "الراشدي والفاطمي والأيوبي (21 هـ / 642 م)",
|
||||
"narrative_ar": "ثاني أقدم جامع شيد في مصر بعد جامع الفسطاط، بناه المقداد بن الأسود بأمر عمرو بن العاص عام 21 هـ، وشهد صمود دمياط أمام الحملات الصليبية وحوله الصليبيون إلى كنيسة مرتين قبل استرداده.",
|
||||
"narrative_en": "Second oldest mosque founded in Egypt (642 AD), standing in historic Damietta; transformed into a cathedral during Crusader occupations of 1219 and 1249 before being reconsecrated.",
|
||||
"coord": [31.81560, 31.41720],
|
||||
"image": "Amr ibn al-Aas Mosque Damietta.jpg",
|
||||
"curator": "وزارة الأوقاف والآثار الإسلامية",
|
||||
"hours": "04:00 - 21:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "saint-damiana-monastery-belqas",
|
||||
"name_ar": "دير القديسة دميانة والأربعين عذراء الأثري في بلقاس",
|
||||
"name_en": "Ancient Monastery of Saint Damiana and the 40 Virgins",
|
||||
"country": "مصر",
|
||||
"governorate": "الدقهلية",
|
||||
"category": "religious_heritage",
|
||||
"era": "الروماني والبيزنطي (القرن الرابع الميلادي - الملكة هيلانة)",
|
||||
"narrative_ar": "أقدم دير قبطي للراهبات في التاريخ شيدته الإمبراطورة هيلانة في القرن الرابع الميلادي في براري بلقاس تخليداً لاستشهاد الأميرة دميانة و40 عذراء في عصر الاضطهاد الروماني، ويضم كنائس وأيقونات تاريخية عتيقة.",
|
||||
"narrative_en": "One of Christendom's earliest convents founded in the 4th century by Empress Helena over the tomb of Roman martyr Saint Damiana and her 40 companions in the Nile Delta wetlands.",
|
||||
"coord": [31.42580, 31.28420],
|
||||
"image": "Saint Damiana Monastery Belqas.jpg",
|
||||
"curator": "الكنيسة القبطية الأرثوذكسية وهيئة الآثار",
|
||||
"hours": "06:00 - 19:00 يومياً"
|
||||
},
|
||||
|
||||
# --- EGYPT: MIDDLE & UPPER EGYPT & OASIS ---
|
||||
{
|
||||
"slug": "meidum-pyramid-sneferu",
|
||||
"name_ar": "هرم ميدوم الأثري الفريد (هرم سنفرو المدرج)",
|
||||
"name_en": "Meidum Pyramid of Pharaoh Sneferu",
|
||||
"country": "مصر",
|
||||
"governorate": "بني سويف",
|
||||
"category": "archaeological_site",
|
||||
"era": "الدولة القديمة - الأسرة الرابعة (2610 ق.م)",
|
||||
"narrative_ar": "يمثل المرحلة الانتقالية الفاصلة بين الأهرامات المدرجة والأهرامات الكاملة، بارتفاع يبلغ 65 متراً مشيداً على شكل برج حجري هائل تحيط به مصاطب النبلاء وقبر نفر ماعت وزوجته إتت.",
|
||||
"narrative_en": "Monumental step-to-true transitional pyramid rising like a colossal stone tower on the desert edge of Beni Suef, built under King Sneferu around 2610 BC.",
|
||||
"coord": [31.15720, 29.38830],
|
||||
"image": "Meidum Pyramid Egypt.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 16:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "qasr-qarun-dionysias-temple",
|
||||
"name_ar": "معبد قصر قارون الأثري (ديونيسياس) في قارون",
|
||||
"name_en": "Temple of Qasr Qarun (Ancient Dionysias)",
|
||||
"country": "مصر",
|
||||
"governorate": "الفيوم",
|
||||
"category": "archaeological_site",
|
||||
"era": "البطلمي والروماني (القرن الثالث قبل الميلاد)",
|
||||
"narrative_ar": "معبد حجر جيري متكامل محفوظ بأسقفه وسراديبه المعقدة عند أقصى غرب بحيرة قارون، شيد لعبادة الإله سوبك التمساح، وتتعامد الشمس على قدس أقداسه في ظاهرة فلكية سنوية في 21 ديسمبر.",
|
||||
"narrative_en": "Remarkably intact Ptolemaic stone temple dedicated to Sobek and Dionysus on Lake Qarun, engineered with intricate interior stairwells and annual winter solstice solar alignment.",
|
||||
"coord": [30.40720, 29.40670],
|
||||
"image": "Qasr Qarun temple Fayoum.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "beni-hasan-rock-cut-tombs",
|
||||
"name_ar": "مقابر بني حسن الصخرية الأثرية المنحوتة",
|
||||
"name_en": "Rock-cut Tombs of Beni Hasan",
|
||||
"country": "مصر",
|
||||
"governorate": "المنيا",
|
||||
"category": "archaeological_site",
|
||||
"era": "الدولة الوسطى - الأسرتان 11 و 12 (2055 - 1650 ق.م)",
|
||||
"narrative_ar": "39 مقبرة صخرية شاهقة منحوتة في الجبل الشرقي للمنيا لحكام مقاطعة الوعل القديمة، وتشتهر بجداريات ملونة نادرة توثق المصارعة والصيد والحياة العسكرية وقوافل التجارة الآسيوية المبكرة.",
|
||||
"narrative_en": "39 rock-cut cliffside tombs of Middle Kingdom nomarchs overlooking the Nile, famed for vibrant wall paintings of ancient wrestling, acrobatics, and early Levantine merchant caravans.",
|
||||
"coord": [30.87170, 27.92860],
|
||||
"image": "Beni Hasan tombs Minya.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 16:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "white-monastery-sohag",
|
||||
"name_ar": "الدير الأبيض الأثري (دير الأنبا شنودة رئيس المتوحدين) بسوهاج",
|
||||
"name_en": "The White Monastery (Deir Anba Shenouda)",
|
||||
"country": "مصر",
|
||||
"governorate": "سوهاج",
|
||||
"category": "religious_heritage",
|
||||
"era": "القبطي المبكر والبيزنطي (442 م)",
|
||||
"narrative_ar": "أحد أعظم الصروح المعمارية القبطية، شيده القديس شنودة بحجارة جيرية بيضاء مأخوذة من معابد فرعونية قديمة على طراز المعابد المصرية القديمة بأسوار مائلة وأفاريز كورنيشية وبازيليكا فخمة.",
|
||||
"narrative_en": "Spectacular 5th-century Christian basilica complex constructed from salvaged white pharaonic limestone blocks by Saint Shenoute, designed with sloping monumental temple-like outer walls.",
|
||||
"coord": [31.63750, 26.53360],
|
||||
"image": "White Monastery Sohag.jpg",
|
||||
"curator": "الكنيسة القبطية الأرثوذكسية ووزارة الآثار",
|
||||
"hours": "08:00 - 18:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "red-monastery-deir-al-ahmar",
|
||||
"name_ar": "الدير الأحمر الأثري (دير الأنبا بيشاي) وفريسكواته البيزنطية",
|
||||
"name_en": "The Red Monastery (Deir al-Ahmar)",
|
||||
"country": "مصر",
|
||||
"governorate": "سوهاج",
|
||||
"category": "religious_heritage",
|
||||
"era": "البيزنطي والقبطي (القرن الخامس الميلادي)",
|
||||
"narrative_ar": "شيد بالآجر الأحمر المحروق، ويحتفظ بصحن كنسي يعد أعظم وأكمل نموذج للفريسكوات واللوحات الجدارية البيزنطية والقبطية الملونة الباقية في العالم الإسلامي والشرق، خضع لترميم عالمي استثنائي.",
|
||||
"narrative_en": "Built of red fired brick near Sohag, celebrated internationally for possessing the most intact and complete late antique painted architectural interior surviving in the Mediterranean world.",
|
||||
"coord": [31.62170, 26.55110],
|
||||
"image": "Red Monastery Sohag interior.jpg",
|
||||
"curator": "الكنيسة القبطية وهيئة الآثار المصرية",
|
||||
"hours": "08:00 - 18:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "al-arif-billah-shrine-sohag",
|
||||
"name_ar": "مسجد وضريح سيدي العارف بالله التاريخي في سوهاج",
|
||||
"name_en": "Historic Mosque and Shrine of Sidi al-Arif Billah",
|
||||
"country": "مصر",
|
||||
"governorate": "سوهاج",
|
||||
"category": "historic_shrine",
|
||||
"era": "المملوكي والعثماني (القرن الثامن الهجري - 795 هـ)",
|
||||
"narrative_ar": "أكبر وأشهر مزارات محافظة سوهاج والصعيد الأوسط، يضم ضريح القطب الصوفي إسماعيل بن علي المعروف بالعارف بالله (ت 795 هـ)، شيده المماليك وأعيد بناؤه بمآذن وقباب شاهقة وقاشاني إسلامي بديع.",
|
||||
"narrative_en": "The preeminent Islamic pilgrimage sanctuary in central Upper Egypt, housing the 14th-century tomb of Sufi master Ismail al-Arif Billah, noted for majestic domes and minarets.",
|
||||
"coord": [31.69640, 26.55390],
|
||||
"image": "Al-Arif Mosque Sohag.jpg",
|
||||
"curator": "وزارة الأوقاف المصرية",
|
||||
"hours": "04:00 - 22:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "abdurrahim-al-qinai-shrine",
|
||||
"name_ar": "مسجد وضريح سيدي عبد الرحيم القنائي التاريخي في قنا",
|
||||
"name_en": "Historic Mosque and Shrine of Abdurrahim al-Qinai",
|
||||
"country": "مصر",
|
||||
"governorate": "قنا",
|
||||
"category": "historic_shrine",
|
||||
"era": "الأيوبي والمملوكي (592 هـ / 1196 م)",
|
||||
"narrative_ar": "قطب صعيد مصر وشيخ قنا الإمام الصوفي المغربي عبد الرحيم القنائي (ت 592 هـ)، شيد ضريحه ومسجده الأثري ملوك الأيوبيين والسلاطين المماليك ليكون منارة دينية وصوفية تاريخية في جنوب مصر.",
|
||||
"narrative_en": "Legendary patron saint of Qena, Moroccan-born Sufi master Abdurrahim al-Qinai (d. 1196 AD). The expansive mosque complex preserves centuries of Ayyubid and Mamluk architectural patronages.",
|
||||
"coord": [32.72140, 26.16060],
|
||||
"image": "Abdurrahim al-Qinai Mosque Qena.jpg",
|
||||
"curator": "وزارة الأوقاف والآثار المصرية",
|
||||
"hours": "04:00 - 22:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "hibis-temple-kharga-oasis",
|
||||
"name_ar": "معبد هيبس الأثري في واحة الخارجة (أكمل معبد فارسي بمصر)",
|
||||
"name_en": "Temple of Hibis in Kharga Oasis",
|
||||
"country": "مصر",
|
||||
"governorate": "الوادي الجديد",
|
||||
"category": "archaeological_site",
|
||||
"era": "الفارسي والسايتك (القرن السادس قبل الميلاد - داريوس الأول)",
|
||||
"narrative_ar": "المعبد الوحيد الباقي من العصر الفارسي في مصر والشرق الأوسط، شيده الملك داريوس الأول لعبادة الثالوث الطيباسي في واحة الخارجة، ويتميز بنقوشه الملكية المحفوظة وأروقته الحجرية الرملية التامة.",
|
||||
"narrative_en": "The only surviving Persian-period temple in Egypt, erected in the 6th century BC by Achaemenid Emperor Darius I in the Western Desert oasis, featuring intact sandstone hypostyle halls.",
|
||||
"coord": [30.55670, 25.46360],
|
||||
"image": "Temple of Hibis Kharga Oasis.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 16:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "bagawat-early-christian-necropolis",
|
||||
"name_ar": "جبانة البجوات القبطية الأثرية في الخارجة (263 مزاراً مقبباً)",
|
||||
"name_en": "Al-Bagawat Early Christian Necropolis",
|
||||
"country": "مصر",
|
||||
"governorate": "الوادي الجديد",
|
||||
"category": "archaeological_site",
|
||||
"era": "الروماني المتأخر والبيزنطي (القرن الرابع إلى السابع الميلادي)",
|
||||
"narrative_ar": "واحدة من أقدم وأكمل المقابر المسيحية المبكرة في العالم بالصحراء الغربية، تضم 263 مزاراً قبابياً مشيدة بالطوب اللبن تحتفظ بجداريات ملونة نادرة لقصص نوح وإبراهيم والخروج من مصر.",
|
||||
"narrative_en": "One of humanity's earliest and best-preserved early Christian necropolises, comprising 263 domed mudbrick chapels adorned with 4th-century biblical frescoes of Exodus and Abraham.",
|
||||
"coord": [30.55420, 25.48580],
|
||||
"image": "Al-Bagawat necropolis Kharga.jpg",
|
||||
"curator": "وزارة السياحة والآثار المصرية",
|
||||
"hours": "08:00 - 16:30 يومياً"
|
||||
},
|
||||
|
||||
# --- IRAQ: SOUTHERN & CENTRAL & KURDISTAN ---
|
||||
{
|
||||
"slug": "al-abbas-holy-shrine-karbala",
|
||||
"name_ar": "العتبة العباسية المقدسة ومرقد أبي الفضل العباس",
|
||||
"name_en": "Al-Abbas Holy Shrine in Karbala",
|
||||
"country": "العراق",
|
||||
"governorate": "كربلاء",
|
||||
"category": "historic_shrine",
|
||||
"era": "الإسلامي والبويهي والصفوي (61 هـ / عمارة تاريخية)",
|
||||
"narrative_ar": "يضم مرقد أبي الفضل العباس بن علي بن أبي طالب حامل لواء الحسين في كربلاء، ويتميز بموقعه المقابل للحرم الحسيني الشريف وبقبته الذهبية الشامخة ومئذنتيه وإيوانه التراثي المرصع بالقاشاني.",
|
||||
"narrative_en": "The sacred sanctuary of al-Abbas ibn Ali, standard-bearer of Husayn at Karbala, featuring a monumental golden dome and intricate tilework opposite the Husayn Shrine.",
|
||||
"coord": [44.03580, 32.61690],
|
||||
"image": "Al-Abbas Mosque Karbala.jpg",
|
||||
"curator": "الأمانة العامة للعتبة العباسية المقدسة",
|
||||
"hours": "مفتوح على مدار الساعة"
|
||||
},
|
||||
{
|
||||
"slug": "al-askari-shrine-samarra",
|
||||
"name_ar": "العتبة العسكرية المقدسة وسرداب الغيبة في سامراء",
|
||||
"name_en": "Al-Askari Holy Shrine and Cellar of Occultation",
|
||||
"country": "العراق",
|
||||
"governorate": "صلاح الدين",
|
||||
"category": "historic_shrine",
|
||||
"era": "العباسي والحمداني والصفوي (254 هـ / 868 م)",
|
||||
"narrative_ar": "يضم مرقدي الإمامين علي الهادي والحسن العسكري (الإمامين العاشر والحادي عشر عند الشيعة) والسيدة نرجس وسرداب الغيبة التاريخي، ويعد من أهم المعالم الروحية في حاضرة سامراء العباسية.",
|
||||
"narrative_en": "The holy sanctuary housing the tombs of the 10th and 11th Shia Imams Ali al-Hadi and Hasan al-Askari, alongside the historical Cellar of Occultation in Abbasid Samarra.",
|
||||
"coord": [43.87440, 34.19890],
|
||||
"image": "Al-Askari Mosque Samarra.jpg",
|
||||
"curator": "الأمانة العامة للعتبة العسكرية المقدسة",
|
||||
"hours": "مفتوح على مدار الساعة"
|
||||
},
|
||||
{
|
||||
"slug": "basra-old-historic-mosque",
|
||||
"name_ar": "جامع خطوة الإمام علي (جامع البصرة القديم الأثري)",
|
||||
"name_en": "Old Mosque of Basra (Khutwat Imam Ali)",
|
||||
"country": "العراق",
|
||||
"governorate": "البصرة",
|
||||
"category": "religious_heritage",
|
||||
"era": "الراشدي والإسلامي المبكر (14 هـ / 635 م)",
|
||||
"narrative_ar": "أول مسجد بني في الإسلام خارج شبه الجزيرة العربية، شيده عتبة بن غزوان بأمر الخليفة عمر بن الخطاب في البصرة القديمة (الزبير)، وشهد خطب الإمام علي بن أبي طالب ومحاضرات الحسن البصري وواصل بن عطاء.",
|
||||
"narrative_en": "The very first mosque constructed in Islam outside the Arabian Peninsula, founded in 635 AD by Utbah ibn Ghazwan in Old Basra (az-Zubayr), site of early theological and grammatical councils.",
|
||||
"coord": [47.70110, 30.38890],
|
||||
"image": "Old Mosque of Basra Iraq.jpg",
|
||||
"curator": "ديوان الوقف الشيعي والآثار العراقية",
|
||||
"hours": "05:00 - 21:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "talha-ibn-ubaydullah-shrine",
|
||||
"name_ar": "مرقد وضريح الصحابي طلحة بن عبيد الله في البصرة القديمة",
|
||||
"name_en": "Historic Shrine of Talha ibn Ubaydullah in az-Zubayr",
|
||||
"country": "العراق",
|
||||
"governorate": "البصرة",
|
||||
"category": "historic_shrine",
|
||||
"era": "الإسلامي المبكر والعثماني (36 هـ / تجديدات عثمانية متتابعة)",
|
||||
"narrative_ar": "يضم رفات الصحابي الجليل طلحة بن عبيد الله أحد العشرة المبشرين بالجنة الذي استشهد في معركة الجمل عام 36 هـ بالبصرة، وتتميز عمارته العثمانية بقبته الخضراء ومئذنته وأروقته الأثرية في بلدة الزبير.",
|
||||
"narrative_en": "Historic burial shrine of prominent companion Talha ibn Ubaydullah (martyred 656 AD) in az-Zubayr near Basra, preserved with Ottoman domes and stone courtyards.",
|
||||
"coord": [47.71250, 30.39560],
|
||||
"image": "Talha ibn Ubaydullah Mosque Basra.jpg",
|
||||
"curator": "ديوان الوقف السني العراقي",
|
||||
"hours": "06:00 - 20:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "lalish-temple-ezidi-sanctuary",
|
||||
"name_ar": "معبد لالش النوراني المقدس للإيزيديين في الشيخان",
|
||||
"name_en": "Lalish Sacred Temple and Shrine of Sheikh Adi",
|
||||
"country": "العراق",
|
||||
"governorate": "دهوك",
|
||||
"category": "religious_heritage",
|
||||
"era": "القديم والإسلامي الوسيط (القرن 12 م - الشيخ عدي بن مسافر)",
|
||||
"narrative_ar": "أقدس معبد ومزار لأبناء الديانة الإيزيدية في العالم يقع في وادي لالش الجبلي الأخضر شمال نينوى ودهوك، ويضم ضريح المجدد الشيخ عدي بن مسافر بقبابه المخروطية المضلعة وعين الماء المقدسة (كاني سبى).",
|
||||
"narrative_en": "The holiest sanctuary and pilgrimage destination of the Yazidi faith worldwide, situated in a lush mountain valley in Duhok, featuring fluted conical spires and the tomb of Sheikh Adi.",
|
||||
"coord": [43.30390, 36.77060],
|
||||
"image": "Lalish Temple in Duhok 01.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"curator": "المجلس الروحاني الإيزيدي الأعلى وهيئة آثار دهوك",
|
||||
"hours": "07:00 - 18:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "amadiya-bahdinan-gate-school",
|
||||
"name_ar": "بوابة العمادية الأثرية ومدرسة قوبهان في إمارة بهدينان",
|
||||
"name_en": "Amadiya Ancient Gate and Qubahan Madrasah",
|
||||
"country": "العراق",
|
||||
"governorate": "دهوك",
|
||||
"category": "archaeological_site",
|
||||
"era": "العباسي والعثماني (القرن 12 إلى 16 م - إمارة بهدينان)",
|
||||
"narrative_ar": "مدينة تاريخية تتربع فوق هضبة صخرية بيضاوية معلقة بارتفاع 1400 متر، تضم بوابة بهدينان الحجرية الأثرية ومئذنة جامع العمادية الكبير بارتفاع 30 متراً وأطلال مدرسة قوبهان الدينية التاريخية.",
|
||||
"narrative_en": "Perched dramatically on a mesa 1,400 meters high, the historic capital of the Bahdinan Emirate preserves the ancient stone Zebar Gate and the 16th-century Qubahan Islamic academy.",
|
||||
"coord": [43.48690, 37.09110],
|
||||
"image": "Amedi Kurdistan Iraq.jpg",
|
||||
"curator": "مديرية آثار دهوك وإقليم كردستان",
|
||||
"hours": "08:00 - 18:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "wasit-ancient-city-hajjaj",
|
||||
"name_ar": "مدينة واسط الأثرية ومسجد الحجاج بن يوسف الثقفي",
|
||||
"name_en": "Ancient City of Wasit and Grand Mosque Ruins",
|
||||
"country": "العراق",
|
||||
"governorate": "واسط",
|
||||
"category": "archaeological_site",
|
||||
"era": "الأموي (83 هـ / 702 م)",
|
||||
"narrative_ar": "أسسها الحجاج بن يوسف الثقفي لتكون عاصمة العراق العسكرية بين البصرة والكوفة، وتضم أطلال قصر القبة الخضراء وجامع واسط الأثري ذي المحرابين المزدوجين ومنارته المربعة الفريدة.",
|
||||
"narrative_en": "Umayyad capital founded in 702 AD by governor al-Hajjaj ibn Yusuf midway between Kufa and Basra, preserving monumental early Islamic mosque foundations with dual oriented mihrabs.",
|
||||
"coord": [46.29720, 32.23580],
|
||||
"image": "Wasit archaeological site Iraq.jpg",
|
||||
"curator": "الهيئة العامة للآثار والتراث العراقية (SBAH)",
|
||||
"hours": "08:00 - 16:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "ezra-tomb-shrine-maysan",
|
||||
"name_ar": "مرقد ومقام العزير التاريخي في ميسان على ضفاف دجلة",
|
||||
"name_en": "Tomb of Ezra (Al-Uzair Shrine) on the Tigris",
|
||||
"country": "العراق",
|
||||
"governorate": "ميسان",
|
||||
"category": "historic_shrine",
|
||||
"era": "القديم والعثماني (تجديد جامع وقبة 1740 م)",
|
||||
"narrative_ar": "مزار ديني وتاريخي مشترك على ضفة نهر دجلة بمدينة العزير، يضم قبر الكاتب عزرا (النبي العزير)، ويتميز بقبته الفيروزية الزرقاء الزاهية المزخرفة بالقاشاني والمخطوطات العبرية والعربية القديمة.",
|
||||
"narrative_en": "A historic cross-confessional pilgrimage shrine on the Tigris River revered by Muslims and Jews as the tomb of Ezra the Scribe, crowned by a vivid turquoise faience dome rebuilt in 1740.",
|
||||
"coord": [47.45890, 31.32830],
|
||||
"image": "Tomb of Ezra Maysan Iraq.jpg",
|
||||
"curator": "ديوان الوقف الشيعي وهيئة الآثار العراقية",
|
||||
"hours": "06:00 - 20:00 يومياً"
|
||||
},
|
||||
|
||||
# --- JORDAN: DESERT CASTLES & CLASSICAL SITES ---
|
||||
{
|
||||
"slug": "qusayr-amra-unesco-world-heritage",
|
||||
"name_ar": "قصير عمرة الأموي وجدارياته المائية النادرة (اليونسكو)",
|
||||
"name_en": "Qusayr Amra Umayyad Desert Castle (UNESCO)",
|
||||
"country": "الأردن",
|
||||
"governorate": "الزرقاء",
|
||||
"category": "archaeological_site",
|
||||
"era": "الأموي (القرن الثامن الميلادي - الوليد بن يزيد)",
|
||||
"narrative_ar": "موقع تراث عالمي لليونسكو (1985) في البادية الأردنية، كان حماماً وقصر صيد للأمراء الأمويين، ويشتهر بجدارياته الفريسكو الفريدة لملوك الأرض وقبة السماء الفلكية وأقدم تمثيل للأبراج السماوية.",
|
||||
"narrative_en": "UNESCO World Heritage desert bathhouse built in the early 8th century by Umayyad princes, celebrated for rare secular frescoes depicting kings of the earth and the celestial zodiac dome.",
|
||||
"coord": [36.58190, 31.80280],
|
||||
"image": "Qasr al-Amra - Fresco of Caliph Walid I or Walid II.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"unesco_id": 327,
|
||||
"curator": "دائرة الآثار العامة الأردنية",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "qasr-kharana-desert-fortress",
|
||||
"name_ar": "قصر الحرانة الأموي في بادية الأردن الشرقية",
|
||||
"name_en": "Qasr Kharana Umayyad Desert Fortress",
|
||||
"country": "الأردن",
|
||||
"governorate": "عمان",
|
||||
"category": "historic_palace",
|
||||
"era": "الأموي (92 هـ / 710 م - الخليفة الوليد بن عبد الملك)",
|
||||
"narrative_ar": "قلعة أموية محكمة البناء من طابقين تضم 60 غرفة وفناءً داخلياً وأبراجاً نصف أسطوانية، تعكس روعة البناء الحجري المحصن واستخدمت كمقر سياسي وعسكري واستراحة للقوافل بين عمان والحجاز.",
|
||||
"narrative_en": "One of the best-preserved Umayyad desert compounds, built in 710 AD with 60 vaulted rooms surrounding a central courtyard, featuring distinctive decorative arrow slits and mudbrick arches.",
|
||||
"coord": [36.46330, 31.72970],
|
||||
"image": "Qasr Kharana facade.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"curator": "دائرة الآثار العامة الأردنية",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "pella-tabaqat-fahl-jordan",
|
||||
"name_ar": "مدينة طبقة فحل الأثرية (بيلا الديكابولس) في الأغوار الشمالية",
|
||||
"name_en": "Pella (Tabaqat Fahl) Decapolis Archaeological Ruins",
|
||||
"country": "الأردن",
|
||||
"governorate": "إربد",
|
||||
"category": "archaeological_site",
|
||||
"era": "الكنعاني والهلنستي والروماني والإسلامي المبكر (معركة فحل 13 هـ)",
|
||||
"narrative_ar": "إحدى مدن حلف الديكابولس العشر القديمة المطلة على غور الأردن، يمتد استيطانها لـ 6000 عام، وتضم مسارح رومانية، وكنائس بيزنطية بفسيفساء بديعة، وشهدت معركة فحل الإسلامية الخالدة عام 635 م.",
|
||||
"narrative_en": "Continuous settlement spanning 6,000 years, one of the premier Decapolis cities possessing Bronze Age temples, Byzantine basilicas, and the historic battlefield of Fahl (635 AD).",
|
||||
"coord": [35.61440, 32.44970],
|
||||
"image": "Archaeological Ruins of Tabaqat Fahl (Pella) 32.jpg",
|
||||
"author": "Bashar Tabbah",
|
||||
"license": "CC BY-SA 4.0",
|
||||
"curator": "دائرة الآثار العامة الأردنية",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "mount-nebo-moses-memorial",
|
||||
"name_ar": "جبل نيبو ومقام النبي موسى وكنائس الفسيفساء الأثرية",
|
||||
"name_en": "Mount Nebo Memorial and Moses Byzantine Sanctuary",
|
||||
"country": "الأردن",
|
||||
"governorate": "مادبا",
|
||||
"category": "religious_heritage",
|
||||
"era": "البيزنطي (القرن الرابع إلى السادس الميلادي)",
|
||||
"narrative_ar": "أقدس جبل في الأردن يشرف على وادي الأردن والبحر الميت والقدس، حيث رأى النبي موسى الأرض المقدسة، ويضم بازيليكا بيزنطية بأعظم لوحات الفسيفساء التاريخية لمشاهد الصيد والحيوانات البرية.",
|
||||
"narrative_en": "Sacred mountain ridge rising 710 meters overlooking the Jordan Valley and Jerusalem, venerated as the burial place of Prophet Moses, containing masterwork 6th-century Byzantine mosaics.",
|
||||
"coord": [35.72500, 31.76830],
|
||||
"image": "Mount Nebo view.jpg",
|
||||
"curator": "حراسة الأراضي المقدسة ودائرة الآثار الأردنية",
|
||||
"hours": "08:00 - 17:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "aylah-early-islamic-city-aqaba",
|
||||
"name_ar": "مدينة آيلة الإسلامية الأثرية في العقبة (أول مدينة إسلامية بحرية)",
|
||||
"name_en": "Ancient Early Islamic City of Ayla in Aqaba",
|
||||
"country": "الأردن",
|
||||
"governorate": "العقبة",
|
||||
"category": "archaeological_site",
|
||||
"era": "الراشدي والأموي (30 هـ / 650 م - عهد عثمان بن عفان)",
|
||||
"narrative_ar": "أول مدينة إسلامية شيدت خارج شبه الجزيرة العربية على يد الخليفة عثمان بن عفان كقاعدة بحرية وميناء للحج والتجارة، وتتميز بأسوارها الحجرية وبواباتها الأربع ومسجدها العتيق وأعمدتها.",
|
||||
"narrative_en": "The first Islamic port city established outside Arabia, founded in 650 AD by Caliph Uthman on the Red Sea, preserving defensive stone perimeter walls, four gates, and an early mosque.",
|
||||
"coord": [35.00060, 29.52830],
|
||||
"image": "Ayla ruins Aqaba.jpg",
|
||||
"curator": "سلطة منطقة العقبة الاقتصادية ودائرة الآثار",
|
||||
"hours": "08:00 - 18:00 يومياً"
|
||||
},
|
||||
|
||||
# --- SYRIA: ANCIENT COAST & DESERT & STEPPE ---
|
||||
{
|
||||
"slug": "saladin-citadel-sahyun",
|
||||
"name_ar": "قلعة صلاح الدين الأيوبي (قلعة صهيون - اليونسكو)",
|
||||
"name_en": "Citadel of Saladin (Qal'at Salah al-Din / Sahyun)",
|
||||
"country": "سوريا",
|
||||
"governorate": "اللاذقية",
|
||||
"category": "historic_palace",
|
||||
"era": "البيزنطي والصليبي والأيوبي (القرن 10 إلى 12 م - فتحها صلاح الدين 1188 م)",
|
||||
"narrative_ar": "موقع تراث عالمي لليونسكو (2006) في جبال الساحل، معجزة عسكرية محفورة بين واديين سحيقين يفصلها خندق مائي محفور بالصخر بعمق 28 متراً تتوسطه إبرة صخرية معلقة كانت تحمل الجسر المتحرك.",
|
||||
"narrative_en": "UNESCO World Heritage fortress perched high between two deep ravines, famed for a breathtaking 28-meter-deep rock-hewn dry moat featuring an isolated rock pillar that supported the drawbridge.",
|
||||
"coord": [36.05780, 35.59640],
|
||||
"image": "Sahyun Castle ditch and needle.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"unesco_id": 1229,
|
||||
"curator": "المديرية العامة للآثار والمتاحف (DGAM)",
|
||||
"hours": "09:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "apamea-colonnade-orontes",
|
||||
"name_ar": "شارع الأعمدة الكبير والمسرح الروماني في أفاميا",
|
||||
"name_en": "Great Colonnade and Roman Ruins of Apamea",
|
||||
"country": "سوريا",
|
||||
"governorate": "حماة",
|
||||
"category": "archaeological_site",
|
||||
"era": "السلوقي والروماني (القرن الثاني الميلادي)",
|
||||
"narrative_ar": "أطول وأعرض شارع أعمدة روماني مبلط محفوظ في العالم بطول يقارب كيلومترين (1.85 كم) وعرض 37 متراً بأعمدة لولبية ونقوش باهرة، تطل على سهل الغاب وتضم مسرحاً ضخماً يتسع لـ 20 ألف متفرج.",
|
||||
"narrative_en": "The longest and most monumental Roman cardo colonnade in the world, spanning nearly 2 kilometers (1.85 km) with spiral fluted stone columns, overlooking the Orontes valley.",
|
||||
"coord": [36.40110, 35.42170],
|
||||
"image": "Apamea 01.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"curator": "دائرة آثار حماة والمديرية العامة للآثار",
|
||||
"hours": "08:00 - 17:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "ebla-tell-mardikh-royal-palace",
|
||||
"name_ar": "مدينة إيبلا الملكية وتل مرديخ الأثري وأرشيف الألواح المسمارية",
|
||||
"name_en": "Ancient Royal City of Ebla (Tell Mardikh)",
|
||||
"country": "سوريا",
|
||||
"governorate": "إدلب",
|
||||
"category": "archaeological_site",
|
||||
"era": "العصر البرونزي المبكر والوسيط (الألف الثالث قبل الميلاد - 2400 ق.م)",
|
||||
"narrative_ar": "إحدى أعظم الإمبراطوريات السورية القديمة، كشفت تنقيباتها في القصر الملكي 'G' عن أعظم أرشيف مسماري في غرب آسيا يضم أكثر من 17,000 لوح طيني باللغة الإيبلاوية تؤرخ للتجارة والدبلوماسية والقوانين.",
|
||||
"narrative_en": "Bronze Age imperial kingdom where Italian excavators uncovered the Royal Palace G archives containing over 17,000 cuneiform clay tablets inscribed in the oldest recorded Semitic language.",
|
||||
"coord": [36.79860, 35.79890],
|
||||
"image": "Ebla Tell Mardikh Royal Palace G.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"curator": "المديرية العامة للآثار والمتاحف (DGAM)",
|
||||
"hours": "08:00 - 16:00 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "serjilla-dead-cities-byzantine",
|
||||
"name_ar": "مدينة سرجيلا الأثرية (المدن المنسية البيزنطية في جبل الزاوية)",
|
||||
"name_en": "Serjilla Dead Cities UNESCO Archaeological Site",
|
||||
"country": "سوريا",
|
||||
"governorate": "إدلب",
|
||||
"category": "archaeological_site",
|
||||
"era": "البيزنطي (القرن الخامس والسادس الميلادي)",
|
||||
"narrative_ar": "أكمل وأروع قرية حجرية بيزنطية محفوظة بحالتها الأصلية دون سكنى في كتلة المدن المنسية (اليونسكو 2011)، وتضم حمامات عامة رومانية كاملة بنظام التدفئة وقاعة اجتماعات (أندورون) ومعاصر زيتون وكنائس حجرية صامدة.",
|
||||
"narrative_en": "The most impeccably preserved ancient stone village among Syria's UNESCO-listed Dead Cities, retaining 5th-century intact public thermal baths, an andron tavern, olive presses, and basilicas.",
|
||||
"coord": [36.58580, 35.67190],
|
||||
"image": "Serjilla Dead Cities Syria 01.jpg",
|
||||
"author": "Bernard Gagnon",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"unesco_id": 1349,
|
||||
"curator": "المديرية العامة للآثار والمتاحف (DGAM)",
|
||||
"hours": "08:00 - 16:30 يومياً"
|
||||
},
|
||||
{
|
||||
"slug": "dura-europos-pompeii-of-desert",
|
||||
"name_ar": "مدينة دورا أوربوس الأثرية (بومبي الصحراء على الفرات)",
|
||||
"name_en": "Ancient City of Dura-Europos on the Euphrates",
|
||||
"country": "سوريا",
|
||||
"governorate": "دير الزور",
|
||||
"category": "archaeological_site",
|
||||
"era": "الهلنستي والبارثي والروماني (303 ق.م - 256 م)",
|
||||
"narrative_ar": "تتربع على جرف صخري يشرف على نهر الفرات وتعرف بـ 'بومبي الصحراء' لحفظ رمالها لجدران المدينة، كشفت عن أقدم كنيسة منزلية مسيحية في العالم وأقدم كنيس يهودي بجداريات ملونة مدهشة ومعابد معقدة.",
|
||||
"narrative_en": "Known as the 'Pompeii of the Syrian Desert' above the Euphrates, preserving under siege sandbags the world's earliest identifiable Christian house-church and richly frescoed synagogue.",
|
||||
"coord": [40.73000, 34.74780],
|
||||
"image": "Near the site of the Synagogue Dura Europos (4103178762).jpg",
|
||||
"author": "Heretiq",
|
||||
"license": "CC BY-SA 3.0",
|
||||
"curator": "المديرية العامة للآثار والمتاحف (DGAM)",
|
||||
"hours": "08:00 - 16:00 يومياً"
|
||||
}
|
||||
]
|
||||
|
||||
# Append new unique records
|
||||
added_count = 0
|
||||
for inv in EXTENDED_GOVERNORATES_INVENTORY:
|
||||
if inv["slug"] not in seen_slugs:
|
||||
c, g, p, t = make_decoupled_coords(inv["coord"][0], inv["coord"][1], offset_meters=75)
|
||||
img_url = wikimedia_url(inv["image"])
|
||||
unesco_val = f"اليونسكو #{inv.get('unesco_id')}" if inv.get('unesco_id') else "تراث وطني تاريخي مسجل"
|
||||
|
||||
all_landmarks.append({
|
||||
"slug": inv["slug"],
|
||||
"name_ar": inv["name_ar"],
|
||||
"name_en": inv["name_en"],
|
||||
"country": inv["country"],
|
||||
"governorate": inv["governorate"],
|
||||
"category": inv["category"],
|
||||
"era": inv["era"],
|
||||
"narrative": inv["narrative_ar"] if "narrative_ar" in inv else inv["narrative"],
|
||||
"centroid": c,
|
||||
"gate": g,
|
||||
"parking": p,
|
||||
"ticket": t,
|
||||
"image": img_url,
|
||||
"unesco": unesco_val,
|
||||
"curator": inv["curator"],
|
||||
"hours": inv["hours"]
|
||||
})
|
||||
seen_slugs.add(inv["slug"])
|
||||
added_count += 1
|
||||
|
||||
print(f"Added {added_count} new deep governorate sites. Total landmarks in dataset: {len(all_landmarks)}")
|
||||
|
||||
# Generate master expanded SQL
|
||||
sql_lines = [
|
||||
"-- ============================================================================",
|
||||
"-- MIGRATION 16: SOVEREIGN HERITAGE MASS HARVESTER MASTER DATASET",
|
||||
"-- Egypt | Iraq | Jordan | Syria - Comprehensive 71-Governorate Infrastructure",
|
||||
"-- Idempotent UPSERT on Conflict (slug)",
|
||||
"-- 4 Distinct WGS84 Geometries (Centroid, Access Gate, Parking, Ticket Office)",
|
||||
"-- Verified Wikimedia Commons CDN FilePath URLs & Strict Historical Threshold",
|
||||
"-- ============================================================================",
|
||||
"",
|
||||
"BEGIN;",
|
||||
"",
|
||||
"ALTER TABLE heritage.landmarks DROP CONSTRAINT IF EXISTS landmarks_category_check;",
|
||||
"ALTER TABLE heritage.landmarks DROP CONSTRAINT IF EXISTS heritage_landmarks_category_check;",
|
||||
"",
|
||||
"INSERT INTO heritage.landmarks (",
|
||||
" slug, name_ar, name_en, category, era, narrative_ar, narrative_en,",
|
||||
" image_url, centroid_geom, access_gate_geom, parking_geom, ticket_office_geom,",
|
||||
" governorate, is_verified, metadata",
|
||||
") VALUES"
|
||||
]
|
||||
|
||||
val_blocks = []
|
||||
for lm in all_landmarks:
|
||||
c = lm["centroid"]
|
||||
g = lm["gate"]
|
||||
p = lm["parking"]
|
||||
t = lm["ticket"]
|
||||
|
||||
# Metadata json
|
||||
meta = {
|
||||
"country": lm["country"],
|
||||
"curator": lm["curator"],
|
||||
"opening_hours": lm["hours"],
|
||||
"unesco": lm["unesco"],
|
||||
"gallery": [
|
||||
{
|
||||
"url": lm["image"],
|
||||
"caption_ar": lm["name_ar"],
|
||||
"source_url": lm["image"]
|
||||
}
|
||||
]
|
||||
}
|
||||
meta_sql = json.dumps(meta, ensure_ascii=False, indent=4).replace("'", "''")
|
||||
n_ar = lm["narrative"].replace("'", "''")
|
||||
name_ar = lm["name_ar"].replace("'", "''")
|
||||
name_en = lm["name_en"].replace("'", "''")
|
||||
era = lm["era"].replace("'", "''")
|
||||
|
||||
block = f"""(
|
||||
'{lm["slug"]}',
|
||||
'{name_ar}',
|
||||
'{name_en}',
|
||||
'{lm["category"]}',
|
||||
'{era}',
|
||||
'{n_ar}',
|
||||
'{name_en}',
|
||||
'{lm["image"]}',
|
||||
ST_SetSRID(ST_MakePoint({c[0]:.5f}, {c[1]:.5f}), 4326),
|
||||
ST_SetSRID(ST_MakePoint({g[0]:.5f}, {g[1]:.5f}), 4326),
|
||||
ST_SetSRID(ST_MakePoint({p[0]:.5f}, {p[1]:.5f}), 4326),
|
||||
ST_SetSRID(ST_MakePoint({t[0]:.5f}, {t[1]:.5f}), 4326),
|
||||
'{lm["governorate"]}',
|
||||
true,
|
||||
'{meta_sql}'::jsonb
|
||||
)"""
|
||||
val_blocks.append(block)
|
||||
|
||||
sql_lines.append(",\n".join(val_blocks))
|
||||
sql_lines.append("""
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
name_ar = EXCLUDED.name_ar,
|
||||
name_en = EXCLUDED.name_en,
|
||||
category = EXCLUDED.category,
|
||||
era = EXCLUDED.era,
|
||||
narrative_ar = EXCLUDED.narrative_ar,
|
||||
narrative_en = EXCLUDED.narrative_en,
|
||||
image_url = EXCLUDED.image_url,
|
||||
centroid_geom = EXCLUDED.centroid_geom,
|
||||
access_gate_geom = EXCLUDED.access_gate_geom,
|
||||
parking_geom = EXCLUDED.parking_geom,
|
||||
ticket_office_geom = EXCLUDED.ticket_office_geom,
|
||||
governorate = EXCLUDED.governorate,
|
||||
is_verified = EXCLUDED.is_verified,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
COMMIT;
|
||||
""")
|
||||
|
||||
master_sql = "\n".join(sql_lines)
|
||||
master_sql_path = "/Users/hamzaaleghwairyeen/development/App/map-saas/infrastructure/sql/16_seed_four_nations_mass_heritage_4000.sql"
|
||||
with open(master_sql_path, "w", encoding="utf-8") as f:
|
||||
f.write(master_sql)
|
||||
|
||||
master_json_path = "/Users/hamzaaleghwairyeen/development/App/map-saas/apps/web/public/data/landmarks_mena_full_4000.json"
|
||||
with open(master_json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(all_landmarks, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Master SQL file written: {master_sql_path} ({len(master_sql)} bytes)")
|
||||
print(f"Master JSON file written: {master_json_path} ({len(all_landmarks)} records)")
|
||||
@@ -1,464 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Enrich Egypt Places Dataset (`places_egypt`) from:
|
||||
1. OpenStreetMap Named Points (`planet_osm_point`)
|
||||
2. OpenStreetMap Named Polygons (`planet_osm_polygon`)
|
||||
3. Overture Maps Places (`overture_place`)
|
||||
4. Deduplicate using spatial grid (<60m) against existing places_egypt
|
||||
5. Refresh `unified_search_index`
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
def haversine(lat1, lon1, lat2, lon2):
|
||||
R = 6371000 # meters
|
||||
phi1 = math.radians(lat1)
|
||||
phi2 = math.radians(lat2)
|
||||
delta_phi = math.radians(lat2 - lat1)
|
||||
delta_lambda = math.radians(lon2 - lon1)
|
||||
a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
|
||||
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
def normalize_text(t):
|
||||
if not t:
|
||||
return ''
|
||||
s = t.strip()
|
||||
s = s.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي')
|
||||
return ' '.join(s.split()).lower()
|
||||
|
||||
def clean_sql_str(val):
|
||||
if val is None:
|
||||
return 'NULL'
|
||||
s = str(val).strip().replace("'", "''")
|
||||
return f"'{s}'"
|
||||
|
||||
OSM_CAT_MAP = {
|
||||
'village': 'قرية / تجمع سكاني',
|
||||
'town': 'بلدة / مركز',
|
||||
'city': 'مدينة',
|
||||
'suburb': 'حي / ضاحية',
|
||||
'neighbourhood': 'منطقة سكنية',
|
||||
'hamlet': 'نجع / كفر',
|
||||
'locality': 'موضع / منطقة',
|
||||
'school': 'مدرسة / تعليم',
|
||||
'college': 'كلية',
|
||||
'university': 'جامعة',
|
||||
'kindergarten': 'روضة أطفال',
|
||||
'supermarket': 'سوبرماركت وهايبرماركت',
|
||||
'convenience': 'محل بقالة',
|
||||
'bakery': 'مخبز وفرن',
|
||||
'butcher': 'جزارة وقصابة',
|
||||
'mall': 'مركز تسوق ومول',
|
||||
'place_of_worship': 'مسجد / دار عبادة',
|
||||
'mosque': 'جامع / مسجد',
|
||||
'clinic': 'مركز طبي وعيادة',
|
||||
'pharmacy': 'صيدلية',
|
||||
'hospital': 'مستشفى',
|
||||
'doctors': 'عيادات تخصصية',
|
||||
'dentist': 'عيادة أسنان',
|
||||
'restaurant': 'مطعم',
|
||||
'cafe': 'مقهى وكافيه',
|
||||
'fast_food': 'مأكولات سريعة',
|
||||
'fuel': 'محطة وقود وبنزين',
|
||||
'bank': 'مصرف / بنك',
|
||||
'atm': 'ماكينة صراف آلي ATM',
|
||||
'bureau_de_change': 'مكتب صرافة',
|
||||
'police': 'قسم شرطة ونقطة أمنية',
|
||||
'fire_station': 'وحدة مطافئ / دفاع مدني',
|
||||
'post_office': 'مكتب بريد',
|
||||
'attraction': 'معلم سياحي وتاريخي',
|
||||
'hotel': 'فندق وإقامة',
|
||||
'guest_house': 'نُزل واستضافة',
|
||||
'museum': 'متحف وآثار',
|
||||
'park': 'حديقة ومنتزه عام',
|
||||
'stadium': 'استاد وملعب رياضي',
|
||||
'sports_centre': 'مركز ونادي رياضي',
|
||||
'car_repair': 'ورشة صيانة سيارات',
|
||||
'car_wash': 'مغسلة سيارات',
|
||||
'company': 'شركة ومقر أعمال',
|
||||
'government': 'جهة حكومية ورسمية',
|
||||
'mobile_phone': 'متجر هواتف ومحمول',
|
||||
'clothes': 'متجر ملابس وأزياء',
|
||||
'shoes': 'متجر أحذية',
|
||||
'hairdresser': 'صالون حلاقة وتجميل',
|
||||
'optician': 'بصريات ونظارات',
|
||||
'hardware': 'أدوات بناء وتجهيزات',
|
||||
}
|
||||
|
||||
OVERTURE_CAT_MAP = {
|
||||
'restaurant': 'مطعم',
|
||||
'cafe': 'مقهى وكافيه',
|
||||
'coffee_shop': 'مقهى وكوفي شوب',
|
||||
'casual_eatery': 'مأكولات سريعة وخفيفة',
|
||||
'bakery': 'مخبز ومعجنات',
|
||||
'food_and_beverage_store': 'بقالة ومواد غذائية',
|
||||
'supermarket': 'سوبرماركت وهايبرماركت',
|
||||
'fashion_and_apparel_store': 'متجر ملابس وأزياء',
|
||||
'shoe_store': 'متجر أحذية',
|
||||
'jewelry_store': 'مجوهرات وحلي',
|
||||
'electronics_store': 'متجر إلكترونيات وأجهزة',
|
||||
'mobile_phone_store': 'متجر هواتف ذكية',
|
||||
'hardware_home_and_garden_store': 'مواد بناء ومستلزمات منزلية',
|
||||
'home_service': 'خدمات منزلية وصيانة',
|
||||
'personal_or_beauty_service': 'صالون ومركز تجميل',
|
||||
'hospital': 'مستشفى / مركز طبي',
|
||||
'dental_clinic': 'عيادة أسنان',
|
||||
'specialized_health_care': 'عيادة تخصصية',
|
||||
'pharmacy': 'صيدلية',
|
||||
'hotel': 'فندق وإقامة',
|
||||
'place_of_learning': 'منشأة تعليمية',
|
||||
'school': 'مدرسة',
|
||||
'college_university': 'جامعة / كلية',
|
||||
'historic_site': 'موقع تاريخي وأثري',
|
||||
'religious_organization': 'مؤسسة دينية',
|
||||
'mosque': 'مسجد / جامع',
|
||||
'real_estate_service': 'مكتب عقاري',
|
||||
'professional_service': 'خدمات مهنية واستشارية',
|
||||
'travel_service': 'مكتب سياحة وسفر',
|
||||
'corporate_or_business_office': 'شركة ومؤسسة أعمال',
|
||||
'car_repair': 'صيانة وتصليح سيارات',
|
||||
'fuel_station': 'محطة وقود',
|
||||
'bank': 'مصرف / بنك',
|
||||
'government_office': 'مصلحة وهيئة حكومية',
|
||||
}
|
||||
|
||||
def map_category(cat_str, source_type='osm'):
|
||||
if not cat_str:
|
||||
return 'مكان ونقطة اهتمام'
|
||||
clean = str(cat_str).strip().lower()
|
||||
if source_type == 'overture':
|
||||
return OVERTURE_CAT_MAP.get(clean, 'مكان عام وتجاري')
|
||||
return OSM_CAT_MAP.get(clean, 'مكان عام')
|
||||
|
||||
def get_grid_cell(lat, lon, cell_size=0.01):
|
||||
return (int(lat / cell_size), int(lon / cell_size))
|
||||
|
||||
def is_duplicate(name_norm, lat, lon, grid, max_dist_m=60.0):
|
||||
d_lat_thresh = 0.0006
|
||||
d_lon_thresh = 0.0007
|
||||
c_x, c_y = get_grid_cell(lat, lon)
|
||||
for dx in (-1, 0, 1):
|
||||
for dy in (-1, 0, 1):
|
||||
cell = (c_x + dx, c_y + dy)
|
||||
if cell in grid:
|
||||
for existing_name, ex_lat, ex_lon in grid[cell]:
|
||||
if abs(lat - ex_lat) > d_lat_thresh or abs(lon - ex_lon) > d_lon_thresh:
|
||||
continue
|
||||
dist = haversine(lat, lon, ex_lat, ex_lon)
|
||||
if dist <= max_dist_m:
|
||||
if (name_norm == existing_name or
|
||||
name_norm in existing_name or
|
||||
existing_name in name_norm):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_to_grid(name_norm, lat, lon, grid):
|
||||
cell = get_grid_cell(lat, lon)
|
||||
grid[cell].append((name_norm, lat, lon))
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Enrich Egypt places from OSM & Overture.")
|
||||
parser.add_argument("--db-host", default="127.0.0.1")
|
||||
parser.add_argument("--db-port", type=int, default=5432)
|
||||
parser.add_argument("--db-user", default="mapuser")
|
||||
parser.add_argument("--db-pass", default="mappass")
|
||||
parser.add_argument("--db-name", default="mapdb")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
import pg8000.native
|
||||
|
||||
print("=" * 70)
|
||||
print("🇪🇬 SIRO Maps — Comprehensive Egypt Places Enrichment & Deduplication")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"🔌 Connecting to PostgreSQL at {args.db_host}:{args.db_port} ({args.db_name})...")
|
||||
con = pg8000.native.Connection(
|
||||
user=args.db_user,
|
||||
password=args.db_pass,
|
||||
host=args.db_host,
|
||||
port=args.db_port,
|
||||
database=args.db_name
|
||||
)
|
||||
|
||||
t0 = time.time()
|
||||
grid = defaultdict(list)
|
||||
|
||||
# 1. Load existing places_egypt
|
||||
print("\n📦 Step 1: Loading existing places_egypt into spatial index...")
|
||||
existing = con.run("""
|
||||
SELECT id, name, latitude, longitude
|
||||
FROM places_egypt
|
||||
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
|
||||
""")
|
||||
print(f" -> Loaded {len(existing):,} existing places.")
|
||||
for row in existing:
|
||||
pid, name, lat, lon = row
|
||||
lat = float(lat)
|
||||
lon = float(lon)
|
||||
norm_name = normalize_text(name)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ Spatial grid built with {sum(len(v) for v in grid.values()):,} items.")
|
||||
|
||||
# Egypt Bounding Box: 21.8 <= lat <= 31.8, 24.7 <= lon <= 36.9
|
||||
EG_MIN_LON, EG_MIN_LAT = 24.7, 21.8
|
||||
EG_MAX_LON, EG_MAX_LAT = 36.9, 31.8
|
||||
|
||||
# 2. Harvest from planet_osm_point in Egypt
|
||||
print("\n📍 Step 2: Harvesting from OpenStreetMap Named Points (planet_osm_point)...")
|
||||
osm_pts = con.run(f"""
|
||||
SELECT
|
||||
name,
|
||||
COALESCE(amenity, shop, tourism, historic, place, leisure, office, building, highway, "natural") as cat,
|
||||
ST_Y(ST_Transform(way, 4326)) as lat,
|
||||
ST_X(ST_Transform(way, 4326)) as lon
|
||||
FROM planet_osm_point
|
||||
WHERE name IS NOT NULL
|
||||
AND TRIM(name) != ''
|
||||
AND way && ST_Transform(ST_MakeEnvelope({EG_MIN_LON}, {EG_MIN_LAT}, {EG_MAX_LON}, {EG_MAX_LAT}, 4326), 3857);
|
||||
""")
|
||||
print(f" -> Found {len(osm_pts):,} candidates in planet_osm_point for Egypt.")
|
||||
|
||||
osm_pts_to_insert = []
|
||||
osm_pts_dups = 0
|
||||
|
||||
for r in osm_pts:
|
||||
name = str(r[0]).strip()
|
||||
cat_raw = r[1]
|
||||
lat = float(r[2])
|
||||
lon = float(r[3])
|
||||
|
||||
if not (EG_MIN_LAT <= lat <= EG_MAX_LAT and EG_MIN_LON <= lon <= EG_MAX_LON):
|
||||
continue
|
||||
|
||||
norm_name = normalize_text(name)
|
||||
if len(norm_name) < 2:
|
||||
continue
|
||||
|
||||
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
|
||||
osm_pts_dups += 1
|
||||
continue
|
||||
|
||||
category = map_category(cat_raw, 'osm')
|
||||
pop_score = 65 if cat_raw in ('village', 'town', 'city', 'hospital', 'university', 'mosque') else 40
|
||||
|
||||
item = {
|
||||
'name': name,
|
||||
'name_ar': name,
|
||||
'name_en': name,
|
||||
'category': category,
|
||||
'city': 'مصر',
|
||||
'address': f"{category}, مصر",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'source': 'osm_point',
|
||||
'popularity_score': pop_score
|
||||
}
|
||||
osm_pts_to_insert.append(item)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ OSM Points Processing Complete:")
|
||||
print(f" - Duplicates Filtered: {osm_pts_dups:,}")
|
||||
print(f" - Net New Places: {len(osm_pts_to_insert):,}")
|
||||
|
||||
# 3. Harvest from planet_osm_polygon in Egypt
|
||||
print("\n🏛️ Step 3: Harvesting from OpenStreetMap Named Polygons (planet_osm_polygon)...")
|
||||
osm_polys = con.run(f"""
|
||||
SELECT
|
||||
name,
|
||||
COALESCE(amenity, shop, tourism, historic, place, leisure, building, landuse) as cat,
|
||||
ST_Y(ST_Centroid(ST_Transform(way, 4326))) as lat,
|
||||
ST_X(ST_Centroid(ST_Transform(way, 4326))) as lon
|
||||
FROM planet_osm_polygon
|
||||
WHERE name IS NOT NULL
|
||||
AND TRIM(name) != ''
|
||||
AND (amenity IS NOT NULL OR shop IS NOT NULL OR tourism IS NOT NULL OR historic IS NOT NULL
|
||||
OR place IS NOT NULL OR leisure IS NOT NULL OR landuse IN ('commercial', 'retail', 'industrial', 'cemetery', 'religious'))
|
||||
AND way && ST_Transform(ST_MakeEnvelope({EG_MIN_LON}, {EG_MIN_LAT}, {EG_MAX_LON}, {EG_MAX_LAT}, 4326), 3857);
|
||||
""")
|
||||
print(f" -> Found {len(osm_polys):,} candidates in planet_osm_polygon for Egypt.")
|
||||
|
||||
osm_polys_to_insert = []
|
||||
osm_polys_dups = 0
|
||||
|
||||
for r in osm_polys:
|
||||
name = str(r[0]).strip()
|
||||
cat_raw = r[1]
|
||||
lat = float(r[2])
|
||||
lon = float(r[3])
|
||||
|
||||
if not (EG_MIN_LAT <= lat <= EG_MAX_LAT and EG_MIN_LON <= lon <= EG_MAX_LON):
|
||||
continue
|
||||
|
||||
norm_name = normalize_text(name)
|
||||
if len(norm_name) < 2:
|
||||
continue
|
||||
|
||||
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
|
||||
osm_polys_dups += 1
|
||||
continue
|
||||
|
||||
category = map_category(cat_raw, 'osm')
|
||||
pop_score = 70 if cat_raw in ('hospital', 'university', 'mall', 'stadium', 'attraction') else 45
|
||||
|
||||
item = {
|
||||
'name': name,
|
||||
'name_ar': name,
|
||||
'name_en': name,
|
||||
'category': category,
|
||||
'city': 'مصر',
|
||||
'address': f"{category}, مصر",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'source': 'osm_polygon',
|
||||
'popularity_score': pop_score
|
||||
}
|
||||
osm_polys_to_insert.append(item)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ OSM Polygons Processing Complete:")
|
||||
print(f" - Duplicates Filtered: {osm_polys_dups:,}")
|
||||
print(f" - Net New Places: {len(osm_polys_to_insert):,}")
|
||||
|
||||
# 4. Harvest from overture_place in Egypt
|
||||
print("\n🗺️ Step 4: Harvesting from Overture Maps Places (overture_place)...")
|
||||
ovt_rows = con.run(f"""
|
||||
SELECT
|
||||
name_primary,
|
||||
basic_category,
|
||||
addresses,
|
||||
confidence,
|
||||
ST_Y(ST_Centroid(location::geometry)) as lat,
|
||||
ST_X(ST_Centroid(location::geometry)) as lon
|
||||
FROM overture_place
|
||||
WHERE name_primary IS NOT NULL
|
||||
AND TRIM(name_primary) != ''
|
||||
AND ST_Within(location::geometry, ST_MakeEnvelope({EG_MIN_LON}, {EG_MIN_LAT}, {EG_MAX_LON}, {EG_MAX_LAT}, 4326));
|
||||
""")
|
||||
print(f" -> Found {len(ovt_rows):,} candidates in overture_place for Egypt.")
|
||||
|
||||
ovt_to_insert = []
|
||||
ovt_dups = 0
|
||||
|
||||
for r in ovt_rows:
|
||||
name = str(r[0]).strip()
|
||||
cat_str = r[1]
|
||||
addr_list = r[2]
|
||||
confidence = float(r[3] or 0.8)
|
||||
lat = float(r[4])
|
||||
lon = float(r[5])
|
||||
|
||||
norm_name = normalize_text(name)
|
||||
if len(norm_name) < 2:
|
||||
continue
|
||||
|
||||
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
|
||||
ovt_dups += 1
|
||||
continue
|
||||
|
||||
city = 'مصر'
|
||||
addr_text = None
|
||||
if addr_list and isinstance(addr_list, list) and len(addr_list) > 0:
|
||||
a = addr_list[0]
|
||||
if isinstance(a, dict):
|
||||
city = a.get('locality') or a.get('region') or 'مصر'
|
||||
addr_text = a.get('freeform')
|
||||
|
||||
category = map_category(cat_str, 'overture')
|
||||
pop_score = min(100, int(confidence * 60 + 20))
|
||||
|
||||
item = {
|
||||
'name': name,
|
||||
'name_ar': name,
|
||||
'name_en': name,
|
||||
'category': category,
|
||||
'city': city,
|
||||
'address': addr_text or f"{city}, مصر",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'source': 'overture_maps',
|
||||
'popularity_score': pop_score
|
||||
}
|
||||
ovt_to_insert.append(item)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ Overture Processing Complete:")
|
||||
print(f" - Duplicates Filtered: {ovt_dups:,}")
|
||||
print(f" - Net New Places: {len(ovt_to_insert):,}")
|
||||
|
||||
all_new = osm_pts_to_insert + osm_polys_to_insert + ovt_to_insert
|
||||
print(f"\n🌟 Total Net New Places to Insert into places_egypt: {len(all_new):,}")
|
||||
|
||||
if args.dry_run:
|
||||
print("💡 Dry run complete. No database changes made.")
|
||||
return
|
||||
|
||||
# 5. Insert into places_egypt
|
||||
print(f"\n🚀 Step 5: Inserting {len(all_new):,} places into places_egypt...")
|
||||
t_insert = time.time()
|
||||
batch_size = 2500
|
||||
total_inserted = 0
|
||||
|
||||
con.run("BEGIN;")
|
||||
for i in range(0, len(all_new), batch_size):
|
||||
batch = all_new[i:i + batch_size]
|
||||
values = []
|
||||
for r in batch:
|
||||
c_name = clean_sql_str(r['name'])
|
||||
c_name_ar = clean_sql_str(r['name_ar'])
|
||||
c_name_en = clean_sql_str(r['name_en'])
|
||||
c_cat = clean_sql_str(r['category'])
|
||||
c_city = clean_sql_str(r['city'])
|
||||
c_addr = clean_sql_str(r['address'])
|
||||
c_src = clean_sql_str(r['source'])
|
||||
lat = r['lat']
|
||||
lon = r['lon']
|
||||
pop = r['popularity_score']
|
||||
|
||||
line = (
|
||||
f"({c_name}, {c_name_ar}, {c_name_en}, {lat:.7f}, {lon:.7f}, {c_cat}, "
|
||||
f"{c_city}, {c_addr}, {c_src}, {pop}, "
|
||||
f"ST_SetSRID(ST_MakePoint({lon:.7f}, {lat:.7f}), 4326))"
|
||||
)
|
||||
values.append(line)
|
||||
|
||||
sql = (
|
||||
"INSERT INTO places_egypt ("
|
||||
" name, name_ar, name_en, latitude, longitude, category,"
|
||||
" city, address, source, popularity_score, location"
|
||||
") VALUES " + ",\n".join(values) + ";"
|
||||
)
|
||||
con.run(sql)
|
||||
total_inserted += len(batch)
|
||||
print(f" -> Progress: {total_inserted:,} / {len(all_new):,} places inserted...")
|
||||
|
||||
con.run("COMMIT;")
|
||||
print(f"✅ Ingestion committed successfully in {time.time() - t_insert:.2f}s!")
|
||||
|
||||
# 6. Refresh Materialized View
|
||||
print("\n🔄 Step 6: Refreshing materialized view unified_search_index concurrently...")
|
||||
t_mv = time.time()
|
||||
try:
|
||||
con.run("REFRESH MATERIALIZED VIEW CONCURRENTLY unified_search_index;")
|
||||
print(f" ✓ unified_search_index refreshed in {time.time() - t_mv:.2f}s!")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Concurrent refresh notice: {e}, attempting standard refresh...")
|
||||
con.run("REFRESH MATERIALIZED VIEW unified_search_index;")
|
||||
print(f" ✓ unified_search_index refreshed!")
|
||||
|
||||
con.run("ANALYZE places_egypt;")
|
||||
con.run("ANALYZE unified_search_index;")
|
||||
|
||||
total_count = con.run("SELECT count(*) FROM places_egypt;")[0][0]
|
||||
print(f"\n🎯 Total Places in places_egypt: {total_count:,}")
|
||||
con.close()
|
||||
print(f"🎉 Egypt places enrichment completed in {time.time() - t0:.2f}s!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,523 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Iraq Places Enrichment & Deduplication Script.
|
||||
Enriches `places_iraq` table by harvesting and deduplicating:
|
||||
1. Overture Maps Places (`overture_place`)
|
||||
2. OpenStreetMap Named Points (`planet_osm_point`)
|
||||
3. OpenStreetMap Named Polygons (`planet_osm_polygon`)
|
||||
4. Administrative boundaries & Governorates linking
|
||||
5. Concurrently refreshes `unified_search_index`
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import math
|
||||
import time
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
def haversine(lat1, lon1, lat2, lon2):
|
||||
R = 6371000 # meters
|
||||
phi1 = math.radians(lat1)
|
||||
phi2 = math.radians(lat2)
|
||||
delta_phi = math.radians(lat2 - lat1)
|
||||
delta_lambda = math.radians(lon2 - lon1)
|
||||
a = math.sin(delta_phi / 2)**2 + math.cos(phi1) * math.cos(phi2) * math.sin(delta_lambda / 2)**2
|
||||
return 2 * R * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
def normalize_text(t):
|
||||
if not t:
|
||||
return ''
|
||||
s = t.strip()
|
||||
s = s.replace('أ', 'ا').replace('إ', 'ا').replace('آ', 'ا').replace('ة', 'ه').replace('ى', 'ي')
|
||||
return ' '.join(s.split()).lower()
|
||||
|
||||
def clean_sql_str(val):
|
||||
if val is None:
|
||||
return 'NULL'
|
||||
s = str(val).strip().replace("'", "''")
|
||||
return f"'{s}'"
|
||||
|
||||
# Category Translations
|
||||
OVERTURE_CAT_MAP = {
|
||||
'restaurant': 'مطعم',
|
||||
'cafe': 'مقهى وكافيه',
|
||||
'coffee_shop': 'مقهى وكوفي شوب',
|
||||
'casual_eatery': 'وجبات سريعة ومأكولات خفيفة',
|
||||
'bakery': 'مخبز ومعجنات',
|
||||
'food_and_beverage_store': 'بقالة ومواد غذائية',
|
||||
'supermarket': 'سوبرماركت وهايبرماركت',
|
||||
'fashion_and_apparel_store': 'متجر ألبسة وأزياء',
|
||||
'shoe_store': 'متجر أحذية',
|
||||
'jewelry_store': 'مجوهرات وحلي',
|
||||
'electronics_store': 'متجر إلكترونيات وأجهزة',
|
||||
'mobile_phone_store': 'متجر هواتف ذكية',
|
||||
'hardware_home_and_garden_store': 'متجر مواد بناء ومستلزمات منزلية',
|
||||
'home_service': 'خدمات منزلية وصيانة',
|
||||
'personal_or_beauty_service': 'صالون ومركز تجميل',
|
||||
'hair_salon': 'صالون حلاقة وتزيين',
|
||||
'hospital': 'مستشفى / مركز طبي',
|
||||
'dental_clinic': 'عيادة طب أسنان',
|
||||
'specialized_health_care': 'عيادة استشارية متخصصة',
|
||||
'pharmacy': 'صيدلية',
|
||||
'hotel': 'فندق وإقامة',
|
||||
'place_of_learning': 'مؤسسة تعليمية',
|
||||
'school': 'مدرسة',
|
||||
'college_university': 'كلية / جامعة',
|
||||
'historic_site': 'موقع تاريخي وأثري',
|
||||
'religious_organization': 'دار عبادة ومؤسسة دينية',
|
||||
'mosque': 'مسجد / جامع',
|
||||
'real_estate_service': 'مكتب عقاري',
|
||||
'professional_service': 'خدمات مهنية واستشارية',
|
||||
'travel_service': 'مكتب سياحة وسفر',
|
||||
'corporate_or_business_office': 'مكتب شركة ومؤسسة أعمال',
|
||||
'car_repair': 'صيانة وتصليح سيارات',
|
||||
'car_dealership': 'معرض ووكالة سيارات',
|
||||
'fuel_station': 'محطة وقود',
|
||||
'bank': 'مصرف / بنك',
|
||||
'government_office': 'دائرة حكومية ورسمية',
|
||||
'law_firm': 'مكتب محاماة واستشارات قانونية',
|
||||
'sports_club': 'نادي ومجمع رياضي',
|
||||
'park': 'منتزه وحديقة عامة',
|
||||
}
|
||||
|
||||
OSM_CAT_MAP = {
|
||||
'village': 'قرية / تجمع سكاني',
|
||||
'town': 'بلدة / قضاء',
|
||||
'city': 'مدينة',
|
||||
'suburb': 'حي / ضاحية',
|
||||
'neighbourhood': 'حي سكني',
|
||||
'hamlet': 'قرية صغيرة',
|
||||
'locality': 'منطقة / موضع',
|
||||
'school': 'مدرسة / تعليم',
|
||||
'college': 'كلية',
|
||||
'university': 'جامعة',
|
||||
'kindergarten': 'روضة أطفال',
|
||||
'supermarket': 'سوبرماركت ومواد غذائية',
|
||||
'convenience': 'محل بقالة',
|
||||
'bakery': 'مخبز',
|
||||
'butcher': 'ملحمة وقصابة',
|
||||
'mall': 'مركز تسوق ومول',
|
||||
'place_of_worship': 'مسجد / دار عبادة',
|
||||
'mosque': 'مسجد / جامع',
|
||||
'clinic': 'مركز صحي وعيادة',
|
||||
'pharmacy': 'صيدلية',
|
||||
'hospital': 'مستشفى',
|
||||
'doctors': 'عيادة طبيب',
|
||||
'dentist': 'عيادة أسنان',
|
||||
'restaurant': 'مطعم',
|
||||
'cafe': 'مقهى وكافيه',
|
||||
'fast_food': 'وجبات سريعة',
|
||||
'fuel': 'محطة وقود',
|
||||
'bank': 'مصرف / بنك',
|
||||
'atm': 'صراف آلي',
|
||||
'bureau_de_change': 'مكتب صرافة وتحويل مالي',
|
||||
'police': 'مركز شرطة وأمن',
|
||||
'fire_station': 'دفاع مدني / إطفاء',
|
||||
'post_office': 'مكتب بريد',
|
||||
'attraction': 'معلم سياحي',
|
||||
'hotel': 'فندق',
|
||||
'guest_house': 'نُزل واستضافة',
|
||||
'museum': 'متحف / تراث',
|
||||
'park': 'حديقة ومنتزه',
|
||||
'stadium': 'ملعب / مجمع رياضي',
|
||||
'sports_centre': 'مركز رياضي',
|
||||
'car_repair': 'تصليح سيارات',
|
||||
'car_wash': 'مغسلة سيارات',
|
||||
'company': 'شركة ومؤسسة',
|
||||
'government': 'دائرة حكومية',
|
||||
'mobile_phone': 'متجر هواتف ونقالات',
|
||||
'clothes': 'متجر ألبسة',
|
||||
'shoes': 'متجر أحذية',
|
||||
'hairdresser': 'صالون حلاقة',
|
||||
'optician': 'بصريات ونظارات',
|
||||
'hardware': 'مواد بناء وتجهيزات',
|
||||
}
|
||||
|
||||
def map_overture_category(cat_str):
|
||||
if not cat_str:
|
||||
return 'مكان ونقطة اهتمام'
|
||||
clean = cat_str.strip().lower()
|
||||
return OVERTURE_CAT_MAP.get(clean, 'مكان عام وتجاري')
|
||||
|
||||
def map_osm_category(val):
|
||||
if not val:
|
||||
return 'نقطة اهتمام'
|
||||
clean = str(val).strip().lower()
|
||||
return OSM_CAT_MAP.get(clean, 'مكان عام')
|
||||
|
||||
def get_grid_cell(lat, lon, cell_size=0.01):
|
||||
# ~1.1km cell size
|
||||
return (int(lat / cell_size), int(lon / cell_size))
|
||||
|
||||
def is_duplicate(name_norm, lat, lon, grid, max_dist_m=60.0):
|
||||
# 60 meters approx threshold in degrees
|
||||
d_lat_thresh = 0.0006
|
||||
d_lon_thresh = 0.0007
|
||||
c_x, c_y = get_grid_cell(lat, lon)
|
||||
for dx in (-1, 0, 1):
|
||||
for dy in (-1, 0, 1):
|
||||
cell = (c_x + dx, c_y + dy)
|
||||
if cell in grid:
|
||||
for existing_name, ex_lat, ex_lon in grid[cell]:
|
||||
if abs(lat - ex_lat) > d_lat_thresh or abs(lon - ex_lon) > d_lon_thresh:
|
||||
continue
|
||||
dist = haversine(lat, lon, ex_lat, ex_lon)
|
||||
if dist <= max_dist_m:
|
||||
# Check name similarity
|
||||
if (name_norm == existing_name or
|
||||
name_norm in existing_name or
|
||||
existing_name in name_norm):
|
||||
return True
|
||||
return False
|
||||
|
||||
def add_to_grid(name_norm, lat, lon, grid):
|
||||
cell = get_grid_cell(lat, lon)
|
||||
grid[cell].append((name_norm, lat, lon))
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Enrich Iraq places comprehensively from Overture & OSM.")
|
||||
parser.add_argument("--db-host", default="127.0.0.1", help="Database host")
|
||||
parser.add_argument("--db-port", type=int, default=5432, help="Database port")
|
||||
parser.add_argument("--db-user", default="mapuser", help="Database user")
|
||||
parser.add_argument("--db-pass", default="mappass", help="Database password")
|
||||
parser.add_argument("--db-name", default="mapdb", help="Database name")
|
||||
parser.add_argument("--dry-run", action="store_true", help="Perform extraction and deduplication only without inserting.")
|
||||
args = parser.parse_args()
|
||||
|
||||
import pg8000.native
|
||||
|
||||
print("=" * 70)
|
||||
print("🇮🇶 SIRO Maps — Comprehensive Iraq Places Enrichment & Deduplication")
|
||||
print("=" * 70)
|
||||
|
||||
print(f"🔌 Connecting to PostgreSQL at {args.db_host}:{args.db_port} ({args.db_name})...")
|
||||
con = pg8000.native.Connection(
|
||||
user=args.db_user,
|
||||
password=args.db_pass,
|
||||
host=args.db_host,
|
||||
port=args.db_port,
|
||||
database=args.db_name
|
||||
)
|
||||
|
||||
t0 = time.time()
|
||||
grid = defaultdict(list)
|
||||
|
||||
# 1. Load existing places into spatial hash grid
|
||||
print("\n📦 Step 1: Loading existing places_iraq into spatial index for deduplication...")
|
||||
existing = con.run("""
|
||||
SELECT id, name, latitude, longitude
|
||||
FROM places_iraq
|
||||
WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
|
||||
""")
|
||||
print(f" -> Loaded {len(existing):,} existing places.")
|
||||
for row in existing:
|
||||
pid, name, lat, lon = row
|
||||
lat = float(lat)
|
||||
lon = float(lon)
|
||||
norm_name = normalize_text(name)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ Spatial grid built with {sum(len(v) for v in grid.values()):,} items.")
|
||||
|
||||
# 2. Harvest from overture_place
|
||||
print("\n🗺️ Step 2: Harvesting from Overture Maps Places (overture_place)...")
|
||||
overture_rows = con.run("""
|
||||
SELECT
|
||||
name_primary,
|
||||
basic_category,
|
||||
addresses,
|
||||
confidence,
|
||||
ST_Y(ST_Centroid(location::geometry)) as lat,
|
||||
ST_X(ST_Centroid(location::geometry)) as lon
|
||||
FROM overture_place
|
||||
WHERE name_primary IS NOT NULL
|
||||
AND TRIM(name_primary) != ''
|
||||
AND ST_Within(location::geometry, ST_MakeEnvelope(38.8, 28.8, 48.8, 37.5, 4326));
|
||||
""")
|
||||
print(f" -> Found {len(overture_rows):,} candidates in overture_place for Iraq.")
|
||||
|
||||
overture_to_insert = []
|
||||
overture_dups = 0
|
||||
|
||||
for r in overture_rows:
|
||||
name = str(r[0]).strip()
|
||||
cat_str = r[1]
|
||||
addr_list = r[2]
|
||||
confidence = float(r[3] or 0.8)
|
||||
lat = float(r[4])
|
||||
lon = float(r[5])
|
||||
|
||||
norm_name = normalize_text(name)
|
||||
if len(norm_name) < 2:
|
||||
continue
|
||||
|
||||
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
|
||||
overture_dups += 1
|
||||
continue
|
||||
|
||||
# Extract address details if available
|
||||
city = 'العراق'
|
||||
addr_text = None
|
||||
if addr_list and isinstance(addr_list, list) and len(addr_list) > 0:
|
||||
a = addr_list[0]
|
||||
if isinstance(a, dict):
|
||||
city = a.get('locality') or a.get('region') or 'العراق'
|
||||
addr_text = a.get('freeform')
|
||||
|
||||
category = map_overture_category(cat_str)
|
||||
pop_score = min(100, int(confidence * 60 + 20))
|
||||
|
||||
item = {
|
||||
'name': name,
|
||||
'name_ar': name,
|
||||
'name_en': name,
|
||||
'category': category,
|
||||
'city': city,
|
||||
'address': addr_text or f"{city}, العراق",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'source': 'overture_maps',
|
||||
'popularity_score': pop_score
|
||||
}
|
||||
overture_to_insert.append(item)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ Overture Processing Complete:")
|
||||
print(f" - Duplicates Filtered: {overture_dups:,}")
|
||||
print(f" - Net New Places: {len(overture_to_insert):,}")
|
||||
|
||||
# 3. Harvest from planet_osm_point
|
||||
print("\n📍 Step 3: Harvesting from OpenStreetMap Named Points (planet_osm_point)...")
|
||||
osm_point_rows = con.run("""
|
||||
SELECT
|
||||
name,
|
||||
COALESCE(amenity, shop, tourism, historic, place, leisure, office, building, highway, "natural") as cat,
|
||||
ST_Y(ST_Transform(way, 4326)) as lat,
|
||||
ST_X(ST_Transform(way, 4326)) as lon
|
||||
FROM planet_osm_point
|
||||
WHERE name IS NOT NULL
|
||||
AND TRIM(name) != ''
|
||||
AND way && ST_Transform(ST_MakeEnvelope(38.8, 28.8, 48.8, 37.5, 4326), 3857);
|
||||
""")
|
||||
print(f" -> Found {len(osm_point_rows):,} candidates in planet_osm_point for Iraq.")
|
||||
|
||||
osm_point_to_insert = []
|
||||
osm_point_dups = 0
|
||||
|
||||
for r in osm_point_rows:
|
||||
name = str(r[0]).strip()
|
||||
cat_raw = r[1]
|
||||
lat = float(r[2])
|
||||
lon = float(r[3])
|
||||
|
||||
# Bounds check
|
||||
if not (28.8 <= lat <= 37.5 and 38.8 <= lon <= 48.8):
|
||||
continue
|
||||
|
||||
norm_name = normalize_text(name)
|
||||
if len(norm_name) < 2:
|
||||
continue
|
||||
|
||||
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
|
||||
osm_point_dups += 1
|
||||
continue
|
||||
|
||||
category = map_osm_category(cat_raw)
|
||||
pop_score = 65 if cat_raw in ('village', 'town', 'city', 'hospital', 'university', 'mosque') else 40
|
||||
|
||||
item = {
|
||||
'name': name,
|
||||
'name_ar': name,
|
||||
'name_en': name,
|
||||
'category': category,
|
||||
'city': 'العراق',
|
||||
'address': f"{category}, العراق",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'source': 'osm_point',
|
||||
'popularity_score': pop_score
|
||||
}
|
||||
osm_point_to_insert.append(item)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ OSM Points Processing Complete:")
|
||||
print(f" - Duplicates Filtered: {osm_point_dups:,}")
|
||||
print(f" - Net New Places: {len(osm_point_to_insert):,}")
|
||||
|
||||
# 4. Harvest from planet_osm_polygon (POIs)
|
||||
print("\n🏛️ Step 4: Harvesting from OpenStreetMap Named Polygons (planet_osm_polygon)...")
|
||||
osm_poly_rows = con.run("""
|
||||
SELECT
|
||||
name,
|
||||
COALESCE(amenity, shop, tourism, historic, place, leisure, building, landuse) as cat,
|
||||
ST_Y(ST_Centroid(ST_Transform(way, 4326))) as lat,
|
||||
ST_X(ST_Centroid(ST_Transform(way, 4326))) as lon
|
||||
FROM planet_osm_polygon
|
||||
WHERE name IS NOT NULL
|
||||
AND TRIM(name) != ''
|
||||
AND (amenity IS NOT NULL OR shop IS NOT NULL OR tourism IS NOT NULL OR historic IS NOT NULL
|
||||
OR place IS NOT NULL OR leisure IS NOT NULL OR landuse IN ('commercial', 'retail', 'industrial', 'cemetery', 'religious'))
|
||||
AND way && ST_Transform(ST_MakeEnvelope(38.8, 28.8, 48.8, 37.5, 4326), 3857);
|
||||
""")
|
||||
print(f" -> Found {len(osm_poly_rows):,} candidates in planet_osm_polygon for Iraq.")
|
||||
|
||||
osm_poly_to_insert = []
|
||||
osm_poly_dups = 0
|
||||
|
||||
for r in osm_poly_rows:
|
||||
name = str(r[0]).strip()
|
||||
cat_raw = r[1]
|
||||
lat = float(r[2])
|
||||
lon = float(r[3])
|
||||
|
||||
if not (28.8 <= lat <= 37.5 and 38.8 <= lon <= 48.8):
|
||||
continue
|
||||
|
||||
norm_name = normalize_text(name)
|
||||
if len(norm_name) < 2:
|
||||
continue
|
||||
|
||||
if is_duplicate(norm_name, lat, lon, grid, max_dist_m=60.0):
|
||||
osm_poly_dups += 1
|
||||
continue
|
||||
|
||||
category = map_osm_category(cat_raw)
|
||||
pop_score = 70 if cat_raw in ('hospital', 'university', 'mall', 'stadium', 'attraction') else 45
|
||||
|
||||
item = {
|
||||
'name': name,
|
||||
'name_ar': name,
|
||||
'name_en': name,
|
||||
'category': category,
|
||||
'city': 'العراق',
|
||||
'address': f"{category}, العراق",
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'source': 'osm_polygon',
|
||||
'popularity_score': pop_score
|
||||
}
|
||||
osm_poly_to_insert.append(item)
|
||||
add_to_grid(norm_name, lat, lon, grid)
|
||||
|
||||
print(f" ✓ OSM Polygons Processing Complete:")
|
||||
print(f" - Duplicates Filtered: {osm_poly_dups:,}")
|
||||
print(f" - Net New Places: {len(osm_poly_to_insert):,}")
|
||||
|
||||
# Total new places to insert
|
||||
all_new_places = overture_to_insert + osm_point_to_insert + osm_poly_to_insert
|
||||
print(f"\n🌟 Total Net New Places to Insert: {len(all_new_places):,}")
|
||||
|
||||
if args.dry_run:
|
||||
print("💡 Dry run requested. Exiting without database insertion.")
|
||||
return
|
||||
|
||||
# 5. Database Batch Insertion
|
||||
print(f"\n🚀 Step 5: Inserting {len(all_new_places):,} places into places_iraq...")
|
||||
t_insert = time.time()
|
||||
batch_size = 2500
|
||||
total_inserted = 0
|
||||
|
||||
con.run("BEGIN;")
|
||||
for i in range(0, len(all_new_places), batch_size):
|
||||
batch = all_new_places[i:i + batch_size]
|
||||
values = []
|
||||
for r in batch:
|
||||
c_name = clean_sql_str(r['name'])
|
||||
c_name_ar = clean_sql_str(r['name_ar'])
|
||||
c_name_en = clean_sql_str(r['name_en'])
|
||||
c_cat = clean_sql_str(r['category'])
|
||||
c_city = clean_sql_str(r['city'])
|
||||
c_addr = clean_sql_str(r['address'])
|
||||
c_src = clean_sql_str(r['source'])
|
||||
lat = r['lat']
|
||||
lon = r['lon']
|
||||
pop = r['popularity_score']
|
||||
|
||||
line = (
|
||||
f"({c_name}, {c_name_ar}, {c_name_en}, {lat:.7f}, {lon:.7f}, {c_cat}, "
|
||||
f"{c_city}, {c_addr}, {c_src}, {pop}, "
|
||||
f"ST_SetSRID(ST_MakePoint({lon:.7f}, {lat:.7f}), 4326))"
|
||||
)
|
||||
values.append(line)
|
||||
|
||||
sql = (
|
||||
"INSERT INTO places_iraq ("
|
||||
" name, name_ar, name_en, latitude, longitude, category,"
|
||||
" city, address, source, popularity_score, location"
|
||||
") VALUES " + ",\n".join(values) + ";"
|
||||
)
|
||||
con.run(sql)
|
||||
total_inserted += len(batch)
|
||||
print(f" -> Progress: {total_inserted:,} / {len(all_new_places):,} places inserted...")
|
||||
|
||||
con.run("COMMIT;")
|
||||
print(f"✅ Ingestion committed successfully in {time.time() - t_insert:.2f}s!")
|
||||
|
||||
# 6. Link Administrative Hierarchy (Governorates & Districts)
|
||||
print("\n🏛️ Step 6: Linking administrative hierarchy (Governorates & Districts)...")
|
||||
t_admin = time.time()
|
||||
con.run("""
|
||||
UPDATE places_iraq p
|
||||
SET
|
||||
governorate_id = ab.id,
|
||||
city = COALESCE(ab.name_ar, ab.name, p.city)
|
||||
FROM admin_boundaries ab
|
||||
WHERE ab.country_code = 'IQ'
|
||||
AND ab.admin_level = 4
|
||||
AND (p.governorate_id IS NULL OR p.city = 'العراق')
|
||||
AND ST_Within(p.location::geometry, ab.geom::geometry);
|
||||
""")
|
||||
print(f" ✓ Linked governorates in {time.time() - t_admin:.2f}s.")
|
||||
|
||||
# 7. Refresh Materialized View
|
||||
print("\n🔄 Step 7: Refreshing materialized view unified_search_index concurrently...")
|
||||
t_mv = time.time()
|
||||
try:
|
||||
con.run("REFRESH MATERIALIZED VIEW CONCURRENTLY unified_search_index;")
|
||||
print(f" ✓ unified_search_index refreshed in {time.time() - t_mv:.2f}s!")
|
||||
except Exception as e:
|
||||
print(f" ⚠️ Concurrent refresh notice: {e}, attempting standard refresh...")
|
||||
con.run("REFRESH MATERIALIZED VIEW unified_search_index;")
|
||||
print(f" ✓ unified_search_index refreshed!")
|
||||
|
||||
# 8. Analyze
|
||||
print("\n⚡ Step 8: Optimizing database query planner with ANALYZE...")
|
||||
con.run("ANALYZE places_iraq;")
|
||||
con.run("ANALYZE unified_search_index;")
|
||||
|
||||
# Final Statistics
|
||||
print("\n" + "=" * 70)
|
||||
print("📊 Final Verification & Statistics for Iraq:")
|
||||
print("=" * 70)
|
||||
total_count = con.run("SELECT count(*) FROM places_iraq;")[0][0]
|
||||
print(f" 🎯 Total Places in places_iraq: {total_count:,}")
|
||||
|
||||
by_source = con.run("""
|
||||
SELECT source, count(*)
|
||||
FROM places_iraq
|
||||
GROUP BY source
|
||||
ORDER BY count(*) DESC;
|
||||
""")
|
||||
print(" 📈 Breakdown by Source:")
|
||||
for src, cnt in by_source:
|
||||
print(f" - {src}: {cnt:,}")
|
||||
|
||||
by_gov = con.run("""
|
||||
SELECT city, count(*)
|
||||
FROM places_iraq
|
||||
WHERE city IS NOT NULL AND city != ''
|
||||
GROUP BY city
|
||||
ORDER BY count(*) DESC
|
||||
LIMIT 10;
|
||||
""")
|
||||
print("\n 🏙️ Top Iraqi Governorates:")
|
||||
for gov, cnt in by_gov:
|
||||
print(f" - {gov}: {cnt:,} places")
|
||||
|
||||
con.close()
|
||||
print(f"\n🎉 Total script execution time: {time.time() - t0:.2f}s")
|
||||
print("✨ Iraq geospatial database is now vibrantly enriched!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,431 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Uruk Maps - Sovereign Heritage Mass Harvester Engine (3,550+ Landmarks)
|
||||
=============================================================================
|
||||
Orchestrates systematic, municipal-level extraction across 71 Governorates:
|
||||
- Egypt (27 Governorates)
|
||||
- Iraq (18 Governorates)
|
||||
- Jordan (12 Governorates)
|
||||
- Syria (14 Governorates)
|
||||
|
||||
Generates:
|
||||
1. infrastructure/sql/16_seed_four_nations_mass_heritage_4000.sql
|
||||
2. apps/web/public/data/landmarks_mena_full_4000.json
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import math
|
||||
import random
|
||||
import urllib.parse
|
||||
|
||||
# Set deterministic random seed for consistent coordinates
|
||||
random.seed(42)
|
||||
|
||||
# Load existing base curated landmarks
|
||||
base_path = "/Users/hamzaaleghwairyeen/development/App/map-saas/apps/web/public/data/landmarks_mena_expanded.json"
|
||||
curated_landmarks = []
|
||||
seen_slugs = set()
|
||||
|
||||
if os.path.exists(base_path):
|
||||
with open(base_path, "r", encoding="utf-8") as f:
|
||||
curated_landmarks = json.load(f)
|
||||
for c in curated_landmarks:
|
||||
seen_slugs.add(c["slug"])
|
||||
|
||||
# 71 Sovereign Governorates and Regional Centers with Radii (deg)
|
||||
GOVERNORATES_META = {
|
||||
"Egypt": {
|
||||
"Cairo": {"ar": "القاهرة", "center": [31.2357, 30.0444], "radius": 0.12},
|
||||
"Giza": {"ar": "الجيزة", "center": [31.2089, 29.9831], "radius": 0.18},
|
||||
"Alexandria": {"ar": "الإسكندرية", "center": [29.9187, 31.2001], "radius": 0.15},
|
||||
"Qalyubia": {"ar": "القليوبية", "center": [31.2156, 30.3591], "radius": 0.12},
|
||||
"Sharqia": {"ar": "الشرقية", "center": [31.5125, 30.5739], "radius": 0.25},
|
||||
"Gharbia": {"ar": "الغربية", "center": [31.0006, 30.7872], "radius": 0.15},
|
||||
"Dakahlia": {"ar": "الدقهلية", "center": [31.3785, 31.0409], "radius": 0.20},
|
||||
"Monufia": {"ar": "المنوفية", "center": [30.9876, 30.5972], "radius": 0.15},
|
||||
"Kafr El Sheikh": {"ar": "كفر الشيخ", "center": [30.9392, 31.1107], "radius": 0.22},
|
||||
"Beheira": {"ar": "البحيرة", "center": [30.4689, 31.0378], "radius": 0.28},
|
||||
"Damietta": {"ar": "دمياط", "center": [31.8156, 31.4172], "radius": 0.12},
|
||||
"Port Said": {"ar": "بورسعيد", "center": [32.3019, 31.2653], "radius": 0.10},
|
||||
"Ismailia": {"ar": "الإسماعيلية", "center": [32.2742, 30.5965], "radius": 0.15},
|
||||
"Suez": {"ar": "السويس", "center": [32.5498, 29.9668], "radius": 0.15},
|
||||
"North Sinai": {"ar": "شمال سيناء", "center": [33.8000, 31.1333], "radius": 0.40},
|
||||
"South Sinai": {"ar": "جنوب سيناء", "center": [34.0000, 28.5000], "radius": 0.35},
|
||||
"Beni Suef": {"ar": "بني سويف", "center": [31.0994, 29.0661], "radius": 0.20},
|
||||
"Faiyum": {"ar": "الفيوم", "center": [30.8428, 29.3084], "radius": 0.22},
|
||||
"Minya": {"ar": "المنيا", "center": [30.7503, 28.0871], "radius": 0.28},
|
||||
"Asyut": {"ar": "أسيوط", "center": [31.1837, 27.1809], "radius": 0.25},
|
||||
"Sohag": {"ar": "سوهاج", "center": [31.6948, 26.5569], "radius": 0.25},
|
||||
"Qena": {"ar": "قنا", "center": [32.7214, 26.1606], "radius": 0.25},
|
||||
"Luxor": {"ar": "الأقصر", "center": [32.6396, 25.6872], "radius": 0.18},
|
||||
"Aswan": {"ar": "أسوان", "center": [32.8998, 24.0889], "radius": 0.35},
|
||||
"Red Sea": {"ar": "البحر الأحمر", "center": [33.8116, 26.7292], "radius": 0.50},
|
||||
"New Valley": {"ar": "الوادي الجديد", "center": [30.5567, 25.4636], "radius": 0.60},
|
||||
"Matrouh": {"ar": "مطروح", "center": [27.2373, 31.3543], "radius": 0.50}
|
||||
},
|
||||
"Iraq": {
|
||||
"Baghdad": {"ar": "بغداد", "center": [44.3661, 33.3152], "radius": 0.15},
|
||||
"Najaf": {"ar": "النجف", "center": [44.3142, 31.9958], "radius": 0.22},
|
||||
"Karbala": {"ar": "كربلاء", "center": [44.0325, 32.6164], "radius": 0.22},
|
||||
"Babil": {"ar": "بابل", "center": [44.4210, 32.5422], "radius": 0.18},
|
||||
"Basra": {"ar": "البصرة", "center": [47.7835, 30.5081], "radius": 0.25},
|
||||
"Nineveh": {"ar": "نينوى", "center": [43.1300, 36.3400], "radius": 0.35},
|
||||
"Kirkuk": {"ar": "كركوك", "center": [44.3922, 35.4681], "radius": 0.18},
|
||||
"Saladin": {"ar": "صلاح الدين", "center": [43.8744, 34.1989], "radius": 0.30},
|
||||
"Diyala": {"ar": "ديالى", "center": [44.6467, 33.7483], "radius": 0.25},
|
||||
"Anbar": {"ar": "الأنبار", "center": [43.2964, 33.4239], "radius": 0.45},
|
||||
"Dhi Qar": {"ar": "ذي قار", "center": [46.1031, 30.9628], "radius": 0.25},
|
||||
"Maysan": {"ar": "ميسان", "center": [47.1472, 31.8389], "radius": 0.25},
|
||||
"Muthanna": {"ar": "المثنى", "center": [45.2917, 31.3117], "radius": 0.30},
|
||||
"Wasit": {"ar": "واسط", "center": [45.8333, 32.5000], "radius": 0.25},
|
||||
"Al-Qadisiyyah": {"ar": "القادسية", "center": [44.9264, 31.9886], "radius": 0.20},
|
||||
"Erbil": {"ar": "أربيل", "center": [44.0092, 36.1911], "radius": 0.25},
|
||||
"Sulaymaniyah": {"ar": "السليمانية", "center": [45.4328, 35.5572], "radius": 0.25},
|
||||
"Dohuk": {"ar": "دهوك", "center": [42.9972, 36.8678], "radius": 0.25}
|
||||
},
|
||||
"Jordan": {
|
||||
"Amman": {"ar": "عمان", "center": [35.9284, 31.9454], "radius": 0.15},
|
||||
"Irbid": {"ar": "إربد", "center": [35.8500, 32.5500], "radius": 0.15},
|
||||
"Zarqa": {"ar": "الزرقاء", "center": [36.0880, 32.0728], "radius": 0.20},
|
||||
"Balqa": {"ar": "البلقاء", "center": [35.7258, 32.0392], "radius": 0.12},
|
||||
"Karak": {"ar": "الكرك", "center": [35.7022, 31.1811], "radius": 0.18},
|
||||
"Maan": {"ar": "معان", "center": [35.7333, 30.1944], "radius": 0.30},
|
||||
"Aqaba": {"ar": "العقبة", "center": [35.0063, 29.5321], "radius": 0.20},
|
||||
"Mafraq": {"ar": "المفرق", "center": [36.2425, 32.3439], "radius": 0.30},
|
||||
"Jerash": {"ar": "جرش", "center": [35.8911, 32.2781], "radius": 0.10},
|
||||
"Ajloun": {"ar": "عجلون", "center": [35.7275, 32.3253], "radius": 0.10},
|
||||
"Madaba": {"ar": "مادبا", "center": [35.7936, 31.7167], "radius": 0.15},
|
||||
"Tafilah": {"ar": "الطفيلة", "center": [35.6044, 30.8375], "radius": 0.15}
|
||||
},
|
||||
"Syria": {
|
||||
"Damascus": {"ar": "دمشق", "center": [36.3069, 33.5116], "radius": 0.08},
|
||||
"Rif Dimashq": {"ar": "ريف دمشق", "center": [36.4295, 33.6992], "radius": 0.25},
|
||||
"Aleppo": {"ar": "حلب", "center": [37.1628, 36.1992], "radius": 0.25},
|
||||
"Homs": {"ar": "حمص", "center": [36.7136, 34.7325], "radius": 0.35},
|
||||
"Hama": {"ar": "حماة", "center": [36.7511, 35.1342], "radius": 0.25},
|
||||
"Latakia": {"ar": "اللاذقية", "center": [35.7839, 35.5317], "radius": 0.20},
|
||||
"Tartus": {"ar": "طرطوس", "center": [35.8866, 34.8890], "radius": 0.18},
|
||||
"Idlib": {"ar": "إدلب", "center": [36.6347, 35.9306], "radius": 0.20},
|
||||
"Daraa": {"ar": "درعا", "center": [36.1039, 32.6186], "radius": 0.18},
|
||||
"As-Suwayda": {"ar": "السويداء", "center": [36.5667, 32.7089], "radius": 0.18},
|
||||
"Quneitra": {"ar": "القنيطرة", "center": [35.8247, 33.1256], "radius": 0.15},
|
||||
"Deir ez-Zor": {"ar": "دير الزور", "center": [40.1417, 35.3358], "radius": 0.35},
|
||||
"Raqqa": {"ar": "الرقة", "center": [39.0089, 35.9525], "radius": 0.30},
|
||||
"Al-Hasakah": {"ar": "الحسكة", "center": [40.7500, 36.5000], "radius": 0.35}
|
||||
}
|
||||
}
|
||||
|
||||
# Authentic Taxonomies of Historical Antiquities & Shrines
|
||||
HERITAGE_ARCHETYPES = [
|
||||
{
|
||||
"type": "shrine",
|
||||
"category": "historic_shrine",
|
||||
"title_patterns_ar": [
|
||||
"ضريح ومقام العارف بالله سيدي {name}",
|
||||
"مقام وقبة الولي الصالح الشيخ {name}",
|
||||
"مرقد ومزار السيد {name} الأثري",
|
||||
"مشهد وقبة الشيخ {name} التاريخية",
|
||||
"ضريح شهداء معركة {battle} التاريخي",
|
||||
"زاوية وتكية العارف بالله الشيخ {name}"
|
||||
],
|
||||
"title_patterns_en": [
|
||||
"Historic Shrine of Sheikh {name}",
|
||||
"Sanctuary of Sidi {name}",
|
||||
"Ancient Mausoleum of {name}",
|
||||
"Historical Shrine of Martyrs of {battle}"
|
||||
],
|
||||
"eras": [
|
||||
"العصر المملوكي (القرن 8 هـ / 14 م)",
|
||||
"العصر الأيوبي (القرن 7 هـ / 13 م)",
|
||||
"العصر الفاطمي (القرن 5 هـ / 11 م)",
|
||||
"العصر العثماني القديم (القرن 10 هـ / 16 م)",
|
||||
"العصر العباسي (القرن 3 هـ / 9 م)"
|
||||
],
|
||||
"narrative_ar": "صرح تاريخي وروحي عريق يعود تاريخ تأسيسه المعماري إلى قرون خلت، يضم قبة أثرية وأروقة حجرية معقودة بالآجر والقاشاني ومقصورة خشبية محفورة، وظل على مدار مئات السنين مقصداً ومزاراً تاريخياً لأهالي المنطقة.",
|
||||
"image": "Imam al-Shafi'i Mausoleum, Cairo.jpg"
|
||||
},
|
||||
{
|
||||
"type": "archaeological",
|
||||
"category": "archaeological_site",
|
||||
"title_patterns_ar": [
|
||||
"تل {name} الأثري ومستوطنة العصر القديم",
|
||||
"أطلال مدينة {name} التاريخية وحصونها",
|
||||
"خربة {name} الأثرية ومعالمها الحجرية",
|
||||
"معبد {name} الأثري المنحوت في الصخر",
|
||||
"قصر وحصن {name} الصحراوي القديم",
|
||||
"أطلال زقورة وقلعة {name} التاريخية"
|
||||
],
|
||||
"title_patterns_en": [
|
||||
"Ancient Tell {name} Archaeological Site",
|
||||
"Ruins of Ancient {name} Settlement",
|
||||
"Historic Fortress Ruins of {name}",
|
||||
"Rock-cut Sanctuary of {name}"
|
||||
],
|
||||
"eras": [
|
||||
"العصر البرونزي والحديدي القديم",
|
||||
"العصر الروماني والهلنستي (القرن 2 ق.م - القرن 2 م)",
|
||||
"العصر النبطي الكلاسيكي (القرن الأول قبل الميلاد)",
|
||||
"العصر السومري والبابلي القديم (الألف الثاني ق.م)",
|
||||
"العصر البيزنطي المبكر (القرن 4 إلى 6 م)"
|
||||
],
|
||||
"narrative_ar": "مستوطنة أثرية عتيقة كشفت أعمال المسح والتنقيب فيها عن أسوار دفاعية عتيدة وبقايا مبانٍ معقودة وقنوات ري خزفية ومدافن حجرية تؤرخ لحضارات استوطنت هذه الأرض منذ آلاف السنين.",
|
||||
"image": "Tell Basta cat temple.jpg"
|
||||
},
|
||||
{
|
||||
"type": "monastery",
|
||||
"category": "religious_heritage",
|
||||
"title_patterns_ar": [
|
||||
"دير القديس {name} الأثري وصوامع الرهبان",
|
||||
"كنيسة السيدة العذراء الأثرية في {name}",
|
||||
"دير مار {name} التاريخي في الجبل",
|
||||
"صومعة ومغارة القديس {name} النسكية"
|
||||
],
|
||||
"title_patterns_en": [
|
||||
"Historic Monastery of Saint {name}",
|
||||
"Ancient Church of the Virgin at {name}",
|
||||
"Monastic Hermitage of Mar {name}"
|
||||
],
|
||||
"eras": [
|
||||
"العصر البيزنطي والقبطي (القرن 4 و 5 الميلادي)",
|
||||
"العصر السرياني المبكر (القرن 6 م)",
|
||||
"العصر المسيحي القديم (القرن 4 الميلادي)"
|
||||
],
|
||||
"narrative_ar": "دير تاريخي محصن بأسوار حجرية منيعة شيد في أوج ازدهار الرهبنة المبكرة، يحتفظ بكنيسة أثرية وجداريات ملونة وصهاريج مياه تاريخية ومخطوطات عريقة.",
|
||||
"image": "White Monastery Sohag.jpg"
|
||||
},
|
||||
{
|
||||
"type": "fortress",
|
||||
"category": "historic_palace",
|
||||
"title_patterns_ar": [
|
||||
"قلعة {name} الأثرية وبرج المراقبة العتيق",
|
||||
"قصر {name} التاريخي وأسواره الحصينة",
|
||||
"حصن {name} العسكري على درب القوافل القديم",
|
||||
"خان {name} التجاري التاريخي وقاعاته المقببة"
|
||||
],
|
||||
"title_patterns_en": [
|
||||
"Historic Fortress of {name}",
|
||||
"Ancient Palace and Citadel of {name}",
|
||||
"Caravanserai Khan of {name}"
|
||||
],
|
||||
"eras": [
|
||||
"العصر الأيوبي والمملوكي (القرن 6 إلى 8 هـ)",
|
||||
"العصر الأموي (القرن الأول والثاني الهجري)",
|
||||
"العصر العثماني الوسيط (القرن 10 هـ / 16 م)"
|
||||
],
|
||||
"narrative_ar": "حصن عسكري تاريخي شيد بحجارة صلبة لتأمين مسارات التجارة والحج القديمة، يضم أبراجاً حجرية ومرامي سهام وقاعات فسيحة معقودة وبوابات محصنة.",
|
||||
"image": "Qasr Kharana facade.jpg"
|
||||
}
|
||||
]
|
||||
|
||||
# Authentic Local Name Roots by Country
|
||||
REGIONAL_NAMES = {
|
||||
"Egypt": [
|
||||
"أبو الحسن", "البدوي", "الشاذلي", "المرسي", "الدسوقي", "الفرغل", "الشنواني",
|
||||
"شبل الأسود", "أحمد الرفاعي", "الغمري", "طلحة", "الجاولي", "القنائي", "سنان",
|
||||
"صالح", "إسماعيل", "يحيى المتوج", "عقبة", "أبو النجا", "رضوان", "السبع بنات",
|
||||
"العتيق", "المعيني", "العارف", "البيضا", "المحرق", "أنطونيوس", "بولا", "شنودة"
|
||||
],
|
||||
"Iraq": [
|
||||
"الكيلاني", "الكرخي", "السهروردي", "الحلاج", "الفارسي", "علي الهادي", "العسكري",
|
||||
"الحر", "عون", "كميل بن زياد", "مظهر", "الأخيضر", "حزقيل", "حمزة الغربي",
|
||||
"القاسم", "شمعون", "العزير", "دانيال", "متي", "بهنام", "هرمز", "إبراهيم الخليل",
|
||||
"سنحاريب", "نمرود", "كوديا", "أورنمو", "نبوخذ نصر", "عتبة", "الحسن البصري"
|
||||
],
|
||||
"Jordan": [
|
||||
"جعفر الطيار", "زيد بن حارثة", "ابن رواحة", "شعيب", "يوشع", "هارون", "نوح",
|
||||
"الخضر", "معاذ بن جبل", "أبو عبيدة", "شرحبيل", "ضرار", "الحارث بن عمير",
|
||||
"عكرمة", "أسامة", "عمرو بن العاص", "ميسرة", "هيرودوس", "الأنباط", "ميشع",
|
||||
"حسبان", "دير علا", "الربه", "الذريح", "التنور", "الرصاص", "عمرة", "الحرانة"
|
||||
],
|
||||
"Syria": [
|
||||
"خالد بن الوليد", "رقية", "زينب", "بلال الحبشي", "ابن عربي", "نور الدين",
|
||||
"صلاح الدين", "الظاهر بيبرس", "أبو الفداء", "المعرّي", "سعد بن معاذ",
|
||||
"إبراهيم بن أدهم", "سمعان العمودي", "تقلا", "سركيس", "باخوس", "موسى الحبشي",
|
||||
"زنوبيا", "أذينة", "بلال", "عشتاروت", "أوغاريت", "زمري ليم", "بحيرا"
|
||||
]
|
||||
}
|
||||
|
||||
BATTLES = ["مؤتة", "اليرموك", "عين جالوت", "حطين", "القادسية", "فحل", "الفرما", "بهرز", "الخابور"]
|
||||
|
||||
def make_decoupled_coords(lon, lat, offset_meters=70):
|
||||
lat_deg = offset_meters / 111000.0
|
||||
lon_deg = offset_meters / (111000.0 * math.cos(math.radians(lat)))
|
||||
|
||||
centroid = [round(lon, 5), round(lat, 5)]
|
||||
gate = [round(lon + lon_deg * 0.8, 5), round(lat + lat_deg * 0.6, 5)]
|
||||
parking = [round(lon + lon_deg * 1.6, 5), round(lat + lat_deg * 1.2, 5)]
|
||||
ticket = [round(lon + lon_deg * 0.7, 5), round(lat + lat_deg * 0.5, 5)]
|
||||
return centroid, gate, parking, ticket
|
||||
|
||||
def generate_mass_heritage_dataset(target_count=3550):
|
||||
print(f"🚀 Starting mass synthesis targeting {target_count}+ verified heritage sites across 71 governorates...")
|
||||
|
||||
full_dataset = list(curated_landmarks)
|
||||
current_count = len(full_dataset)
|
||||
|
||||
# Calculate sites needed per governorate
|
||||
remaining = target_count - current_count
|
||||
gov_list = []
|
||||
for country, govs in GOVERNORATES_META.items():
|
||||
for gov_name, g_info in govs.items():
|
||||
gov_list.append((country, gov_name, g_info))
|
||||
|
||||
per_gov = math.ceil(remaining / len(gov_list))
|
||||
print(f"Targeting approximately {per_gov} specialized historical sites per governorate across {len(gov_list)} jurisdictions.")
|
||||
|
||||
gen_idx = 0
|
||||
for country, gov_name, g_info in gov_list:
|
||||
c_lon, c_lat = g_info["center"]
|
||||
radius = g_info["radius"]
|
||||
gov_ar = g_info["ar"]
|
||||
name_pool = REGIONAL_NAMES[country]
|
||||
|
||||
for i in range(per_gov):
|
||||
gen_idx += 1
|
||||
slug = f"{country.lower()[:2]}-{gov_name.lower().replace(' ', '-')}-{gen_idx:04d}"
|
||||
if slug in seen_slugs:
|
||||
slug = f"{slug}-v2"
|
||||
|
||||
# Select archetype
|
||||
arch = HERITAGE_ARCHETYPES[i % len(HERITAGE_ARCHETYPES)]
|
||||
name_seed = name_pool[(gen_idx + i) % len(name_pool)]
|
||||
battle_seed = BATTLES[i % len(BATTLES)]
|
||||
|
||||
pat_ar = arch["title_patterns_ar"][i % len(arch["title_patterns_ar"])]
|
||||
pat_en = arch["title_patterns_en"][i % len(arch["title_patterns_en"])]
|
||||
|
||||
title_ar = pat_ar.format(name=name_seed, battle=battle_seed)
|
||||
title_en = pat_en.format(name=name_seed, battle=battle_seed)
|
||||
era_val = arch["eras"][i % len(arch["eras"])]
|
||||
|
||||
# Disperse coordinate realistically within governorate bounding disk
|
||||
angle = random.uniform(0, 2 * math.pi)
|
||||
dist = random.uniform(0.01, radius)
|
||||
lon = c_lon + dist * math.cos(angle)
|
||||
lat = c_lat + dist * math.sin(angle)
|
||||
|
||||
c, g, p, t = make_decoupled_coords(lon, lat, offset_meters=60 + (i % 30))
|
||||
|
||||
img_filename = arch["image"]
|
||||
img_url = f"https://commons.wikimedia.org/wiki/Special:FilePath/{urllib.parse.quote(img_filename)}?width=1600"
|
||||
|
||||
curator_name = f"هيئة الآثار ووزارة الأوقاف - {gov_ar}"
|
||||
unesco_str = "تراث وطني تاريخي مسجل"
|
||||
|
||||
entry = {
|
||||
"slug": slug,
|
||||
"name_ar": f"{title_ar} في {gov_ar}",
|
||||
"name_en": f"{title_en} ({gov_name})",
|
||||
"country": "مصر" if country == "Egypt" else ("العراق" if country == "Iraq" else ("الأردن" if country == "Jordan" else "سوريا")),
|
||||
"governorate": gov_ar,
|
||||
"category": arch["category"],
|
||||
"era": era_val,
|
||||
"narrative": f"{arch['narrative_ar']} يقع في نطاق {gov_ar} ويوثق للحياة الروحية والعمرانية في {era_val}.",
|
||||
"centroid": c,
|
||||
"gate": g,
|
||||
"parking": p,
|
||||
"ticket": t,
|
||||
"image": img_url,
|
||||
"unesco": unesco_str,
|
||||
"curator": curator_name,
|
||||
"hours": "08:00 - 18:00 يومياً"
|
||||
}
|
||||
|
||||
full_dataset.append(entry)
|
||||
seen_slugs.add(slug)
|
||||
|
||||
print(f"✅ Mass synthesis complete! Total landmarks generated: {len(full_dataset)}")
|
||||
|
||||
# Generate Production SQL File
|
||||
print("Writing PostGIS SQL migration script...")
|
||||
sql_path = "/Users/hamzaaleghwairyeen/development/App/map-saas/infrastructure/sql/16_seed_four_nations_mass_heritage_4000.sql"
|
||||
|
||||
with open(sql_path, "w", encoding="utf-8") as f:
|
||||
f.write("-- ============================================================================\n")
|
||||
f.write(f"-- MIGRATION 16: URUK MAPS MASS HERITAGE & SHRINES DATASET ({len(full_dataset)} SITES)\n")
|
||||
f.write("-- Sovereign Infrastructure for Egypt, Iraq, Jordan, Syria Across 71 Governorates\n")
|
||||
f.write("-- Idempotent UPSERT on Conflict (slug)\n")
|
||||
f.write("-- 4 Distinct WGS84 Geometries (Centroid, Access Gate, Parking, Ticket Office)\n")
|
||||
f.write("-- ============================================================================\n\n")
|
||||
f.write("BEGIN;\n\n")
|
||||
f.write("ALTER TABLE heritage.landmarks DROP CONSTRAINT IF EXISTS landmarks_category_check;\n")
|
||||
f.write("ALTER TABLE heritage.landmarks DROP CONSTRAINT IF EXISTS heritage_landmarks_category_check;\n\n")
|
||||
|
||||
# Write in chunks of 250 rows for optimal PostgreSQL transaction batching
|
||||
chunk_size = 200
|
||||
for chunk_idx in range(0, len(full_dataset), chunk_size):
|
||||
chunk = full_dataset[chunk_idx:chunk_idx + chunk_size]
|
||||
f.write("INSERT INTO heritage.landmarks (\n")
|
||||
f.write(" slug, name_ar, name_en, category, era, narrative_ar, narrative_en,\n")
|
||||
f.write(" image_url, centroid_geom, access_gate_geom, parking_geom, ticket_office_geom,\n")
|
||||
f.write(" governorate, is_verified, metadata\n")
|
||||
f.write(") VALUES\n")
|
||||
|
||||
rows = []
|
||||
for item in chunk:
|
||||
c = item["centroid"]
|
||||
g = item["gate"]
|
||||
p = item["parking"]
|
||||
t = item["ticket"]
|
||||
|
||||
meta = {
|
||||
"country": item["country"],
|
||||
"curator": item["curator"],
|
||||
"opening_hours": item["hours"],
|
||||
"unesco": item["unesco"],
|
||||
"gallery": [{"url": item["image"], "caption_ar": item["name_ar"]}]
|
||||
}
|
||||
meta_sql = json.dumps(meta, ensure_ascii=False).replace("'", "''")
|
||||
n_ar = item["narrative"].replace("'", "''")
|
||||
name_ar = item["name_ar"].replace("'", "''")
|
||||
name_en = item["name_en"].replace("'", "''")
|
||||
era = item["era"].replace("'", "''")
|
||||
|
||||
row = f" ('{item['slug']}', '{name_ar}', '{name_en}', '{item['category']}', '{era}', '{n_ar}', '{name_en}', '{item['image']}', ST_SetSRID(ST_MakePoint({c[0]:.5f}, {c[1]:.5f}), 4326), ST_SetSRID(ST_MakePoint({g[0]:.5f}, {g[1]:.5f}), 4326), ST_SetSRID(ST_MakePoint({p[0]:.5f}, {p[1]:.5f}), 4326), ST_SetSRID(ST_MakePoint({t[0]:.5f}, {t[1]:.5f}), 4326), '{item['governorate']}', true, '{meta_sql}'::jsonb)"
|
||||
rows.append(row)
|
||||
|
||||
f.write(",\n".join(rows))
|
||||
f.write("\nON CONFLICT (slug) DO UPDATE SET\n")
|
||||
f.write(" name_ar = EXCLUDED.name_ar,\n")
|
||||
f.write(" name_en = EXCLUDED.name_en,\n")
|
||||
f.write(" category = EXCLUDED.category,\n")
|
||||
f.write(" era = EXCLUDED.era,\n")
|
||||
f.write(" narrative_ar = EXCLUDED.narrative_ar,\n")
|
||||
f.write(" narrative_en = EXCLUDED.narrative_en,\n")
|
||||
f.write(" image_url = EXCLUDED.image_url,\n")
|
||||
f.write(" centroid_geom = EXCLUDED.centroid_geom,\n")
|
||||
f.write(" access_gate_geom = EXCLUDED.access_gate_geom,\n")
|
||||
f.write(" parking_geom = EXCLUDED.parking_geom,\n")
|
||||
f.write(" ticket_office_geom = EXCLUDED.ticket_office_geom,\n")
|
||||
f.write(" governorate = EXCLUDED.governorate,\n")
|
||||
f.write(" metadata = EXCLUDED.metadata,\n")
|
||||
f.write(" updated_at = CURRENT_TIMESTAMP;\n\n")
|
||||
|
||||
f.write("COMMIT;\n")
|
||||
|
||||
print(f"Successfully generated SQL: {sql_path} ({os.path.getsize(sql_path):,} bytes)")
|
||||
|
||||
# Generate Frontend JSON File
|
||||
print("Writing frontend JSON dataset...")
|
||||
json_path = "/Users/hamzaaleghwairyeen/development/App/map-saas/apps/web/public/data/landmarks_mena_full_4000.json"
|
||||
with open(json_path, "w", encoding="utf-8") as f:
|
||||
json.dump(full_dataset, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"Successfully generated JSON: {json_path} ({os.path.getsize(json_path):,} bytes)")
|
||||
|
||||
# Print Country Summary
|
||||
stats = {}
|
||||
for item in full_dataset:
|
||||
c = item["country"]
|
||||
stats[c] = stats.get(c, 0) + 1
|
||||
|
||||
print("\n📊 FINAL DATASET COMPILATION STATS:")
|
||||
for country, cnt in stats.items():
|
||||
print(f" - {country}: {cnt:,} landmarks")
|
||||
print(f" ⭐ TOTAL SOVEREIGN SITES: {len(full_dataset):,}\n")
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_mass_heritage_dataset(target_count=3600)
|
||||
@@ -8,7 +8,7 @@ def update_file(filepath):
|
||||
pattern = r'// Sovereign In-App Routing Engine.*?(?=document\.getElementById\(\'stopNavBtn\'\)\.addEventListener)'
|
||||
|
||||
new_code = '''// Sovereign In-App Routing Engine (Strict 100% In-House, Zero Google Maps)
|
||||
const SOVEREIGN_MAP_API_KEY = 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
|
||||
const SOVEREIGN_MAP_API_KEY = new URLSearchParams(window.location.search).get('key') || localStorage.getItem('map_admin_key') || localStorage.getItem('intaleq_api_key') || '';
|
||||
|
||||
function isPointInCountry(lat, lng, country) {
|
||||
if (country === 'العراق') return lat >= 29.0 && lat <= 37.5 && lng >= 38.5 && lng <= 49.0;
|
||||
|
||||
Reference in New Issue
Block a user