init
Some checks failed
Build and Release / release (push) Failing after 16s

This commit is contained in:
2026-04-20 09:29:40 +07:00
commit ea1e12a5bc
20 changed files with 4335 additions and 0 deletions

223
routes/geometries.js Normal file
View 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;
}