BinghamtonBetterBus-v2/src/server/graph.ts
Pagwin f18509ff1d
Merge in server with some reverse proxy stuff
server is basically fully implemented
2025-04-02 15:37:36 -04:00

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