Files
oeffimaster/lib/transport_helper.dart
T
fbachus 32d8473086 feat(transport_helper): add conflictAlgorithm to db inserts
this way we can try to persist data at liberty without risking failure
right now, redundant entries will be ignored and updates have to happen
with update
nothing calls dbUpdate yet
2026-07-22 02:59:14 +02:00

974 lines
25 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 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<Segment> 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<String, Object?> _toMapRoute() {
return {"id": id, "startstation": startStation, "endstation": endStation};
}
/// 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.empty(Station start) {
return Route(
startStation: start.name,
segments: List.empty(),
duration: Duration(minutes: 0),
);
}
factory Route.fromMap(
Map<String, Object?> routeMaps,
List<Segment> 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<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;
}
/// 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<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 _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";
// TODO: add attribute for departures on the way?
int? id;
String ref;
Station startPoint;
Station endPoint;
DateTime startTime;
DateTime endTime;
Vehicle vehicle;
Segment({
this.id, // might throw errors - needs testing, I guess
required this.ref,
required this.startPoint,
required this.endPoint,
required this.startTime,
required this.endTime,
required this.vehicle,
});
Map<String, Object?> toMap() {
return {
"ref": ref,
"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,
ref: map["ref"] as String,
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) {
Segment tmp = Segment(
ref: json["JourneyDetail"]["ref"],
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(
id INTEGER PRIMARY KEY AUTOINCREMENT,
ref TEXT,
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(),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
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<Line> transportLines = List.empty(); // could be normalized away
Station({
required this.id,
required this.name,
required this.transportLines,
});
Map<String, Object?> _toMap() {
return {"stationid": id, "name": name};
}
List<Map<String, Object?>> _toMapLines() {
return transportLines
.map(
(line) => {"stationid": id, "lineid": line},
)
.toList();
}
//TODO: usage
factory Station.fromMap(
Map<String, Object?> map, {
List<Line>? stationLines,
}) {
return Station(
id: map["stationid"] as String,
name: map["name"] as String,
transportLines: stationLines ?? List.empty(),
);
}
factory Station.fromJson(Map<String, dynamic> 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<String, dynamic>),
//: Null,
)
.toList()
.cast<Line>()
: [],
);
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(),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
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<String, Object?> _toMap() {
return {
"id": id,
"name": name,
"line": vehicleLineId,
};
}
factory Vehicle.fromMap(
Map<String, Object?> 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<String, dynamic> 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<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(),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
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<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"] 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<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(),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
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 arrivalTime;
DateTime departureTime;
Departure({
this.id,
required this.vehicleId,
required this.vehicleName,
required this.stationId,
required this.arrivalTime,
required this.departureTime,
});
Map<String, Object?> _toMap() {
return {
"id": id,
"vehicle": vehicleId,
"vehiclename": vehicleName,
"station": stationId,
"arrivaltime": arrivalTime,
"departuretime": departureTime,
};
}
factory Departure.fromMap(Map<String, Object?> map) {
return Departure(
id: map["id"] as int,
vehicleId: map["vehicle"] as String,
vehicleName: map["vehiclename"] as String,
stationId: map["station"] as String,
arrivalTime: map["arrivaltime"] as DateTime,
departureTime: map["departuretime"] as DateTime,
);
}
factory Departure.fromJson(Map<String, dynamic> 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();
// }
DateTime? arrDateTime;
DateTime? depDateTime;
String? arrivalDate = json["arrDate"];
String? arrivalTime = json["arrTime"];
if (arrivalDate != null && arrivalTime != null) {
arrDateTime = DateTime.parse('${arrivalDate}T$arrivalTime');
}
String? departureDate = json["depDate"] ?? json["date"];
String? departureTime = json["depTime"] ?? json["time"];
if (departureDate != null && departureTime != null) {
depDateTime = DateTime.parse('${departureDate}T$departureTime');
}
return Departure(
vehicleId: vehicle.id,
vehicleName: vehicle.name,
stationId: station.id,
arrivalTime:
arrDateTime ??
depDateTime as DateTime, // one of these has to be in the json
departureTime:
depDateTime ??
arrDateTime as DateTime, // one of these has to be in the json
);
}
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(),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
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<String, dynamic> json) => Color(
r: json["r"],
g: json["g"],
b: json["b"],
);
}
enum VehicleType {
bus,
tram,
metro,
subway,
regionalTrain,
PLACEHOLDER,
}