81 lines
2.8 KiB
JavaScript
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;
|
|
}
|