Files
oeffimaster/lib/transport_helper.dart
T

718 lines
18 KiB
Dart

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 dbName) async {
// var db = await openDatabase('/var/db/oeffimaster/transport_db.db');
var databasesPath = await getDatabasesPath();
var path = databasesPath + dbName;
// var newDb = await openDatabase(path);
// var dbHelper = DbHelper._openDb(newDb, dbName);
db = await openDatabase(path);
dbName = dbName;
// 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;
List<Segment> segments = List.empty();
Duration duration = Duration(minutes: 0);
Route({
this.id,
required this.segments,
required this.duration,
});
/// toMap for Route but it's just route ids for now
Map<String, Object?> toMapRoute() {
return {"id": id};
}
/// toMap for routesegments relational table
List<Map<String, Object?>> toMapRouteSegments() {
//asMap() apparently enumerates
List<Map<String, Object?>> tmp = List.empty();
for (final (segmentIdx, segment) in segments.indexed) {
tmp.add({
"route": id,
"segmentid": segment.id,
"segmentorder": segmentIdx,
});
}
return tmp;
}
factory Route.fromMap(
Map<String, Object?> routeMaps,
List<Segment> segmentMaps,
) {
return Route(
id: routeMaps["id"] as int,
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<String, dynamic> 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;
}
static void initRouteTable() async {
Database database = DbHelper.db;
database.execute("""CREATE TABLE IF NOT EXISTS route(
id INTEGER PRIMARY KEY AUTOINCREMENT,
);
""");
}
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<Route> dbGet(int id) async {
Database database = DbHelper.db;
Future<List<Segment>> 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<List<Segment>> 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 dbInsertRouteSegments() async {
Database database = DbHelper.db;
var routeSegments = toMapRouteSegments();
for (var i = 0; i < routeSegments.length; i++) {
database.insert(
"routesegments",
routeSegments[i],
);
}
}
void dbDelete() async {
Database database = DbHelper.db;
int result = await database.delete(
"route",
where: "id = ?",
whereArgs: [id],
);
assert(result == 0);
}
void dbDeleteLastRouteSegment() {
Database database = DbHelper.db;
database.delete(
"routesegments",
where: "route = ? AND segmentorder = ?",
whereArgs: [id, segments.length - 1], // TODO: test the index
);
}
void dbDeleteTrailingRouteSegment(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<String, Object?> toMap() {
return {
"startpoint": startPoint.id,
"endpoint": endPoint.id,
"starttime": startTime,
"endtime": endTime,
"vehicle": vehicle.id,
};
}
factory Segment.fromMap(Map<String, Object?> 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<String, dynamic> json) {
Database database = DbHelper.db;
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;
}
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<Segment> 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<List<Segment>> 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 dbDelete() {
Database database = DbHelper.db;
database.delete(
tableName,
where: "rowid = ?",
whereArgs: [id],
);
}
}
class Station {
static const String tableName = "station";
String id;
String name;
List<Line> transportLines = List.empty(); // could be normalized away
Station({
required this.id,
required this.name,
required this.transportLines,
});
Map<String, Object?> toMap() {
return {"id": id, "name": name};
}
List<Map<String, Object?>> toMapLines() {
return transportLines
.map(
(line) => {"stationid": id, "lineid": line},
)
.toList();
}
factory Station.fromMap(
Map<String, Object?> map, {
List<Line>? stationLines,
}) {
return Station(
id: map["id"] as String,
name: map["name"] as String,
transportLines: stationLines ?? List.empty(),
);
}
factory Station.fromJson(Map<String, dynamic> json) => Station(
id: json["id"] ?? json["extId"] as String,
name: json["name"],
transportLines: json.containsKey("productAtStop")
? json["productAtStop"].map(
(jsonLine) => Line.fromJson(jsonLine as Map<String, dynamic>),
)
: const [],
);
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<Station> 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<List<Line>> 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 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 vehicleLineId;
Vehicle({
required this.id,
required this.vehicleLineId,
});
Map<String, Object?> toMap() {
return {
"id": id,
"line": vehicleLineId,
};
}
factory Vehicle.fromMap(
Map<String, Object?> map,
) {
return Vehicle(
id: map["id"] 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<String, dynamic> json) => Vehicle(
id: json["matchId"],
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,
line TEXT,
FOREIGN KEY(line) REFERENCES $line(lineid)
);
""");
}
static Future<Vehicle> 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 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<String, Object?> toMap() {
return {
"lineid": lineId,
"destination": destination,
"direction": direction,
"mode": mode,
"name": name,
"fgcolor": fgColor,
"bgcolor": bgColor,
};
}
factory Line.fromMap(Map<String, Object?> 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<String, dynamic> json) => Line(
lineId: json["lineId"],
name: json["name"],
mode: _vehicleTypeAsCatCode[int.parse(json["catCode"])],
fgColor:
Color.fromJson(json["foregroundColor"]) as Color? ??
Color(r: 255, g: 255, b: 255), // fallback
bgColor:
Color.fromJson(json["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<Line> 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 dbDelete() async {
Database database = DbHelper.db;
database.delete(tableName, where: "lineid = ?", whereArgs: [lineId]);
}
}
// TODO: add fromJson
class Departure {
static const String tableName = "departure";
int? id;
String vehicleId;
String stationId;
DateTime departureTime;
Departure({
this.id,
required this.vehicleId,
required this.stationId,
required this.departureTime,
});
Map<String, Object?> toMap() {
return {
"id": id,
"vehicle": vehicleId,
"station": stationId,
"departuretime": departureTime,
};
}
factory Departure.fromMap(Map<String, Object?> map) {
return Departure(
id: map["id"] as int,
vehicleId: map["vehicle"] as String,
stationId: map["station"] as String,
departureTime: map["departuretime"] as DateTime,
);
}
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<List<Departure>> 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<List<Departure>> 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<Departure> 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 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<String, dynamic> json) => Color(
r: json["r"],
g: json["g"],
b: json["b"],
);
}
enum VehicleType {
bus,
tram,
metro,
subway,
regionalTrain,
PLACEHOLDER,
}