170 lines
4.7 KiB
TypeScript
170 lines
4.7 KiB
TypeScript
// Import Deno-compatible modules
|
|
import { parse as parseCSV } from "https://deno.land/std/csv/mod.ts";
|
|
import { Neo4j } from "https://deno.land/x/neo4j_lite_client/mod.ts";
|
|
|
|
// Define types
|
|
interface LocationNode {
|
|
id: number;
|
|
lat: number;
|
|
lng: number;
|
|
}
|
|
|
|
interface GTFSStop {
|
|
stop_id: string;
|
|
stop_name: string;
|
|
stop_lat: string;
|
|
stop_lon: string;
|
|
location_type?: string;
|
|
parent_station?: string;
|
|
zone_id?: string;
|
|
stop_url?: string;
|
|
[key: string]: string | undefined; // Allow for additional fields
|
|
}
|
|
|
|
/**
|
|
* Sets up a Neo4j graph database with location nodes from JSON and GTFS stop data
|
|
* @param driver Neo4j driver instance to use for database operations
|
|
* @param jsonFilePath Optional path to the JSON file containing location nodes (defaults to './data/locations.json')
|
|
* @param gtfsStopsPath Optional path to the GTFS stops.txt file (defaults to './data/gtfs/stops.txt')
|
|
*/
|
|
export async function graph_setup(
|
|
driver: Neo4j.Driver,
|
|
OCCT_stops_json: string = "./data/OCCT/stops.json",
|
|
BCT_GTFS_stops_txt: string = "./data/BCT/stops.txt",
|
|
): Promise<void> {
|
|
const session = driver.session();
|
|
|
|
const jsonData = await Deno.readTextFile(OCCT_stops_json);
|
|
const locationNodes: LocationNode[] = JSON.parse(jsonData);
|
|
|
|
await stops_json_node_import(session, locationNodes);
|
|
|
|
const BCTStopsData = await Deno.readTextFile(BCT_GTFS_stops_txt);
|
|
|
|
const BCT_stops = await parse_gtfs_stops(BCTStopsData);
|
|
|
|
await stops_gtfs_node_import(session, BCT_stops);
|
|
|
|
await session.close();
|
|
}
|
|
|
|
async function stops_json_node_import(
|
|
session: Neo4j.Session,
|
|
stops: LocationNode[],
|
|
) {
|
|
for (const node of stops) {
|
|
await session.run(
|
|
`
|
|
MERGE (n:TransportNode {id: $id})
|
|
ON CREATE SET
|
|
n.originalId = $originalId,
|
|
n.latitude = $lat,
|
|
n.longitude = $lng,
|
|
n.source = 'OCCT'
|
|
ON MATCH SET
|
|
n.originalId = $originalId,
|
|
n.latitude = $lat,
|
|
n.longitude = $lng,
|
|
n.source = 'OCCT'
|
|
`,
|
|
{
|
|
id: `OCCT_${node.id}`,
|
|
originalId: node.id,
|
|
lat: node.lat,
|
|
lng: node.lng,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
async function stops_gtfs_node_import(
|
|
session: Neo4j.Session,
|
|
stops: GTFSStop[],
|
|
) {
|
|
// Add GTFS stops to Neo4j
|
|
for (const stop of stops) {
|
|
// Make sure required fields exist
|
|
if (
|
|
!stop.stop_id || !stop.stop_name || !stop.stop_lat || !stop.stop_lon
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Use MERGE to update existing nodes or create new ones
|
|
await session.run(
|
|
`
|
|
MERGE (s:TransportNode {id: $id})
|
|
ON CREATE SET
|
|
s.name = $name,
|
|
s.latitude = $lat,
|
|
s.longitude = $lng,
|
|
s.locationType = $locationType,
|
|
s.parentStation = $parentStation,
|
|
s.zoneId = $zoneId,
|
|
s.url = $url,
|
|
s.source = 'BCT'
|
|
ON MATCH SET
|
|
s.name = $name,
|
|
s.latitude = $lat,
|
|
s.longitude = $lng,
|
|
s.locationType = $locationType,
|
|
s.parentStation = $parentStation,
|
|
s.zoneId = $zoneId,
|
|
s.url = $url,
|
|
s.source = 'BCT'
|
|
`,
|
|
{
|
|
id: "BCT_" + stop.stop_id,
|
|
name: stop.stop_name,
|
|
lat: parseFloat(stop.stop_lat),
|
|
lng: parseFloat(stop.stop_lon),
|
|
locationType: stop.location_type || null,
|
|
parentStation: stop.parent_station || null,
|
|
zoneId: stop.zone_id || null,
|
|
url: stop.stop_url || null,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
async function parse_gtfs_stops(txt: string): Promise<GTFSStop[]> {
|
|
// First, read the file content to manually determine headers
|
|
const lines = txt.split("\n");
|
|
const headers = lines[0].split(",").map((header) => header.trim());
|
|
|
|
// Parse CSV data using Deno's CSV parser with flexible parsing options
|
|
const gtfsStops = await parseCSV(txt, {
|
|
skipFirstRow: false, // We'll handle headers manually
|
|
columns: headers,
|
|
parse: (input: unknown[]) => {
|
|
// This function gets called for each row
|
|
// The first row contains headers, so we'll skip it
|
|
if (input[0] === headers[0]) {
|
|
return null; // Skip the header row
|
|
}
|
|
|
|
// Create a stop object with the right properties
|
|
const stop: GTFSStop = {
|
|
stop_id: "",
|
|
stop_name: "",
|
|
stop_lat: "",
|
|
stop_lon: "",
|
|
};
|
|
|
|
// Map the values to their corresponding headers
|
|
headers.forEach((header, index) => {
|
|
if (input[index] !== undefined) {
|
|
// @ts-ignore - We're dynamically setting properties
|
|
stop[header] = input[index];
|
|
}
|
|
});
|
|
|
|
return stop;
|
|
},
|
|
}) as GTFSStop[];
|
|
|
|
// Filter out any null values (like the header row we skipped)
|
|
const validStops = gtfsStops.filter((stop) => stop !== null);
|
|
|
|
return validStops;
|
|
}
|