228 lines
4.4 KiB
JavaScript
228 lines
4.4 KiB
JavaScript
"use strict";
|
|
|
|
const db = require("../db/knex");
|
|
|
|
let RBushClass = null;
|
|
let tree = null;
|
|
let dirty = true;
|
|
let buildPromise = null;
|
|
let generation = 0;
|
|
|
|
function numberOrNull(value) {
|
|
if (value === null || value === undefined || value === "") {
|
|
return null;
|
|
}
|
|
|
|
const n = Number(value);
|
|
return Number.isFinite(n) ? n : null;
|
|
}
|
|
|
|
function makeIndexItem(row) {
|
|
const minLat = numberOrNull(row.min_latitude);
|
|
const maxLat = numberOrNull(row.max_latitude);
|
|
const minLon = numberOrNull(row.min_longitude);
|
|
const maxLon = numberOrNull(row.max_longitude);
|
|
|
|
// Usa il bounding box quando tutti e quattro i limiti sono presenti.
|
|
if (
|
|
minLat !== null &&
|
|
maxLat !== null &&
|
|
minLon !== null &&
|
|
maxLon !== null &&
|
|
minLat >= -90 &&
|
|
maxLat <= 90 &&
|
|
minLat <= maxLat &&
|
|
minLon >= -180 &&
|
|
maxLon <= 180 &&
|
|
minLon <= maxLon
|
|
) {
|
|
return {
|
|
minX: minLon,
|
|
minY: minLat,
|
|
maxX: maxLon,
|
|
maxY: maxLat,
|
|
id: String(row.id),
|
|
};
|
|
}
|
|
|
|
// Fallback per attività prive di bounding box completo.
|
|
const lat = numberOrNull(row.center_latitude);
|
|
const lon = numberOrNull(row.center_longitude);
|
|
|
|
if (
|
|
lat === null ||
|
|
lon === null ||
|
|
lat < -90 ||
|
|
lat > 90 ||
|
|
lon < -180 ||
|
|
lon > 180
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
minX: lon,
|
|
minY: lat,
|
|
maxX: lon,
|
|
maxY: lat,
|
|
id: String(row.id),
|
|
};
|
|
}
|
|
|
|
async function loadRBush() {
|
|
if (!RBushClass) {
|
|
const module = await import("rbush");
|
|
RBushClass = module.default;
|
|
}
|
|
|
|
return RBushClass;
|
|
}
|
|
|
|
async function ensureIndex() {
|
|
if (!dirty && tree) {
|
|
return tree;
|
|
}
|
|
|
|
if (buildPromise) {
|
|
await buildPromise;
|
|
|
|
if (!dirty && tree) {
|
|
return tree;
|
|
}
|
|
}
|
|
|
|
const buildGeneration = generation;
|
|
|
|
buildPromise = (async () => {
|
|
const RBush = await loadRBush();
|
|
|
|
const rows = await db("activities").select(
|
|
"id",
|
|
"min_latitude",
|
|
"max_latitude",
|
|
"min_longitude",
|
|
"max_longitude",
|
|
"center_latitude",
|
|
"center_longitude"
|
|
);
|
|
|
|
const items = [];
|
|
|
|
for (const row of rows) {
|
|
const item = makeIndexItem(row);
|
|
if (item) items.push(item);
|
|
}
|
|
|
|
const nextTree = new RBush();
|
|
nextTree.load(items);
|
|
|
|
// Non pubblicare un indice costruito mentre era in corso
|
|
// un'invalidazione dovuta a una modifica dei dati.
|
|
if (buildGeneration === generation) {
|
|
tree = nextTree;
|
|
dirty = false;
|
|
}
|
|
})();
|
|
|
|
try {
|
|
await buildPromise;
|
|
} finally {
|
|
buildPromise = null;
|
|
}
|
|
|
|
if (dirty || !tree) {
|
|
return ensureIndex();
|
|
}
|
|
|
|
return tree;
|
|
}
|
|
|
|
function invalidateActivitySpatialIndex() {
|
|
generation += 1;
|
|
dirty = true;
|
|
}
|
|
|
|
function searchBoxes(treeInstance, west, east, south, north) {
|
|
const boxes =
|
|
west <= east
|
|
? [{ minX: west, maxX: east, minY: south, maxY: north }]
|
|
: [
|
|
{ minX: west, maxX: 180, minY: south, maxY: north },
|
|
{ minX: -180, maxX: east, minY: south, maxY: north },
|
|
];
|
|
|
|
const found = new Map();
|
|
|
|
for (const box of boxes) {
|
|
for (const item of treeInstance.search(box)) {
|
|
found.set(item.id, item);
|
|
}
|
|
}
|
|
|
|
return [...found.keys()];
|
|
}
|
|
|
|
async function searchActivitiesInBounds({
|
|
west,
|
|
east,
|
|
south,
|
|
north,
|
|
readableSources,
|
|
includeDeleted = false,
|
|
limit = 1500,
|
|
}) {
|
|
const index = await ensureIndex();
|
|
|
|
const ids = searchBoxes(index, west, east, south, north);
|
|
|
|
if (!ids.length || !readableSources.length) {
|
|
return { items: [], truncated: false };
|
|
}
|
|
|
|
const rowsById = new Map();
|
|
const batchSize = 400;
|
|
|
|
// Blocchi piccoli per evitare di superare il limite dei parametri SQLite.
|
|
for (let i = 0; i < ids.length; i += batchSize) {
|
|
const batch = ids.slice(i, i + batchSize);
|
|
|
|
let query = db("activities")
|
|
.whereIn("id", batch)
|
|
.whereIn("user", readableSources);
|
|
|
|
if (!includeDeleted) {
|
|
query = query.whereNull("deleted_at");
|
|
}
|
|
|
|
const rows = await query.select("*");
|
|
|
|
for (const row of rows) {
|
|
rowsById.set(String(row.id), row);
|
|
}
|
|
}
|
|
|
|
// Mantiene un ordine deterministico rispetto ai candidati RBush.
|
|
const matching = [];
|
|
|
|
for (const id of ids) {
|
|
const row = rowsById.get(id);
|
|
|
|
if (row) {
|
|
matching.push(row);
|
|
|
|
if (matching.length > limit) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
items: matching.slice(0, limit),
|
|
truncated: matching.length > limit,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
searchActivitiesInBounds,
|
|
invalidateActivitySpatialIndex,
|
|
};
|