// 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 { 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 { // 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; }