import 'package:sqflite/sqflite.dart'; // import './state_helper.dart'; // import './db_helper.dart'; // TODO: get access to DB class DbHelper { static late Database db; static String dbName = ""; // DbHelper({ // required this.db, // required this.dbName, // }); // remnant from when DB was not static // factory DbHelper._openDb(Database database, String dbName) => DbHelper( // db: database, // dbName: dbName, // ); static void initDb(String name) async { // var db = await openDatabase('/var/db/oeffimaster/transport_db.db'); var databasesPath = await getDatabasesPath(); var path = databasesPath + name; // var newDb = await openDatabase(path); // var dbHelper = DbHelper._openDb(newDb, dbName); db = await openDatabase(path); dbName = name; // TODO: Make sure the directory exists // TODO: future optimisation: batch transactions Route.initRouteTable(); Segment.initSegmentTable(); Station.initStationTable(); Vehicle.initVehicleTable(); Departure.initDepartureTable(); } } /// this class covers two database tables: "route" and "routesegments" /// while route will for now only hold an id, routesegments will store the /// m-n/ 1-n relations between routes and segments - the segments contain the most /// relevant info, while routesegments stores which route the segments belong to /// and in which order they make up a route class Route { // TODO: add last_viewed, last_modified and such int? id; String? startStation; String? endStation; List segments = List.empty(); Duration duration = Duration(minutes: 0); Route({ this.id, this.startStation, this.endStation, required this.segments, required this.duration, }); /// toMap for Route but it's just route ids for now Map _toMapRoute() { return {"id": id, "startstation": startStation, "endstation": endStation}; } /// toMap for routesegments relational table List> _toMapRouteSegments() { //asMap() apparently enumerates List> tmp = List.empty(); for (final (segmentIdx, segment) in segments.indexed) { tmp.add({ "route": id, "segmentid": segment.id, "segmentorder": segmentIdx, }); } return tmp; } factory Route.empty(Station start) { return Route( startStation: start.name, segments: List.empty(), duration: Duration(minutes: 0), ); } factory Route.fromMap( Map routeMaps, List segmentMaps, ) { return Route( id: routeMaps["id"] as int, startStation: routeMaps["startstation"] as String?, endStation: routeMaps["endstation"] as String?, segments: segmentMaps, duration: Duration(minutes: 0), ); } // from trip API request // TODO: how to connect this best with the database for read/write? factory Route.fromJson(Map json) { Route newRoute = Route( segments: json["LegList"]["Leg"].map((leg) => Segment.fromJson(leg)), duration: json["LegList"]["Leg"][-1]["Origin"]["time"] - json["LegList"]["Leg"][0]["Origin"]["time"], ); newRoute._dbInsert(); return newRoute; } /// recalculate some technically redundant data for fast and easy access void sync() async { if (segments.isNotEmpty) { startStation = segments.first.startPoint.name; endStation = segments.last.endPoint.name; duration = segments.first.startTime.difference(segments.last.endTime); } else { // if the route has no segments yet, at least the startStation should remain, // otherwise delete the thing // having a startStation makes the first step faster at least // no need to put a mostly empty route into the db though endStation = null; duration = Duration(minutes: 0); } } void addSegment(Segment newSeg) { segments.add(newSeg); _dbInsertLastRouteSegment(); } void removeLastSegment() { _dbDeleteLastRouteSegment(); segments.removeLast(); } void removeSegmentsTillEnd(int idx) { segments.removeRange(idx, segments.length - 1); _dbDeleteTrailingRouteSegments(idx); } void _removeSegment(int idx) { segments.removeAt(idx); } void replaceSegment(int idx, Segment replacement) { if ((idx < segments.length - 1 && replacement._fitsBetween(segments[idx - 1], segments[idx + 1])) || replacement.startTime.isAfter(segments[idx - 1].endTime)) { segments[idx] = replacement; _dbUpdateRouteSegments(); } else if (idx >= segments.length) { addSegment(replacement); } else { throw "startTime for Segment does not fit"; //TODO: can do this better and maybe we should allow this } } static void initRouteTable() async { Database database = DbHelper.db; database.execute("""CREATE TABLE IF NOT EXISTS route( id INTEGER PRIMARY KEY AUTOINCREMENT, startstation TEXT, endstation TEXT, FOREIGN KEY (startstation) REFERENCES station(name) FOREIGN KEY (endstation) REFERENCES station(name) ); """); } static void initRouteSegmentsTable() async { Database database = DbHelper.db; String segment = Segment.tableName; database.execute("""CREATE TABLE IF NOT EXISTS routesegments( id INTEGER PRIMARY KEY AUTOINCREMENT, route INTEGER, segment INTEGER, segmentorder INTEGER, -- TODO: implement sorting FOREIGN KEY(route) REFERENCES route(id), FOREIGN KEY(segment) REFERENCES $segment(id) ); """); } static Future dbGet(int id) async { Database database = DbHelper.db; Future> routeSegments = _dbGetRouteSegments(id); var result = await database.query( "route", where: "id = ?", whereArgs: [id], ); assert(result.length < 2, "found more than 1 route for id $id"); Route route = Route.fromMap(result.first, await routeSegments); return route; } static Future> _dbGetRouteSegments( int routeId, ) async { Database database = DbHelper.db; var tmp = await database.rawQuery(""" SELECT * FROM segments JOIN routesegments ON routesegments.segmentid = segments.id WHERE routesegments.route = $routeId ORDER BY routesegments.segmentorder ASC; """); return tmp.map((x) => Segment.fromMap(x)).toList(); } void _dbInsert() async { Database database = DbHelper.db; id = await database.insert( "route", _toMapRoute(), conflictAlgorithm: ConflictAlgorithm.replace, ); } void _dbInsertLastRouteSegment() async { Database database = DbHelper.db; var lastRouteSegment = _toMapRouteSegments().last; database.insert("routesegments", lastRouteSegment); } void _dbInsertRouteSegments() async { Database database = DbHelper.db; var routeSegments = _toMapRouteSegments(); for (var i = 0; i < routeSegments.length; i++) { database.insert( "routesegments", routeSegments[i], //TODO: does this work? ); } } void _dbUpdate() async { Database database = DbHelper.db; id = await database.update( "route", _toMapRoute(), where: "id = ?", whereArgs: [id], conflictAlgorithm: ConflictAlgorithm.replace, ); } void _dbUpdateRouteSegments() async { Database database = DbHelper.db; var routeSegments = _toMapRouteSegments(); for (var i = 0; i < routeSegments.length; i++) { database.update( "routesegments", routeSegments[i], where: "route = ? AND segmentoder = ?", whereArgs: [id, i], ); } } void dbDelete() async { Database database = DbHelper.db; int result = await database.delete( "route", where: "id = ?", whereArgs: [id], ); assert(result == 0); } void _dbDeleteRouteSegment(int idx) { Database database = DbHelper.db; database.delete( "routesegments", where: "route = ? AND segmentorder = ?", whereArgs: [id, idx], // TODO: test the index ); } void _dbDeleteLastRouteSegment() { Database database = DbHelper.db; database.delete( "routesegments", where: "route = ? AND segmentorder = ?", whereArgs: [id, segments.length - 1], // TODO: test the index ); } void _dbDeleteTrailingRouteSegments(int position) { Database database = DbHelper.db; if (position <= segments.length) { database.delete( "routesegments", where: "route = ? AND segmentorder >= ?", whereArgs: [id, position], ); } } // TODO: delete leading segments and decrement following segments } class Segment { static const String tableName = "segment"; int? id; Station startPoint; Station endPoint; DateTime startTime; DateTime endTime; Vehicle vehicle; Segment({ this.id, // might throw errors - needs testing, I guess required this.startPoint, required this.endPoint, required this.startTime, required this.endTime, required this.vehicle, }); Map toMap() { return { "startpoint": startPoint.id, "endpoint": endPoint.id, "starttime": startTime, "endtime": endTime, "vehicle": vehicle.id, }; } factory Segment.fromMap(Map map) { return Segment( id: map["id"] as int, startPoint: map["startPoint"] as Station, endPoint: map["endPoint"] as Station, startTime: map["startTime"] as DateTime, endTime: map["endTime"] as DateTime, vehicle: map["vehicle"] as Vehicle, ); } factory Segment.fromJson(Map json) { Segment tmp = Segment( startPoint: Station.fromJson(json["Origin"]), endPoint: Station.fromJson(json["Destination"]), startTime: json["Origin"]["rtTime"], endTime: json["Destination"]["rtTime"], vehicle: Vehicle.fromJson(json["Product"]), ); tmp.dbInsert(); return tmp; } bool _fitsBetween(Segment prev, Segment next) { return startTime.isAfter(prev.endTime) && endTime.isBefore(next.startTime); } bool _sameEndpoints(Segment other) { return startPoint == other.startPoint && endPoint == other.endPoint; } static void initSegmentTable() async { Database database = DbHelper.db; String station = Station.tableName; String vehicle = Vehicle.tableName; database.execute(""" CREATE TABLE IF NOT EXISTS $tableName( startpoint TEXT, endpoint TEXT, starttime TEXT, endtime TEXT, vehicle INTEGER, FOREIGN KEY(startpoint) REFERENCES $station(stationid), FOREIGN KEY(endpoint) REFERENCES $station(stationid), FOREIGN KEY(vehicle) REFERENCES $vehicle(vehicleid) ); """); } static Future dbGet(int id) async { Database database = DbHelper.db; var tmp = await database.query(tableName, where: "id = ?", whereArgs: [id]); var seg = tmp.map((x) => Segment.fromMap(x)); //since we are querying for the primary key, this list should never be > 1 assert(seg.length < 2, "found more than one segment for id $id"); return seg.first; } static Future> dbGetAll() async { Database database = DbHelper.db; var seglist = await database.query(tableName); return seglist.map((seg) => Segment.fromMap(seg)).toList(); } void dbInsert() async { Database database = DbHelper.db; id = await database.insert(tableName, toMap()); } void dbUpdate() async { Database database = DbHelper.db; id = await database.update( tableName, toMap(), where: "rowid =?", whereArgs: [id], ); } void dbDelete() { Database database = DbHelper.db; database.delete( tableName, where: "rowid = ?", whereArgs: [id], ); } } class Station { static const String tableName = "station"; String id; String name; List transportLines = List.empty(); // could be normalized away Station({ required this.id, required this.name, required this.transportLines, }); Map _toMap() { return {"stationid": id, "name": name}; } List> _toMapLines() { return transportLines .map( (line) => {"stationid": id, "lineid": line}, ) .toList(); } //TODO: usage factory Station.fromMap( Map map, { List? stationLines, }) { return Station( id: map["stationid"] as String, name: map["name"] as String, transportLines: stationLines ?? List.empty(), ); } factory Station.fromJson(Map json) => Station( id: json["id"] as String, name: json["name"] as String, transportLines: json.containsKey("productAtStop") && json["productAtStop"] != Null ? json["productAtStop"] .map( (jsonLine) => //jsonLine != Null /*?*/ Line.fromJson(jsonLine! as Map), //: Null, ) .toList() .cast() : [], ); static void initStationTable() async { Database database = DbHelper.db; database.execute("""CREATE TABLE IF NOT EXISTS station( stationid TEXT PRIMARY KEY, name TEXT ); """); } static void initStationLinesTable() async { Database database = DbHelper.db; database.execute("""CREATE TABLE IF NOT EXISTS stationlines( id INTEGER PRIMARY KEY AUTOINCREMENT, stationid TEXT, lineid TEXT, FOREIGN KEY (stationid) REFERENCES station(stationid), FOREIGN KEY (lineid) REFERENCES line(lineid) ); """); } static Future dbGet(String stationId) async { Database database = DbHelper.db; var stationLines = _dbGetStationLines(stationId); var stationMap = await database.query( tableName, where: "stationid = ?", whereArgs: [stationId], ); var station = Station.fromMap(stationMap.first); station.transportLines = await stationLines; return station; } static Future> _dbGetStationLines(String stationId) async { Database database = DbHelper.db; var lines = await database.rawQuery(""" SELECT * FROM lines JOIN stationlines sl ON sl.lineid = line.id WHERE stationlines.stationid = $stationId ORDER BY line.id ASC; """); return lines.map((x) => Line.fromMap(x)).toList(); } void dbInsert() async { Database database = DbHelper.db; database.insert(tableName, _toMap()); } void _dbInsertStationLines() async { Database database = DbHelper.db; var stationLines = _toMapLines(); for (var i = 0; i < stationLines.length; i++) { database.insert("stationlines", stationLines[i]); } } void dbUpdate() async { Database database = DbHelper.db; database.update(tableName, _toMap(), where: "id = ?", whereArgs: [id]); } void dbUpdateStationLines() async { Database database = DbHelper.db; var stationLines = _toMapLines(); for (var i = 0; i < stationLines.length; i++) { database.update( "stationlines", stationLines[i], where: "stationid = ? AND lineid = ?", whereArgs: [id, stationLines[i]["lineid"]], ); } } void dbDelete() async { Database database = DbHelper.db; database.delete(tableName, where: "stationid = ?", whereArgs: [id]); } /// remove all Lines connected to a Station - should be used together with some kind of reparsing. void _dbDeleteStationLines() async { Database database = DbHelper.db; database.delete("stationlines", where: "stationid = ?", whereArgs: [id]); } } class Vehicle { static const String tableName = "vehicle"; String id; String name; String vehicleLineId; Vehicle({ required this.id, required this.name, required this.vehicleLineId, }); Map _toMap() { return { "id": id, "name": name, "line": vehicleLineId, }; } factory Vehicle.fromMap( Map map, ) { return Vehicle( id: map["id"] as String, name: map["name"] as String, vehicleLineId: map["line"] as String, ); } // we can use the "product" class from within any JSON response // (within hafas API, on the appropriate level) factory Vehicle.fromJson(Map json) => Vehicle( id: json["num"], name: json["name"], vehicleLineId: Line.fromJson(json).lineId, ); static void initVehicleTable() async { Database database = DbHelper.db; String line = Line.tableName; database.execute("""CREATE TABLE IF NOT EXISTS $tableName( id TEXT PRIMARY KEY, name TEXT, line TEXT, FOREIGN KEY(line) REFERENCES $line(lineid) ); """); } static Future dbGet(String vehicleId) async { Database database = DbHelper.db; var vehicleMap = await database.query( tableName, where: "id = ?", whereArgs: [vehicleId], ); return Vehicle.fromMap(vehicleMap.first); } void dbInsert() async { Database database = DbHelper.db; database.insert(tableName, _toMap()); } void dbUpdate() async { Database database = DbHelper.db; database.update(tableName, _toMap(), where: "id=?", whereArgs: [id]); } void dbDelete() async { Database database = DbHelper.db; database.delete(tableName, where: "id = ?", whereArgs: [id]); } } class Line { static const String tableName = "line"; String lineId; String? destination; String? direction; VehicleType mode; String name; Color? fgColor; Color? bgColor; Line({ required this.lineId, this.destination, this.direction, required this.name, required this.mode, this.fgColor, this.bgColor, }); Map _toMap() { return { "lineid": lineId, "destination": destination, "direction": direction, "mode": mode, "name": name, "fgcolor": fgColor, "bgcolor": bgColor, }; } factory Line.fromMap(Map map) { return Line( lineId: map["lineId"] as String, destination: map["destination"] as String, direction: map["direction"] as String, mode: map["mode"] as VehicleType, name: map["name"] as String, fgColor: map["fgcolor"] as Color, bgColor: map["bgcolor"] as Color, ); } // TODO: EXPLAIN!!!!!!!!!!!! static const _vehicleTypeAsCatCode = [ VehicleType.metro, VehicleType.subway, VehicleType.PLACEHOLDER, VehicleType.bus, ]; factory Line.fromJson(Map json) => Line( lineId: json["lineId"] as String, name: json["name"] as String, mode: _vehicleTypeAsCatCode[int.parse(json["catCode"] as String)], fgColor: Color.fromJson(json["icon"]["foregroundColor"]) as Color? ?? Color(r: 255, g: 255, b: 255), // fallback bgColor: Color.fromJson(json["icon"]["backgroundColor"]) as Color? ?? Color(r: 50, g: 50, b: 50), // fallback ); static void initLineTable() async { Database database = DbHelper.db; database.execute("""CREATE TABLE IF NOT EXISTS $tableName( lineid TEXT PRIMARY KEY, destination TEXT, direction TEXT, mode TEXT, name TEXT, fgcolor TEXT, bgcolor TEXT ); """); } static Future dbGet(String lineId) async { Database database = DbHelper.db; var tmp = await database.query( tableName, where: "lineid = ?", whereArgs: [lineId], ); return Line.fromMap(tmp.first); } void dbInsert() async { Database database = DbHelper.db; database.insert(tableName, _toMap()); } void dbUpdate() async { Database database = DbHelper.db; database.update( tableName, _toMap(), where: "lineid = ?", whereArgs: [lineId], ); } void dbDelete() async { Database database = DbHelper.db; database.delete(tableName, where: "lineid = ?", whereArgs: [lineId]); } } class Departure { static const String tableName = "departure"; int? id; String vehicleId; String vehicleName; String stationId; DateTime departureTime; Departure({ this.id, required this.vehicleId, required this.vehicleName, required this.stationId, required this.departureTime, }); Map _toMap() { return { "id": id, "vehicle": vehicleId, "vehiclename": vehicleName, "station": stationId, "departuretime": departureTime, }; } factory Departure.fromMap(Map map) { return Departure( id: map["id"] as int, vehicleId: map["vehicle"] as String, vehicleName: map["vehiclename"] as String, stationId: map["station"] as String, departureTime: map["departuretime"] as DateTime, ); } factory Departure.fromJson(Map json) { Vehicle vehicle = Vehicle.fromJson(json["Product"][0]); // try { // vehicle.dbInsert(); // } catch (e) { // vehicle.dbUpdate(); // } Station station = Station( id: json["stopid"], name: json["stop"], transportLines: [], ); // try { // station.dbInsert(); // } catch (e) { // station.dbUpdate(); // } return Departure( vehicleId: vehicle.id, vehicleName: vehicle.name, stationId: station.id, departureTime: DateTime.parse('${json["date"]}T${json["time"]}'), ); } static void initDepartureTable() async { Database database = DbHelper.db; String vehicle = Vehicle.tableName; String station = Station.tableName; database.execute("""CREATE TABLE IF NOT EXISTS $tableName( id INTEGER, vehicle TEXT, station TEXT, departuretime TEXT, FOREIGN KEY(vehicle) REFERENCES $vehicle(id), FOREIGN KEY(station) REFERENCES $station(id) ); """); } static Future> dbGetByVehicle( String vehicleId, { DateTime? intervalStart, }) async { Database database = DbHelper.db; intervalStart ??= DateTime.now(); // assign if null int limit = 50; List results = await database.query( tableName, where: "vehicle= ? AND departuretime > ?", whereArgs: [ vehicleId, intervalStart, ], orderBy: "departuretime ASC", limit: limit, ); return results.map((dep) => Departure.fromMap(dep)).toList(); } static Future> dbGetByStation( String stationId, { DateTime? intervalStart, }) async { Database database = DbHelper.db; intervalStart ??= DateTime.now(); // assign if null int limit = 50; List results = await database.query( tableName, where: "station = ? AND departuretime > ?", whereArgs: [stationId, intervalStart], orderBy: "departuretime", limit: limit, ); return results.map((dep) => Departure.fromMap(dep)).toList(); } static Future dbGet(int id) async { Database database = DbHelper.db; var depMap = await database.query( tableName, where: "id = ?", whereArgs: [id], ); return Departure.fromMap(depMap.first); } void dbInsert() async { Database database = DbHelper.db; id = await database.insert(tableName, _toMap()); } void dbUpdate() async { Database database = DbHelper.db; id = await database.update( tableName, _toMap(), where: "id = ?", whereArgs: [id], ); } void dbDelete() async { Database database = DbHelper.db; database.delete(tableName, where: "id = ?", whereArgs: [id]); } } class Color { int r; int g; int b; Color({ required this.r, required this.g, required this.b, }); factory Color.fromJson(Map json) => Color( r: json["r"], g: json["g"], b: json["b"], ); } enum VehicleType { bus, tram, metro, subway, regionalTrain, PLACEHOLDER, }