283 lines
15 KiB
JavaScript
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
|