This commit is contained in:
223
routes/geometries.js
Normal file
223
routes/geometries.js
Normal file
@@ -0,0 +1,223 @@
|
||||
const express = require("express");
|
||||
const db = require("../db/polygons");
|
||||
|
||||
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({
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user