613 lines
28 KiB
JavaScript
613 lines
28 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 RoadRefinementController_1;
|
|
var _a, _b, _c;
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.RoadRefinementController = void 0;
|
|
const common_1 = require("@nestjs/common");
|
|
const swagger_1 = require("@nestjs/swagger");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const typeorm_2 = require("typeorm");
|
|
const candidate_road_entity_1 = require("./candidate-road.entity");
|
|
const road_stat_entity_1 = require("./road-stat.entity");
|
|
const api_key_guard_1 = require("../common/guards/api-key.guard");
|
|
let RoadRefinementController = RoadRefinementController_1 = class RoadRefinementController {
|
|
candidateRepo;
|
|
roadStatRepo;
|
|
dataSource;
|
|
logger = new common_1.Logger(RoadRefinementController_1.name);
|
|
constructor(candidateRepo, roadStatRepo, dataSource) {
|
|
this.candidateRepo = candidateRepo;
|
|
this.roadStatRepo = roadStatRepo;
|
|
this.dataSource = dataSource;
|
|
}
|
|
async getSummary() {
|
|
try {
|
|
const [cCount] = await this.dataSource.query("SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status='pending') as pending FROM candidate_roads");
|
|
const [clCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM road_segment_stats WHERE "isClosed" = true');
|
|
const [rCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM road_segment_stats');
|
|
return {
|
|
roads: {
|
|
analyzed: parseInt(rCount?.count || '0', 10),
|
|
closed: parseInt(clCount?.count || '0', 10),
|
|
},
|
|
candidates: {
|
|
total: parseInt(cCount?.total || '0', 10),
|
|
pending: parseInt(cCount?.pending || '0', 10),
|
|
},
|
|
};
|
|
}
|
|
catch (e) {
|
|
this.logger.error(`Error in summary: ${e.message}`);
|
|
return {
|
|
roads: { analyzed: 0, closed: 0 },
|
|
candidates: { total: 0, pending: 0 },
|
|
};
|
|
}
|
|
}
|
|
async getCandidates(status) {
|
|
const s = status || 'pending';
|
|
try {
|
|
const rows = await this.dataSource.query(`SELECT id, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
|
|
confidence, status, source, name, highway, oneway, "discoveredAt", "reviewedAt",
|
|
ST_AsGeoJSON(geometry) as geojson
|
|
FROM candidate_roads
|
|
WHERE status = $1
|
|
ORDER BY confidence DESC, "discoveredAt" DESC
|
|
LIMIT 100`, [s]);
|
|
return rows.map((r) => ({
|
|
...r,
|
|
geometry: r.geojson ? JSON.parse(r.geojson) : null,
|
|
}));
|
|
}
|
|
catch (e) {
|
|
this.logger.error(`Error fetching candidates: ${e.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
async submitManualCandidate(body) {
|
|
if (!body.geojson || !body.geojson.coordinates || body.geojson.coordinates.length < 2) {
|
|
throw new common_1.HttpException('Invalid GeoJSON LineString coordinates', common_1.HttpStatus.BAD_REQUEST);
|
|
}
|
|
try {
|
|
const geojsonStr = JSON.stringify(body.geojson);
|
|
const name = body.name || 'Unnamed Road';
|
|
const highway = body.highway || 'residential';
|
|
let result;
|
|
try {
|
|
result = await this.dataSource.query(`WITH raw_input AS (
|
|
SELECT ST_SetSRID(ST_GeomFromGeoJSON($1), 4326) AS geom
|
|
),
|
|
start_snap AS (
|
|
SELECT COALESCE(
|
|
(
|
|
SELECT ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)
|
|
FROM raw_input r
|
|
CROSS JOIN LATERAL (SELECT ST_Transform(ST_StartPoint(r.geom), 3857) AS pt) p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_StartPoint(r.geom)::geography) < 30
|
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_StartPoint(r.geom)::geography) ASC
|
|
LIMIT 1
|
|
),
|
|
(
|
|
SELECT ST_Transform(ST_ClosestPoint(l.way, ST_Transform(ST_StartPoint(r.geom), 3857)), 4326)
|
|
FROM raw_input r
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(ST_StartPoint(r.geom), 3857), 60)
|
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, ST_StartPoint(r.geom)::geography) < 30
|
|
ORDER BY ST_Distance(l.way, ST_Transform(ST_StartPoint(r.geom), 3857)) ASC
|
|
LIMIT 1
|
|
),
|
|
(SELECT ST_StartPoint(geom) FROM raw_input)
|
|
) AS pt
|
|
),
|
|
end_snap AS (
|
|
SELECT COALESCE(
|
|
(
|
|
SELECT ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)
|
|
FROM raw_input r
|
|
CROSS JOIN LATERAL (SELECT ST_Transform(ST_EndPoint(r.geom), 3857) AS pt) p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_EndPoint(r.geom)::geography) < 30
|
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_EndPoint(r.geom)::geography) ASC
|
|
LIMIT 1
|
|
),
|
|
(
|
|
SELECT ST_Transform(ST_ClosestPoint(l.way, ST_Transform(ST_EndPoint(r.geom), 3857)), 4326)
|
|
FROM raw_input r
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(ST_EndPoint(r.geom), 3857), 60)
|
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, ST_EndPoint(r.geom)::geography) < 30
|
|
ORDER BY ST_Distance(l.way, ST_Transform(ST_EndPoint(r.geom), 3857)) ASC
|
|
LIMIT 1
|
|
),
|
|
(SELECT ST_EndPoint(geom) FROM raw_input)
|
|
) AS pt
|
|
),
|
|
snapped_input AS (
|
|
SELECT ST_SetPoint(
|
|
ST_SetPoint(r.geom, 0, s.pt),
|
|
ST_NPoints(r.geom) - 1,
|
|
e.pt
|
|
) AS geom
|
|
FROM raw_input r, start_snap s, end_snap e
|
|
)
|
|
INSERT INTO candidate_roads (
|
|
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
|
|
confidence, status, source, name, highway, oneway, "discoveredAt"
|
|
)
|
|
SELECT
|
|
geom,
|
|
1, 10, 30.0,
|
|
ST_Length(ST_Transform(geom, 3857)),
|
|
0.90, 'pending', 'manual', $2, $3, 0, NOW()
|
|
FROM snapped_input
|
|
RETURNING id, name, highway, confidence, status`, [geojsonStr, name, highway]);
|
|
}
|
|
catch (err) {
|
|
this.logger.warn(`Complex snapping query failed, falling back to direct insertion: ${err.message}`);
|
|
result = await this.dataSource.query(`INSERT INTO candidate_roads (
|
|
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
|
|
confidence, status, source, name, highway, oneway, "discoveredAt"
|
|
)
|
|
VALUES (
|
|
ST_SetSRID(ST_GeomFromGeoJSON($1), 4326),
|
|
1, 10, 30.0,
|
|
ST_Length(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 3857)),
|
|
0.90, 'pending', 'manual', $2, $3, 0, NOW()
|
|
)
|
|
RETURNING id, name, highway, confidence, status`, [geojsonStr, name, highway]);
|
|
}
|
|
this.logger.log(`✍️ Manual candidate road registered: ${result[0]?.id} (${name})`);
|
|
return { success: true, candidate: result[0] };
|
|
}
|
|
catch (e) {
|
|
this.logger.error(`Failed to insert manual candidate road: ${e.message}`);
|
|
throw new common_1.HttpException(`Failed to create candidate road: ${e.message}`, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
async approveCandidate(id) {
|
|
try {
|
|
await this.dataSource.query(`
|
|
CREATE TABLE IF NOT EXISTS approved_roads (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
candidate_id UUID UNIQUE,
|
|
geometry GEOMETRY(LineString, 4326),
|
|
name VARCHAR(255),
|
|
highway VARCHAR(32),
|
|
confidence FLOAT,
|
|
"uniqueDriverCount" INT DEFAULT 1,
|
|
oneway SMALLINT DEFAULT 0,
|
|
start_node BIGINT,
|
|
end_node BIGINT,
|
|
approved_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS approved_roads_geom_idx ON approved_roads USING GIST(geometry);
|
|
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
|
|
`);
|
|
await this.candidateRepo.update(id, {
|
|
status: 'approved',
|
|
reviewedAt: new Date(),
|
|
});
|
|
await this.dataSource.query(`
|
|
DO $$
|
|
DECLARE
|
|
cand RECORD;
|
|
start_pt GEOMETRY;
|
|
end_pt GEOMETRY;
|
|
snapped_geom GEOMETRY;
|
|
s_node BIGINT := NULL;
|
|
e_node BIGINT := NULL;
|
|
node_rec RECORD;
|
|
line_rec RECORD;
|
|
BEGIN
|
|
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
|
|
INTO cand
|
|
FROM candidate_roads WHERE id = $1;
|
|
|
|
IF FOUND THEN
|
|
snapped_geom := cand.geom;
|
|
start_pt := ST_StartPoint(snapped_geom);
|
|
end_pt := ST_EndPoint(snapped_geom);
|
|
|
|
-- 1. Start node & geometry snapping
|
|
BEGIN
|
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
|
INTO node_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
s_node := node_rec.id;
|
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
|
ELSE
|
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
|
INTO line_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
|
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
|
END IF;
|
|
END IF;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
NULL;
|
|
END;
|
|
|
|
-- 2. End node & geometry snapping
|
|
BEGIN
|
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
|
INTO node_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
e_node := node_rec.id;
|
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
|
ELSE
|
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
|
INTO line_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
|
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
|
END IF;
|
|
END IF;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
NULL;
|
|
END;
|
|
|
|
-- Update candidate_roads
|
|
UPDATE candidate_roads SET geometry = snapped_geom WHERE id = cand.id;
|
|
|
|
-- Insert or update approved_roads
|
|
INSERT INTO approved_roads (
|
|
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, start_node, end_node
|
|
)
|
|
VALUES (
|
|
cand.id, snapped_geom, cand.name, cand.highway, cand.confidence, cand."uniqueDriverCount", cand.oneway, s_node, e_node
|
|
)
|
|
ON CONFLICT (candidate_id) DO UPDATE SET
|
|
geometry = EXCLUDED.geometry,
|
|
start_node = EXCLUDED.start_node,
|
|
end_node = EXCLUDED.end_node;
|
|
|
|
END IF;
|
|
END $$;
|
|
`, [id]);
|
|
try {
|
|
await this.dataSource.query(`
|
|
DO $$
|
|
DECLARE
|
|
r RECORD;
|
|
target_table TEXT := 'places_jordan';
|
|
center_lat FLOAT;
|
|
center_lng FLOAT;
|
|
BEGIN
|
|
SELECT name, ST_Y(ST_Centroid(geometry::geometry)) as lat, ST_X(ST_Centroid(geometry::geometry)) as lng
|
|
INTO r
|
|
FROM candidate_roads WHERE id = $1 AND name IS NOT NULL AND trim(name) != '';
|
|
|
|
IF FOUND THEN
|
|
center_lat := r.lat;
|
|
center_lng := r.lng;
|
|
|
|
IF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 38.7 AND center_lng <= 48.8 THEN
|
|
target_table := 'places_iraq';
|
|
ELSIF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 34.5 AND center_lng <= 42.5 THEN
|
|
IF center_lat > 32.5 AND center_lng > 35.8 THEN
|
|
target_table := 'places_syria';
|
|
ELSE
|
|
target_table := 'places_jordan';
|
|
END IF;
|
|
ELSIF center_lat >= 22 AND center_lat <= 32 AND center_lng >= 24.5 AND center_lng <= 37 THEN
|
|
target_table := 'places_egypt';
|
|
END IF;
|
|
|
|
EXECUTE format('
|
|
INSERT INTO %I (name, name_ar, category, latitude, longitude, location, source)
|
|
VALUES ($1, $1, $2, $3, $4, ST_SetSRID(ST_MakePoint($4, $3), 4326), $5)
|
|
', target_table) USING r.name, 'street', center_lat, center_lng, 'approved_road';
|
|
END IF;
|
|
END $$;
|
|
`, [id]);
|
|
}
|
|
catch (err) {
|
|
this.logger.warn(`Could not index approved road to places: ${err.message}`);
|
|
}
|
|
this.logger.log(`✅ Road candidate ${id} approved & published.`);
|
|
return { success: true, id, status: 'approved' };
|
|
}
|
|
catch (e) {
|
|
this.logger.error(`Failed to approve candidate road: ${e.message}`);
|
|
throw new common_1.HttpException(e.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
async rejectCandidate(id) {
|
|
try {
|
|
await this.candidateRepo.update(id, {
|
|
status: 'rejected',
|
|
reviewedAt: new Date(),
|
|
});
|
|
return { success: true, id, status: 'rejected' };
|
|
}
|
|
catch (e) {
|
|
this.logger.error(`Failed to reject candidate road: ${e.message}`);
|
|
throw new common_1.HttpException(e.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
async getClosures() {
|
|
return this.roadStatRepo.find({
|
|
where: { isClosed: true },
|
|
order: { sampleCount: 'DESC' },
|
|
take: 50,
|
|
});
|
|
}
|
|
async discoverOvertureGaps() {
|
|
try {
|
|
const exists = await this.dataSource.query(`
|
|
SELECT to_regclass('public.overture_transportation') as tbl;
|
|
`);
|
|
if (!exists[0]?.tbl) {
|
|
return { success: true, candidatesFound: 0, message: 'Overture transportation table not loaded yet.' };
|
|
}
|
|
const inserted = await this.dataSource.query(`
|
|
INSERT INTO candidate_roads (geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters", confidence, status, source, name, highway, oneway)
|
|
SELECT
|
|
o.geometry,
|
|
5, 50, 40.0,
|
|
ST_Length(ST_Transform(o.geometry::geometry, 3857)),
|
|
0.85,
|
|
'pending',
|
|
'overture',
|
|
o.name,
|
|
COALESCE(o.class, 'residential'),
|
|
0
|
|
FROM overture_transportation o
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM planet_osm_line l
|
|
WHERE ST_DWithin(ST_Transform(o.geometry::geometry, 3857), l.way, 25)
|
|
)
|
|
AND ST_Length(ST_Transform(o.geometry::geometry, 3857)) > 40
|
|
LIMIT 50
|
|
RETURNING id;
|
|
`);
|
|
return { success: true, candidatesFound: inserted.length };
|
|
}
|
|
catch (e) {
|
|
this.logger.warn(`Overture gap check skipped: ${e.message}`);
|
|
return { success: true, candidatesFound: 0 };
|
|
}
|
|
}
|
|
async discoverClosures() {
|
|
return { success: true, roadsClosed: 0 };
|
|
}
|
|
async snapExistingApprovedRoads() {
|
|
try {
|
|
await this.dataSource.query(`
|
|
DO $$
|
|
DECLARE
|
|
r RECORD;
|
|
start_pt GEOMETRY;
|
|
end_pt GEOMETRY;
|
|
snapped_geom GEOMETRY;
|
|
s_node BIGINT;
|
|
e_node BIGINT;
|
|
node_rec RECORD;
|
|
line_rec RECORD;
|
|
BEGIN
|
|
FOR r IN SELECT id, geometry FROM approved_roads LOOP
|
|
snapped_geom := r.geometry;
|
|
s_node := NULL;
|
|
e_node := NULL;
|
|
start_pt := ST_StartPoint(snapped_geom);
|
|
end_pt := ST_EndPoint(snapped_geom);
|
|
|
|
-- Snap start
|
|
BEGIN
|
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
|
INTO node_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
|
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
s_node := node_rec.id;
|
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
|
ELSE
|
|
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
|
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
|
INTO line_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
|
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
|
END IF;
|
|
END IF;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
NULL;
|
|
END;
|
|
|
|
-- Snap end
|
|
BEGIN
|
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
|
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
|
|
INTO node_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
JOIN planet_osm_ways w ON w.id = l.osm_id
|
|
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
|
|
JOIN planet_osm_nodes n ON n.id = wn.node_id
|
|
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
|
|
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
e_node := node_rec.id;
|
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
|
|
ELSE
|
|
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
|
|
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
|
|
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
|
|
INTO line_rec
|
|
FROM p
|
|
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
|
|
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
|
|
ORDER BY ST_Distance(l.way, p.pt) ASC
|
|
LIMIT 1;
|
|
|
|
IF FOUND THEN
|
|
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
|
|
END IF;
|
|
END IF;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
NULL;
|
|
END;
|
|
|
|
UPDATE approved_roads
|
|
SET geometry = snapped_geom,
|
|
start_node = COALESCE(s_node, start_node),
|
|
end_node = COALESCE(e_node, end_node)
|
|
WHERE id = r.id;
|
|
END LOOP;
|
|
END $$;
|
|
`);
|
|
return { success: true, message: 'All approved roads snapped & nodes resolved' };
|
|
}
|
|
catch (e) {
|
|
this.logger.error(`Failed to snap existing roads: ${e.message}`);
|
|
throw new common_1.HttpException(e.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
|
|
}
|
|
}
|
|
};
|
|
exports.RoadRefinementController = RoadRefinementController;
|
|
__decorate([
|
|
(0, common_1.Get)('summary'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Get road intelligence summary 📊' }),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", []),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "getSummary", null);
|
|
__decorate([
|
|
(0, common_1.Get)('candidates'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Get candidate roads list 🛣️' }),
|
|
(0, swagger_1.ApiQuery)({ name: 'status', required: false }),
|
|
__param(0, (0, common_1.Query)('status')),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", [String]),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "getCandidates", null);
|
|
__decorate([
|
|
(0, common_1.Post)('candidates/manual'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Submit manual candidate road drawn on map ✍️' }),
|
|
__param(0, (0, common_1.Body)()),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", [Object]),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "submitManualCandidate", null);
|
|
__decorate([
|
|
(0, common_1.Patch)('candidates/:id/approve'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Approve candidate road and move to approved_roads with auto-snapped topology ✅' }),
|
|
(0, swagger_1.ApiParam)({ name: 'id', description: 'Candidate UUID' }),
|
|
__param(0, (0, common_1.Param)('id')),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", [String]),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "approveCandidate", null);
|
|
__decorate([
|
|
(0, common_1.Patch)('candidates/:id/reject'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Reject candidate road ❌' }),
|
|
(0, swagger_1.ApiParam)({ name: 'id', description: 'Candidate UUID' }),
|
|
__param(0, (0, common_1.Param)('id')),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", [String]),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "rejectCandidate", null);
|
|
__decorate([
|
|
(0, common_1.Get)('closures'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Get active road closures 🚧' }),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", []),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "getClosures", null);
|
|
__decorate([
|
|
(0, common_1.Post)('discover-overture-gaps'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Discover missing roads against Overture Maps 🗺️' }),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", []),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "discoverOvertureGaps", null);
|
|
__decorate([
|
|
(0, common_1.Post)('discover-closures'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Discover closures 🚧' }),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", []),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "discoverClosures", null);
|
|
__decorate([
|
|
(0, common_1.Post)('snap-existing-roads'),
|
|
(0, swagger_1.ApiOperation)({ summary: 'Auto-snap and resolve topology nodes for all existing approved roads' }),
|
|
__metadata("design:type", Function),
|
|
__metadata("design:paramtypes", []),
|
|
__metadata("design:returntype", Promise)
|
|
], RoadRefinementController.prototype, "snapExistingApprovedRoads", null);
|
|
exports.RoadRefinementController = RoadRefinementController = RoadRefinementController_1 = __decorate([
|
|
(0, swagger_1.ApiTags)('map-refinement-roads'),
|
|
(0, common_1.Controller)('map-refinement/roads'),
|
|
(0, common_1.UseGuards)(api_key_guard_1.ApiKeyGuard),
|
|
__param(0, (0, typeorm_1.InjectRepository)(candidate_road_entity_1.CandidateRoad)),
|
|
__param(1, (0, typeorm_1.InjectRepository)(road_stat_entity_1.RoadSegmentStat)),
|
|
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.DataSource !== "undefined" && typeorm_2.DataSource) === "function" ? _c : Object])
|
|
], RoadRefinementController);
|
|
//# sourceMappingURL=road-refinement.controller.js.map
|