Files
NODEDC_PLATFORM/services/gelios-gateway/src/data-products.mjs
T

81 lines
2.8 KiB
JavaScript

export const FLEET_POSITIONS_CURRENT = Object.freeze({
id: "fleet.positions.current.v1",
version: "1.0.0",
delivery: "snapshot+patch",
semanticTypes: Object.freeze(["map.moving_object", "geo.position"]),
});
export function toFleetPositionsSnapshot(rows, { tenantId, connectionId, generatedAt = new Date(), staleAfterMs }) {
return {
schemaVersion: "nodedc.data-product.snapshot.v1",
dataProduct: FLEET_POSITIONS_CURRENT,
scope: { tenantId, connectionId },
generatedAt: iso(generatedAt),
entities: rows.map((row) => toFleetPositionEntity(row, { generatedAt, staleAfterMs })),
};
}
export function toFleetPositionPatch(row, { generatedAt = new Date(), staleAfterMs }) {
return {
schemaVersion: "nodedc.data-product.patch.v1",
dataProductId: FLEET_POSITIONS_CURRENT.id,
operation: "upsert",
emittedAt: iso(generatedAt),
entity: toFleetPositionEntity(row, { generatedAt, staleAfterMs }),
};
}
export function toFleetPositionEntity(row, { generatedAt, staleAfterMs }) {
const observedAt = date(row.observedAt);
const receivedAt = date(row.receivedAt);
const latitude = finiteNumber(row.latitude);
const longitude = finiteNumber(row.longitude);
const hasPosition = latitude !== null && longitude !== null;
const ageMs = observedAt ? Math.max(0, generatedAt.getTime() - observedAt.getTime()) : null;
const status = !hasPosition ? "no-position" : ageMs !== null && ageMs > staleAfterMs ? "stale" : "active";
return {
subjectId: String(row.subjectId),
semanticType: "map.moving_object",
label: String(row.name || row.subjectId),
status,
observedAt: observedAt ? observedAt.toISOString() : null,
receivedAt: receivedAt ? receivedAt.toISOString() : null,
position: hasPosition ? { latitude, longitude } : null,
motion: {
speed: finiteNumber(row.speed),
course: finiteNumber(row.course),
},
quality: {
gpsValid: booleanOrNull(row.gpsValid),
satellites: finiteNumber(row.satellites),
hdop: finiteNumber(row.hdop),
accuracyM: finiteNumber(row.accuracyM),
},
};
}
function iso(value) {
const parsed = date(value);
if (!parsed) throw new Error("data_product_timestamp_invalid");
return parsed.toISOString();
}
function date(value) {
if (value instanceof Date && !Number.isNaN(value.getTime())) return value;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
}
function finiteNumber(value) {
if (value === null || value === undefined || value === "") return null;
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function booleanOrNull(value) {
if (value === true || value === 1 || value === "1" || value === "true") return true;
if (value === false || value === 0 || value === "0" || value === "false") return false;
return null;
}