Files
maps-saas/apps/api/dist/geocoding/administrative-linking.service.js
T

283 lines
15 KiB
JavaScript

"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;
var _a, _b, _c, _d;
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", [typeof (_a = typeof typeorm_2.DataSource !== "undefined" && typeorm_2.DataSource) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _c : Object, typeof (_d = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _d : Object])
], AdministrativeLinkingService);
//# sourceMappingURL=administrative-linking.service.js.map