"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var AdministrativeLinkingService_1; Object.defineProperty(exports, "__esModule", { value: true }); exports.AdministrativeLinkingService = void 0; const common_1 = require("@nestjs/common"); const typeorm_1 = require("@nestjs/typeorm"); const typeorm_2 = require("typeorm"); const neighborhood_point_entity_1 = require("./entities/neighborhood-point.entity"); const neighborhood_polygon_entity_1 = require("./entities/neighborhood-polygon.entity"); const admin_boundary_entity_1 = require("./entities/admin-boundary.entity"); const axios_1 = __importDefault(require("axios")); let AdministrativeLinkingService = AdministrativeLinkingService_1 = class AdministrativeLinkingService { dataSource; neighborhoodPointRepo; neighborhoodPolygonRepo; adminBoundaryRepo; logger = new common_1.Logger(AdministrativeLinkingService_1.name); constructor(dataSource, neighborhoodPointRepo, neighborhoodPolygonRepo, adminBoundaryRepo) { this.dataSource = dataSource; this.neighborhoodPointRepo = neighborhoodPointRepo; this.neighborhoodPolygonRepo = neighborhoodPolygonRepo; this.adminBoundaryRepo = adminBoundaryRepo; } overpassMirrors = [ 'https://overpass-api.de/api/interpreter', 'https://overpass.kumi.systems/api/interpreter', 'https://z.overpass-api.de/api/interpreter' ]; async fetchWithRetry(data, retries = 3) { for (let i = 0; i <= retries; i++) { const url = this.overpassMirrors[i % this.overpassMirrors.length]; try { const response = await axios_1.default.post(url, data, { timeout: 45000, headers: { 'User-Agent': 'IntaleqMapBot/1.0', 'Content-Type': 'application/x-www-form-urlencoded' } }); return response.data; } catch (error) { this.logger.warn(`Mirror ${url} failed: ${error.message}. Trying next mirror...`); if (i === retries) throw error; await new Promise(res => setTimeout(res, 2000)); } } } async syncOsmNeighborhoodPoints(bbox = '31.86,35.94,32.22,36.25', country) { this.logger.log(`Syncing neighborhood points for country: ${country || 'auto'}, bbox: ${bbox}`); const query = `[out:json][timeout:900];node["place"~"neighbourhood|suburb|town"](${bbox});out;`; try { const data = await this.fetchWithRetry(`data=${encodeURIComponent(query)}`); const elements = data.elements; this.logger.log(`Found ${elements.length} points in OSM.`); let detectedCountry = country; if (!detectedCountry) { const firstLat = parseFloat(bbox.split(',')[0]); if (firstLat > 32.3) detectedCountry = 'syria'; else if (firstLat < 31.0) detectedCountry = 'egypt'; else detectedCountry = 'jordan'; } for (const e of elements) { const nameAr = e.tags['name:ar'] || e.tags['name']; const nameEn = e.tags['name:en'] || e.tags['name']; const geometry = { type: 'Point', coordinates: [e.lon, e.lat] }; await this.dataSource.query(` INSERT INTO neighborhood_points (osm_id, name_ar, name_en, place_type, geometry, country) VALUES ($1, $2, $3, $4, ST_SetSRID(ST_GeomFromGeoJSON($5), 4326), $6) ON CONFLICT (osm_id) DO UPDATE SET name_ar = EXCLUDED.name_ar, name_en = EXCLUDED.name_en, place_type = EXCLUDED.place_type, geometry = EXCLUDED.geometry, country = COALESCE(EXCLUDED.country, neighborhood_points.country); `, [e.id, nameAr, nameEn, e.tags['place'], JSON.stringify(geometry), detectedCountry]); } await this.dataSource.query(` UPDATE neighborhood_points np SET district_id = COALESCE( ( SELECT id FROM admin_boundaries ab WHERE ab.admin_level IN (6, 8) AND ST_Contains(ab.geom::geometry, np.geometry::geometry) ORDER BY ab.admin_level DESC LIMIT 1 ), ( SELECT id FROM admin_boundaries ab WHERE ab.admin_level IN (6, 8) ORDER BY ab.geom::geometry <-> np.geometry::geometry LIMIT 1 ) ) WHERE district_id IS NULL AND (country = $1 OR $1 IS NULL); `, [detectedCountry]); const totalPoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points`); const linkedPoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points WHERE district_id IS NOT NULL`); const countryPoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points WHERE country = $1`, [detectedCountry]); const diagnostics = { detected_country: detectedCountry, osm_fetched: elements.length, country_points: parseInt(countryPoints[0].cnt), total_points_in_db: parseInt(totalPoints[0].cnt), linked_to_district: parseInt(linkedPoints[0].cnt), }; this.logger.log(`Step 1 diagnostics: ${JSON.stringify(diagnostics)}`); return diagnostics; } catch (error) { this.logger.error(`Failed to sync OSM points: ${error.message}`, error.stack); throw error; } } async generateVoronoiNeighborhoods(country) { this.logger.log(`Generating Voronoi polygons for ${country || 'all'} neighborhoods...`); const whereClause = country ? `WHERE district_id IS NOT NULL AND country = '${country}'` : `WHERE district_id IS NOT NULL`; const prePoints = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_points ${whereClause}`); const preDistricts = await this.dataSource.query(`SELECT district_id, count(*) as cnt FROM neighborhood_points ${whereClause} GROUP BY district_id`); const deleteWhere = country ? `WHERE method = 'voronoi' AND country = '${country}'` : `WHERE method = 'voronoi'`; await this.dataSource.query(`DELETE FROM neighborhood_polygons ${deleteWhere}`); this.logger.log(`Deleted old voronoi polygons for ${country || 'all'}.`); let totalInserted = 0; for (const row of preDistricts) { const districtId = row.district_id; const pointCount = parseInt(row.cnt); try { if (pointCount >= 2) { const result = await this.dataSource.query(` INSERT INTO neighborhood_polygons (osm_id, name_ar, name_en, place_type, parent_id, method, country, geometry) WITH voronoi_cells AS ( SELECT (ST_Dump(ST_VoronoiPolygons( ST_Collect(np.geometry::geometry), 0.00001, d.geom::geometry ))).geom as cell FROM neighborhood_points np JOIN admin_boundaries d ON d.id = np.district_id WHERE np.district_id = $1 GROUP BY d.geom ), matched AS ( SELECT np.osm_id, np.name_ar, np.name_en, np.place_type, np.district_id, np.country, ST_Multi(ST_Intersection(vc.cell::geometry, d.geom::geometry)) as geometry FROM voronoi_cells vc CROSS JOIN LATERAL ( SELECT np2.* FROM neighborhood_points np2 WHERE np2.district_id = $1 ORDER BY np2.geometry::geometry <-> vc.cell::geometry LIMIT 1 ) np JOIN admin_boundaries d ON d.id = $1 ) SELECT osm_id, name_ar, name_en, place_type, district_id, 'voronoi', country, geometry FROM matched WHERE ST_IsValid(geometry) AND NOT ST_IsEmpty(geometry) `, [districtId]); totalInserted += (result?.length || result?.[1] || 0); } else if (pointCount === 1) { await this.dataSource.query(` INSERT INTO neighborhood_polygons (osm_id, name_ar, name_en, place_type, parent_id, method, country, geometry) SELECT np.osm_id, np.name_ar, np.name_en, np.place_type, np.district_id, 'voronoi', np.country, ST_Multi(d.geom::geometry) FROM neighborhood_points np JOIN admin_boundaries d ON d.id = np.district_id WHERE np.district_id = $1 `, [districtId]); totalInserted++; } } catch (err) { this.logger.error(`Voronoi FAILED for district ${districtId}: ${err.message}`); } } const postCount = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_polygons ${country ? `WHERE country = '${country}'` : ''}`); return { status: 'success', country: country || 'all', total_polygons: parseInt(postCount[0].cnt), districts_processed: preDistricts.length }; } async linkPlaces(country) { const tableName = `places_${country}`; const polyCount = await this.dataSource.query(`SELECT count(*) as cnt FROM neighborhood_polygons`); const placeCount = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE location IS NOT NULL`); this.logger.log(`linkPlaces pre-flight: ${polyCount[0].cnt} polygons, ${placeCount[0].cnt} places with location in ${tableName}`); if (parseInt(polyCount[0].cnt) === 0) { this.logger.error('ABORT: neighborhood_polygons is EMPTY. Run generate-voronoi first!'); return { status: 'FAILED', error: 'neighborhood_polygons table is empty. Run generate-voronoi first.', polygons: 0, places: parseInt(placeCount[0].cnt) }; } this.logger.log(`Linking administrative hierarchy for ${tableName} using KNN...`); const updateResult = await this.dataSource.query(` UPDATE ${tableName} p SET neighborhood_id = ( SELECT np.id FROM neighborhood_polygons np WHERE (np.country = $1 OR np.country IS NULL) ORDER BY p.location::geometry <-> np.geometry::geometry LIMIT 1 ), sub_district_id = ( SELECT ab.id FROM admin_boundaries ab WHERE ab.admin_level = 8 AND ST_Within(p.location::geometry, ab.geom::geometry) LIMIT 1 ), district_id = ( SELECT ab.id FROM admin_boundaries ab WHERE ab.admin_level = 6 AND ST_Within(p.location::geometry, ab.geom::geometry) LIMIT 1 ), governorate_id = ( SELECT ab.id FROM admin_boundaries ab WHERE ab.admin_level = 4 AND ST_Within(p.location::geometry, ab.geom::geometry) LIMIT 1 ) WHERE p.location IS NOT NULL; `, [country]); const linkedNeighborhood = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE neighborhood_id IS NOT NULL`); const linkedDistrict = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE district_id IS NOT NULL`); const linkedGov = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName} WHERE governorate_id IS NOT NULL`); const totalPlaces = await this.dataSource.query(`SELECT count(*) as cnt FROM ${tableName}`); const samplePlace = await this.dataSource.query(` SELECT p.id, p.name_ar, p.neighborhood_id, p.district_id, p.governorate_id, n.name_ar as neighborhood_name FROM ${tableName} p LEFT JOIN neighborhood_polygons n ON p.neighborhood_id = n.id WHERE p.name_ar LIKE '%معصوم%' LIMIT 3 `); const diagnostics = { status: 'completed', country, total_places: parseInt(totalPlaces[0].cnt), linked_neighborhood: parseInt(linkedNeighborhood[0].cnt), linked_district: parseInt(linkedDistrict[0].cnt), linked_governorate: parseInt(linkedGov[0].cnt), sample_masoum: samplePlace, rows_affected: updateResult?.[1] || 'unknown' }; this.logger.log(`Step 3 diagnostics: ${JSON.stringify(diagnostics)}`); return diagnostics; } }; exports.AdministrativeLinkingService = AdministrativeLinkingService; exports.AdministrativeLinkingService = AdministrativeLinkingService = AdministrativeLinkingService_1 = __decorate([ (0, common_1.Injectable)(), __param(1, (0, typeorm_1.InjectRepository)(neighborhood_point_entity_1.NeighborhoodPoint)), __param(2, (0, typeorm_1.InjectRepository)(neighborhood_polygon_entity_1.NeighborhoodPolygon)), __param(3, (0, typeorm_1.InjectRepository)(admin_boundary_entity_1.AdminBoundary)), __metadata("design:paramtypes", [typeorm_2.DataSource, typeorm_2.Repository, typeorm_2.Repository, typeorm_2.Repository]) ], AdministrativeLinkingService); //# sourceMappingURL=administrative-linking.service.js.map