mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
211 lines
6.9 KiB
JavaScript
211 lines
6.9 KiB
JavaScript
import { CAMERA_SEEDS, SOURCE_ENDPOINT } from './policy.js';
|
|
|
|
export function createCatalog({ state: layerState, services, parts, source }) {
|
|
const { CITY_POIS } = services.locations;
|
|
|
|
/**
|
|
* Builds the initial camera catalog from CAMERA_SEEDS definitions.
|
|
* Each seed is resolved against its city's POI coordinates, offset, and
|
|
* passed through ensureCameraPose to populate derived fields.
|
|
* @returns {Object[]} Array of fully-initialized camera objects.
|
|
*/
|
|
|
|
function seedCatalog() {
|
|
const catalog = [];
|
|
for (const seed of CAMERA_SEEDS) {
|
|
const city = CITY_POIS[seed.cityId];
|
|
const poi = city?.pois?.[seed.poiIndex];
|
|
if (!city || !poi) continue;
|
|
const { latOffset, lonOffset } = parts.model.offsetDegrees(
|
|
poi.lat,
|
|
seed.offsetNorthM || 0,
|
|
seed.offsetEastM || 0,
|
|
);
|
|
const camera = {
|
|
id: seed.id,
|
|
name: seed.label,
|
|
cityId: seed.cityId,
|
|
city: city.name,
|
|
provider: 'OSM Camera Grid',
|
|
sourceKind: 'seed',
|
|
feedType: 'image',
|
|
feedConfigured: false,
|
|
headingConfidence: 'medium',
|
|
lat: poi.lat + latOffset,
|
|
lon: poi.lon + lonOffset,
|
|
headingDeg: parts.model.normalizeHeading(
|
|
seed.headingDeg ?? poi.heading ?? 0,
|
|
),
|
|
fovDeg: parts.model.clamp(seed.fovDeg ?? 70, 20, 120),
|
|
rangeM: parts.model.clamp(seed.rangeM ?? 700, 260, 1800),
|
|
mountHeightM: parts.model.clamp(seed.elevationM ?? 22, 8, 80),
|
|
groundElevationM: Number(city.groundElevation) || 0,
|
|
absoluteHeightM:
|
|
(Number(city.groundElevation) || 0) +
|
|
parts.model.clamp(seed.elevationM ?? 22, 8, 80),
|
|
pitchDeg: parts.model.clamp(seed.pitchDeg ?? -17, -40, -4),
|
|
};
|
|
parts.model.ensureCameraPose(camera);
|
|
catalog.push(camera);
|
|
}
|
|
return catalog;
|
|
}
|
|
|
|
/**
|
|
* Looks up a city ID from CITY_POIS by exact or partial name match.
|
|
* @param {string} cityName
|
|
* @returns {string|null} Matching city ID or null.
|
|
*/
|
|
|
|
function cityIdByName(cityName) {
|
|
const probe = String(cityName || '')
|
|
.trim()
|
|
.toLowerCase();
|
|
if (!probe) return null;
|
|
for (const [cityId, city] of Object.entries(CITY_POIS)) {
|
|
if (city.name.toLowerCase() === probe) return cityId;
|
|
}
|
|
for (const [cityId, city] of Object.entries(CITY_POIS)) {
|
|
if (
|
|
city.name.toLowerCase().includes(probe) ||
|
|
probe.includes(city.name.toLowerCase())
|
|
)
|
|
return cityId;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Fetches configured camera sources from the backend.
|
|
* @returns {Promise<Object[]>} Array of raw source objects, or empty on failure.
|
|
*/
|
|
|
|
async function loadCameraSources() {
|
|
try {
|
|
const signal = layerState._sourceAbort?.signal;
|
|
const data = await source.getCatalog({ signal });
|
|
signal?.throwIfAborted();
|
|
if (!Array.isArray(data?.sources)) return [];
|
|
return data.sources;
|
|
} catch (error) {
|
|
if (error?.name === 'AbortError') throw error;
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Merges raw backend sources with seed data to produce the final camera catalog.
|
|
* Seeds provide fallback values for heading, FOV, range, etc. when not specified
|
|
* by the source. Each camera is passed through ensureCameraPose.
|
|
* @param {Object[]} rawSources - Raw source objects from the backend.
|
|
* @returns {Object[]} Array of fully-initialized camera objects.
|
|
*/
|
|
|
|
function buildCatalogFromSources(rawSources) {
|
|
const sources = Array.isArray(rawSources) ? rawSources : [];
|
|
if (!sources.length) return [];
|
|
|
|
const seeded = seedCatalog();
|
|
const seedById = new Map(seeded.map((camera) => [camera.id, camera]));
|
|
|
|
const catalog = [];
|
|
for (const source of sources) {
|
|
if (!source || typeof source !== 'object') continue;
|
|
const id = String(source.id || '').trim();
|
|
if (!id) continue;
|
|
const seed = seedById.get(id);
|
|
const cityId =
|
|
String(source.cityId || '').trim() ||
|
|
cityIdByName(source.city) ||
|
|
seed?.cityId ||
|
|
'';
|
|
const city = cityId && CITY_POIS[cityId] ? CITY_POIS[cityId] : null;
|
|
|
|
const lat = parts.model.safeNumber(source.lat, seed?.lat ?? NaN);
|
|
const lon = parts.model.safeNumber(source.lon, seed?.lon ?? NaN);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lon)) continue;
|
|
|
|
const sourceHeading = parts.model.safeNumber(source.headingDeg, NaN);
|
|
const headingDeg = parts.model.normalizeHeading(
|
|
Number.isFinite(sourceHeading)
|
|
? sourceHeading
|
|
: (seed?.headingDeg ?? parts.model.headingFromId(id)),
|
|
);
|
|
const fovDeg = parts.model.clamp(
|
|
parts.model.safeNumber(source.fovDeg, seed?.fovDeg ?? 74),
|
|
20,
|
|
125,
|
|
);
|
|
const rangeM = parts.model.clamp(
|
|
parts.model.safeNumber(source.rangeM, seed?.rangeM ?? 700),
|
|
220,
|
|
2200,
|
|
);
|
|
const mountHeightM = parts.model.clamp(
|
|
parts.model.safeNumber(source.mountHeightM, seed?.mountHeightM ?? 24),
|
|
6,
|
|
120,
|
|
);
|
|
const pitchDeg = parts.model.clamp(
|
|
parts.model.safeNumber(source.pitchDeg, seed?.pitchDeg ?? -17),
|
|
-55,
|
|
-2,
|
|
);
|
|
const groundElevationM = parts.model.safeNumber(
|
|
source.groundElevationM,
|
|
city?.groundElevation ?? seed?.groundElevationM ?? 0,
|
|
);
|
|
const feedType = parts.model.normalizeFeedType(
|
|
source.feedType || source.type || 'image',
|
|
);
|
|
const headingConfidence = String(
|
|
source.headingConfidence || (seed ? 'high' : 'low'),
|
|
).toLowerCase();
|
|
// CAL badge input (design §3b passthrough): hand-authored file/env source
|
|
// entries may carry poseSource:'curated'. Austin Open Data rows never set
|
|
// this — they stay RAW PRIOR until a human manually calibrates them.
|
|
const poseSource =
|
|
source.poseSource === 'curated' ? 'curated' : seed?.poseSource || null;
|
|
|
|
const camera = {
|
|
id,
|
|
name: String(source.name || seed?.name || id),
|
|
cityId,
|
|
city: String(source.city || city?.name || seed?.city || 'Global'),
|
|
provider: String(
|
|
source.provider || seed?.provider || 'Configured CCTV Source',
|
|
),
|
|
sourceKind: String(
|
|
source.sourceKind ||
|
|
source.kind ||
|
|
(source.url ? 'configured' : 'seed'),
|
|
).toLowerCase(),
|
|
feedType,
|
|
feedConfigured: typeof source.url === 'string' && !!source.url.trim(),
|
|
lat,
|
|
lon,
|
|
headingDeg,
|
|
headingConfidence,
|
|
fovDeg,
|
|
rangeM,
|
|
mountHeightM,
|
|
groundElevationM,
|
|
absoluteHeightM: groundElevationM + mountHeightM,
|
|
pitchDeg,
|
|
license: String(source.license || source.licenseNote || ''),
|
|
poseSource,
|
|
};
|
|
parts.model.ensureCameraPose(camera);
|
|
catalog.push(camera);
|
|
}
|
|
|
|
return catalog;
|
|
}
|
|
return {
|
|
seedCatalog,
|
|
cityIdByName,
|
|
loadCameraSources,
|
|
buildCatalogFromSources,
|
|
};
|
|
}
|