"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, };