NODEDC_PLATFORM/services/map-gateway/scripts/generate-reference-station-...

111 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const [inputPath, outputPath] = process.argv.slice(2);
if (!inputPath || !outputPath) {
console.error("usage: generate-reference-station-seed.mjs <input.geojson> <output.json>");
process.exit(2);
}
const source = JSON.parse(await readFile(resolve(inputPath), "utf8"));
if (source?.type !== "FeatureCollection" || !Array.isArray(source.features)) {
throw new Error("station_seed_feature_collection_required");
}
const generatedAt = isIso(source.timestamp) ? new Date(source.timestamp).toISOString() : new Date(0).toISOString();
const facts = source.features.map((feature, index) => normalizeFeature(feature, generatedAt, index));
facts.sort((left, right) => left.sourceId.localeCompare(right.sourceId));
const contentDigest = createHash("sha256")
.update(JSON.stringify(facts))
.digest("hex");
const counts = Object.fromEntries(["metro", "railway_station", "railway_terminal"].map((category) => [
category,
facts.filter((fact) => fact.attributes.category === category).length,
]));
const document = {
schemaVersion: "nodedc.map-reference-seed/v1",
profileId: "transport-stations.v1",
sourceRevision: `seed-${contentDigest.slice(0, 16)}`,
generatedAt,
contentDigest,
complete: true,
facts,
metadata: {
factCount: facts.length,
categoryCounts: counts,
},
};
await mkdir(dirname(resolve(outputPath)), { recursive: true });
await writeFile(resolve(outputPath), `${JSON.stringify(document)}\n`, "utf8");
console.log(JSON.stringify({ output: resolve(outputPath), ...document.metadata, contentDigest }));
function normalizeFeature(feature, generatedAt, index) {
if (feature?.type !== "Feature" || feature?.geometry?.type !== "Point") {
throw new Error(`station_seed_feature_${index}_invalid`);
}
const coordinates = feature.geometry.coordinates;
if (!Array.isArray(coordinates) || coordinates.length < 2) {
throw new Error(`station_seed_feature_${index}_coordinates_invalid`);
}
const longitude = Number(coordinates[0]);
const latitude = Number(coordinates[1]);
if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180
|| !Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
throw new Error(`station_seed_feature_${index}_coordinates_invalid`);
}
const properties = feature.properties && typeof feature.properties === "object" ? feature.properties : {};
const category = normalizeCategory(properties.sort);
const nativeId = String(feature.id || properties["@id"] || "").trim();
const identity = nativeId.match(/^(node|way|relation)\/([1-9]\d*)$/);
if (!identity) throw new Error(`station_seed_feature_${index}_identity_invalid`);
const attributes = compact({
name: firstString(properties.name, properties["name:ru"], properties.official_name, properties.loc_name),
category,
network: optionalString(properties.network),
operator: optionalString(properties.operator),
official_name: optionalString(properties.official_name),
local_name: optionalString(properties.loc_name),
uic_ref: optionalString(properties.uic_ref),
wheelchair: optionalString(properties.wheelchair),
});
if (!attributes.name) throw new Error(`station_seed_feature_${index}_name_required`);
return {
sourceId: `osm.${identity[1]}.${identity[2]}`,
semanticType: category === "railway_terminal" ? "map.terminal" : "map.station",
observedAt: generatedAt,
receivedAt: generatedAt,
attributes,
geometry: { type: "Point", coordinates: [longitude, latitude] },
presentationStatus: "active",
};
}
function normalizeCategory(value) {
if (value === "metro") return "metro";
if (value === "terminal") return "railway_terminal";
if (value === "rzd_station") return "railway_station";
throw new Error("station_seed_category_invalid");
}
function firstString(...values) {
return values.map(optionalString).find(Boolean);
}
function optionalString(value) {
if (typeof value !== "string") return undefined;
const normalized = value.trim();
return normalized && normalized.length <= 256 ? normalized : undefined;
}
function compact(value) {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
}
function isIso(value) {
return typeof value === "string" && !Number.isNaN(Date.parse(value));
}