refactor: separate map feature queries and source records

This commit is contained in:
Sameh Khamis committed 2026-09-15 08:10:32 -07:00
1 parent a8125ad917
commit bf1d50f29b
28 files changed
+1128 -675

No files matched your search

+29 -61
View File
@@ -1,3 +1,4 @@
import { createOverpassFeatureSource } from './sources/overpassFeatures.js';
import { applicationServices } from './services/application.js';
import * as Cesium from 'cesium';
import {
@@ -891,7 +892,18 @@ export async function searchAndFlyTo(viewer, query, options = {}) {
const shouldResolveBuilding = navigationMode === 'precise-place';
const buildingBounds = shouldResolveBuilding
? await resolveBuildingBounds(lat, lng, query, options.boundaries)
? await resolveBuildingBounds(
lat,
lng,
query,
options.features ??
(options.boundaries
? createOverpassFeatureSource({
boundarySource: options.boundaries,
})
: undefined),
options.signal,
)
: null;
const range = requestedRange || defaultRangeForNavigationMode(navigationMode);
if (!mayFly()) return CANCELLED_SEARCH;
@@ -1340,36 +1352,22 @@ async function resolveBuildingBounds(
lat,
lon,
query,
boundaries = applicationServices.boundaries,
features = applicationServices.features,
signal,
) {
const overpassQuery = `
[out:json][timeout:10];
(
way(around:180,${lat},${lon})["building"];
relation(around:180,${lat},${lon})["building"];
way(around:180,${lat},${lon})["man_made"];
relation(around:180,${lat},${lon})["man_made"];
way(around:180,${lat},${lon})["tourism"="attraction"];
relation(around:180,${lat},${lon})["tourism"="attraction"];
);
out tags center geom;
`;
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 6000);
try {
const elements = await boundaries.query(overpassQuery, {
signal: controller.signal,
});
const candidates = await features.getFocusFootprints(
{ lat, lon },
{ signal },
);
return selectBuildingBounds(
Array.isArray(elements) ? elements : [],
Array.isArray(candidates) ? candidates : [],
lat,
lon,
query,
);
} catch {
return null;
} finally {
window.clearTimeout(timeout);
}
}
@@ -1377,11 +1375,11 @@ function selectBuildingBounds(elements, targetLat, targetLon, query) {
const queryWords = normalizedWords(query);
const candidates = [];
for (const element of elements) {
const coordinates = elementCoordinates(element);
const coordinates = element.coordinates;
if (coordinates.length < 3) continue;
const bounds = coordinateBounds(coordinates, targetLat);
if (!bounds || bounds.width < 2 || bounds.depth < 2) continue;
const tags = element.tags || {};
const names = element.names;
const center = element.center || averageCoordinate(coordinates);
const distanceM = approximateDistanceM(
targetLat,
@@ -1390,22 +1388,24 @@ function selectBuildingBounds(elements, targetLat, targetLon, query) {
center.lon,
);
const nameWords = normalizedWords(
[tags.name, tags['name:en'], tags.official_name, tags.alt_name]
[names.primary, names.english, names.official, names.alternate]
.filter(Boolean)
.join(' '),
);
const nameScore = wordOverlap(queryWords, nameWords);
const containsTarget = pointInPolygon(targetLon, targetLat, coordinates);
const height = buildingHeightFromTags(tags, bounds);
const height =
element.heightM ??
Math.max(12, Math.min(80, Math.max(bounds.width, bounds.depth) * 0.8));
candidates.push({
lat: center.lat,
lon: center.lon,
height,
width: bounds.width,
depth: bounds.depth,
osmName: tags.name || tags['name:en'] || null,
osmType: element.type,
osmId: element.id,
osmName: names.primary || names.english || null,
osmType: element.provenance?.type,
osmId: element.provenance?.id,
score: nameScore * 1000 + (containsTarget ? 500 : 0) - distanceM,
});
}
@@ -1415,22 +1415,6 @@ function selectBuildingBounds(elements, targetLat, targetLon, query) {
return best;
}
function elementCoordinates(element) {
if (Array.isArray(element.geometry)) {
return element.geometry.filter(
(point) => Number.isFinite(point?.lat) && Number.isFinite(point?.lon),
);
}
if (!Array.isArray(element.members)) return [];
return element.members.flatMap((member) =>
Array.isArray(member.geometry)
? member.geometry.filter(
(point) => Number.isFinite(point?.lat) && Number.isFinite(point?.lon),
)
: [],
);
}
function coordinateBounds(coordinates, latitude) {
const latitudes = coordinates.map((point) => point.lat);
const longitudes = coordinates.map((point) => point.lon);
@@ -1444,22 +1428,6 @@ function coordinateBounds(coordinates, latitude) {
};
}
function buildingHeightFromTags(tags, bounds) {
const explicitHeight = parseMeters(tags.height || tags['building:height']);
if (explicitHeight) return explicitHeight;
const levels = Number.parseFloat(tags['building:levels']);
const roofHeight = parseMeters(tags['roof:height']) || 0;
if (Number.isFinite(levels) && levels > 0) return levels * 3.3 + roofHeight;
return Math.max(12, Math.min(80, Math.max(bounds.width, bounds.depth) * 0.8));
}
function parseMeters(value) {
if (value == null) return 0;
const number = Number.parseFloat(String(value).replace(',', '.'));
if (!Number.isFinite(number) || number <= 0) return 0;
return /\b(ft|feet|foot)\b/i.test(String(value)) ? number * 0.3048 : number;
}
function averageCoordinate(coordinates) {
const total = coordinates.reduce(
(sum, point) => ({