111 lines
4.4 KiB
JavaScript
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));
|
|
}
|