mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
refactor(cctv): separate camera source and scene components
This commit is contained in:
1 parent
ca5f5fff93
commit
519bc6c362
32 files changed
+6873
-4835
No files matched your search
@@ -0,0 +1,210 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user