225 lines
6.8 KiB
JavaScript
225 lines
6.8 KiB
JavaScript
const express = require("express");
|
|
const db = require("../db/polygons");
|
|
const { normalizeFeatureCollectionContract, normalizeFeatureContract } = require("../types/contracts");
|
|
|
|
const router = express.Router();
|
|
|
|
// =======================
|
|
// Query by bbox/time(/entity)
|
|
// =======================
|
|
router.get("/", (req, res) => {
|
|
const minLng = Number(req.query.minLng);
|
|
const minLat = Number(req.query.minLat);
|
|
const maxLng = Number(req.query.maxLng);
|
|
const maxLat = Number(req.query.maxLat);
|
|
|
|
if (
|
|
!Number.isFinite(minLng) ||
|
|
!Number.isFinite(minLat) ||
|
|
!Number.isFinite(maxLng) ||
|
|
!Number.isFinite(maxLat)
|
|
) {
|
|
return res.status(400).json({ error: "Missing/invalid bbox" });
|
|
}
|
|
|
|
let t = null;
|
|
if (req.query.time !== undefined && req.query.time !== "") {
|
|
const parsed = Number(req.query.time);
|
|
if (!Number.isFinite(parsed)) {
|
|
return res.status(400).json({ error: "Invalid time" });
|
|
}
|
|
t = Math.trunc(parsed);
|
|
}
|
|
|
|
let entityId = null;
|
|
try {
|
|
entityId = normalizeEntityId(req.query.entity_id);
|
|
} catch (err) {
|
|
return res.status(err.status || 400).json({ error: err.message });
|
|
}
|
|
|
|
const rows = db.prepare(`
|
|
SELECT
|
|
g.*
|
|
FROM geometries g
|
|
WHERE g.is_deleted = 0
|
|
AND g.bbox_max_lng >= ?
|
|
AND g.bbox_min_lng <= ?
|
|
AND g.bbox_max_lat >= ?
|
|
AND g.bbox_min_lat <= ?
|
|
AND (
|
|
? IS NULL
|
|
OR (
|
|
(g.time_start IS NULL OR g.time_start <= ?)
|
|
AND (g.time_end IS NULL OR g.time_end >= ?)
|
|
)
|
|
)
|
|
AND (
|
|
? IS NULL
|
|
OR EXISTS (
|
|
SELECT 1
|
|
FROM entity_geometries eg
|
|
JOIN entities e
|
|
ON e.id = eg.entity_id
|
|
AND e.is_deleted = 0
|
|
WHERE eg.geometry_id = g.id
|
|
AND eg.entity_id = ?
|
|
)
|
|
)
|
|
`).all(minLng, maxLng, minLat, maxLat, t, t, t, entityId, entityId);
|
|
|
|
const geometryIds = rows.map((row) => String(row.id));
|
|
const linksByGeometryId = loadGeometryLinksByGeometryId(geometryIds);
|
|
|
|
res.json(normalizeFeatureCollectionContract({
|
|
type: "FeatureCollection",
|
|
features: rows.map((row) => {
|
|
const linkedEntities = linksByGeometryId.get(String(row.id)) || [];
|
|
return buildFeatureFromRow(row, linkedEntities);
|
|
}),
|
|
}));
|
|
});
|
|
|
|
module.exports = router;
|
|
|
|
function buildFeatureFromRow(row, linkedEntities = []) {
|
|
const entityIds = linkedEntities.map((entity) => entity.entity_id);
|
|
const primaryEntityId = entityIds[0] || null;
|
|
const orderedEntityIds = entityIds;
|
|
const orderedLinkedEntities = linkedEntities;
|
|
const primaryEntity =
|
|
orderedLinkedEntities.find((entity) => entity.entity_id === primaryEntityId) ||
|
|
orderedLinkedEntities[0] ||
|
|
null;
|
|
const storedGeometryType = normalizeGeometryType(row.type);
|
|
const semanticType = storedGeometryType && !isLegacyLineModeToken(storedGeometryType)
|
|
? storedGeometryType
|
|
: normalizeGeometryType(primaryEntity?.entity_type_id);
|
|
|
|
return normalizeFeatureContract({
|
|
type: "Feature",
|
|
properties: {
|
|
id: row.id,
|
|
type: semanticType || null,
|
|
time_start: row.time_start,
|
|
time_end: row.time_end,
|
|
binding: parseBindingIds(row.binding),
|
|
entity_id: primaryEntityId,
|
|
entity_ids: orderedEntityIds,
|
|
entity_name: primaryEntity?.entity_name || null,
|
|
entity_names: orderedLinkedEntities
|
|
.map((entity) => entity.entity_name)
|
|
.filter((name) => typeof name === "string" && name.length > 0),
|
|
entity_type_id: primaryEntity?.entity_type_id || null,
|
|
},
|
|
geometry: JSON.parse(row.draw_geometry),
|
|
});
|
|
}
|
|
|
|
function normalizeGeometryType(value) {
|
|
if (value === undefined || value === null || value === "") return null;
|
|
const normalized = String(value).trim().toLowerCase();
|
|
return normalized.length ? normalized : null;
|
|
}
|
|
|
|
function isLegacyLineModeToken(value) {
|
|
return value === "line" || value === "path";
|
|
}
|
|
|
|
function normalizeEntityId(value) {
|
|
if (value === undefined || value === null || value === "") return null;
|
|
const normalized = String(value).trim();
|
|
return normalized || null;
|
|
}
|
|
|
|
function normalizeBindingIds(rawBinding) {
|
|
if (rawBinding === undefined || rawBinding === null || rawBinding === "") {
|
|
return [];
|
|
}
|
|
|
|
if (!Array.isArray(rawBinding)) {
|
|
throw createValidationError("binding must be an array");
|
|
}
|
|
|
|
const deduped = [];
|
|
const seen = new Set();
|
|
|
|
for (const rawId of rawBinding) {
|
|
if (typeof rawId !== "string" && typeof rawId !== "number") {
|
|
throw createValidationError("binding must contain geometry ids as string/number");
|
|
}
|
|
|
|
const normalized = String(rawId).trim();
|
|
if (!normalized || seen.has(normalized)) continue;
|
|
seen.add(normalized);
|
|
deduped.push(normalized);
|
|
}
|
|
|
|
return deduped;
|
|
}
|
|
|
|
function parseBindingIds(rawBinding) {
|
|
if (rawBinding === undefined || rawBinding === null || rawBinding === "") {
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(rawBinding)) {
|
|
try {
|
|
return normalizeBindingIds(rawBinding);
|
|
} catch (_err) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (typeof rawBinding === "string") {
|
|
try {
|
|
const parsed = JSON.parse(rawBinding);
|
|
return normalizeBindingIds(parsed);
|
|
} catch (_err) {
|
|
const plain = rawBinding.trim();
|
|
return plain.length ? [plain] : [];
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function loadGeometryLinksByGeometryId(geometryIds) {
|
|
const map = new Map();
|
|
if (!geometryIds.length) return map;
|
|
|
|
const placeholders = geometryIds.map(() => "?").join(",");
|
|
const rows = db.prepare(`
|
|
SELECT
|
|
eg.geometry_id,
|
|
eg.entity_id,
|
|
e.name AS entity_name,
|
|
e.type_id AS entity_type_id
|
|
FROM entity_geometries eg
|
|
JOIN entities e
|
|
ON e.id = eg.entity_id
|
|
AND e.is_deleted = 0
|
|
WHERE eg.geometry_id IN (${placeholders})
|
|
ORDER BY eg.rowid ASC
|
|
`).all(...geometryIds);
|
|
|
|
for (const row of rows) {
|
|
const geometryId = String(row.geometry_id);
|
|
const current = map.get(geometryId) || [];
|
|
current.push({
|
|
entity_id: row.entity_id,
|
|
entity_name: row.entity_name || null,
|
|
entity_type_id: row.entity_type_id || null,
|
|
});
|
|
map.set(geometryId, current);
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
function createValidationError(message, status = 400) {
|
|
const err = new Error(message);
|
|
err.status = status;
|
|
return err;
|
|
}
|