10 Commits
8 changed files with 230 additions and 83 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="oeffimaster" android:label="oeffimaster"
android:name="${applicationName}" android:name="${applicationName}"
@@ -42,4 +43,4 @@
<data android:mimeType="text/plain"/> <data android:mimeType="text/plain"/>
</intent> </intent>
</queries> </queries>
</manifest> </manifest>
+64
View File
@@ -0,0 +1,64 @@
erDiagram
journey||--|{routesegments: "consists of"
routesegments||--||segment: "sorts"
segment||--|{departure: "consists of"
departure}|--|{station: "originates from"
departure}|--||vehicle: uses
vehicle||..|{line: "shared data"
segment}|--||vehicle: uses
stationlines}|..||station: "general connections"
stationlines}|..||line: groups
journey {
int id PK
string startstation
string endstation
}
routesegments {
int id PK
int journey FK
int segment FK
int segmentorder
}
segment {
int id PK
string ref
int startid FK
int endid FK
string vehicleid FK
}
departure {
int id PK
string ref
string direction
string vehicle FK
string vehiclename
string station FK
string stationname
string arrivaltime
string departuretime
}
station {
string id PK
string name
}
stationlines {
int id PK
string stationid FK
string lineid FK
}
vehicle {
string vehicleid PK
string name
string line FK
}
line {
string lineid PK
string name
string mode
string destination
string direction
string fgcolor
string bgcolor
}
+8
View File
@@ -0,0 +1,8 @@
flowchart TD
Start-->stat[Search Station]-->selstat[Select station]
selstat-->seldep["`Select departure:
vehicle, direction & time`"]
seldep-->selarr["`Select arrival station:
when & where to exit`"] --> sw{switch?} --yes-->seldep
sw--"no"-->fin([journey details])
+19
View File
@@ -0,0 +1,19 @@
sequenceDiagram
participant api@{ "type": "entity"} as public transport API
participant ah as api_handler
participant ui as UI
participant th as transport_helper
participant db@{ "type":"database"} as database
ui-->>ah: find Station
ah-->>api: GET request w/ stationname
api-->>ah: return json
ah-->>th: provide json Map
th-->>db: store object
db-->>th: return id
th-->>ah: return object
ah-->>ui: display data
+10 -10
View File
@@ -1,10 +1,10 @@
- running mobile app - [x] running mobile app
- users should be able to - [ ] users should be able to
- plan public transport in the VBB transit network - [~] plan public transport in the VBB transit network
- create and customise routes quickly and on-the-fly - [~] create and customise routes quickly and on-the-fly
- edit routes from anywhere, where respective route information can be seen - [ ] edit routes from anywhere, where respective route information can be seen
- compare routes easily, especially between different endpoints - [ ] compare routes easily, especially between different endpoints
- the app should offer up the information most relevant to current route planning - [~] the app should offer up the information most relevant to current route planning
- between route steps, there should be no more than 3 taps required, optimally even just one(1), scrolling excluded - [x] between route steps, there should be no more than 3 taps required, optimally even just one(1), scrolling excluded
- e.g. only lines that connect to a station should be shown - [x] e.g. only lines that connect to a station should be shown
- stations that a line goes through should be available as next node/ step for planning - [x] stations that a line goes through should be available as next node/ step for planning
+12 -9
View File
@@ -6,19 +6,19 @@ import 'dart:convert';
import 'transport_helper.dart'; import 'transport_helper.dart';
String timeOf(DateTime fullDT) {
// doing it this way so we always get two digits on the hour
String full = fullDT.toString();
List<String> splits = full.split(" ")[1].split(":");
String time = '${splits[0]}:${splits[1]}';
return time;
}
class ApiHandler { class ApiHandler {
// TODO: Split next line for better Error Handling // TODO: Split next line for better Error Handling
static final String baseUrl = dotenv.get("VBB_API_URL"); static final String baseUrl = dotenv.get("VBB_API_URL");
static final String accessID = dotenv.get("VBB_API_KEY"); static final String accessID = dotenv.get("VBB_API_KEY");
// TODO: function calls for the following API calls
// - [x] location.name: findStations
// - [ ] location.nearbystops
// - [ ] location.details
// - [ ] departureBoard
// - [ ] multiDepartureBoard
// - [ ] journeyDetails
//static Future<Map<String, dynamic>> findStations(String locationName) async { //static Future<Map<String, dynamic>> findStations(String locationName) async {
static Future<List<Station>> findStations( static Future<List<Station>> findStations(
String locationName, { String locationName, {
@@ -79,6 +79,7 @@ class ApiHandler {
static Future<List<Departure>> departures( static Future<List<Departure>> departures(
String stationId, { String stationId, {
int? maxDepartures = -1, int? maxDepartures = -1,
DateTime? time,
int? duration = 0, int? duration = 0,
}) async { }) async {
String urlPath = '/api/fahrinfo/latest/departureBoard'; String urlPath = '/api/fahrinfo/latest/departureBoard';
@@ -87,7 +88,9 @@ class ApiHandler {
Uri url = Uri.https(baseUrl, urlPath, { Uri url = Uri.https(baseUrl, urlPath, {
'accessId': accessID, 'accessId': accessID,
'id': stationString, 'id': stationString,
////'time': /current time //time from which departures will be shown 'time': timeOf(
time ?? DateTime.now(),
), //time from which departures will be shown
'duration': duration.toString(), //interval time in minutes 'duration': duration.toString(), //interval time in minutes
'maxJourneys': maxDepartures.toString(), 'maxJourneys': maxDepartures.toString(),
//'type': 'S', // Station/Stops only //'type': 'S', // Station/Stops only
+72 -23
View File
@@ -4,12 +4,12 @@ import 'api_handler.dart';
import 'transport_helper.dart'; import 'transport_helper.dart';
import 'dart:async'; import 'dart:async';
import 'package:sqflite_common_ffi/sqflite_ffi.dart'; //for testing on desktop // import 'package:sqflite_common_ffi/sqflite_ffi.dart'; //for testing on desktop
void main() async { void main() async {
await dotenv.load(); await dotenv.load();
const String dbName = "oeffimasterTransportDb"; const String dbName = "oeffimasterTransportDb";
databaseFactory = databaseFactoryFfi; // for debugging and desktop test // databaseFactory = databaseFactoryFfi; // for debugging and desktop test
DbHelper.initDb(dbName); DbHelper.initDb(dbName);
runApp(const MyApp()); runApp(const MyApp());
} }
@@ -273,9 +273,11 @@ class StationList extends StatelessWidget {
class ChooseDeparturePage extends StatefulWidget { class ChooseDeparturePage extends StatefulWidget {
final Station startStation; final Station startStation;
final int? journeyId; final int? journeyId;
final Departure? arrival;
const ChooseDeparturePage({ const ChooseDeparturePage({
super.key, super.key,
this.journeyId, this.journeyId,
this.arrival,
required this.startStation, required this.startStation,
}); });
@@ -288,7 +290,10 @@ class _ChooseDeparturePageState extends State<ChooseDeparturePage> {
Journey? singleJourney; Journey? singleJourney;
void loadDepartures(Station startStation) async { void loadDepartures(Station startStation) async {
departureList = await ApiHandler.departures(startStation.id); departureList = await ApiHandler.departures(
startStation.id,
time: widget.arrival?.arrivalTime ?? DateTime.now(),
);
setState(() {}); setState(() {});
} }
@@ -330,17 +335,26 @@ class _ChooseDeparturePageState extends State<ChooseDeparturePage> {
), ),
), ),
), ),
ElevatedButton( // ElevatedButton(
onPressed: () { // onPressed: () {
if (widget.journeyId != null) { // if (widget.journeyId != null) {
loadJourney(widget.journeyId as int); // loadJourney(widget.journeyId as int);
} // }
}, // },
child: const Text('Finish Route'), // child: const Text('Finish Route'),
), // ),
], ],
), ),
), ),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (widget.journeyId != null) {
loadJourney(widget.journeyId as int);
}
},
tooltip: 'Finish route',
child: const Icon(Icons.check),
),
); );
} }
@@ -470,6 +484,7 @@ class _ChooseArrivalPageState extends State<ChooseArrivalPage> {
builder: (context) => ChooseDeparturePage( builder: (context) => ChooseDeparturePage(
startStation: startStation, startStation: startStation,
journeyId: journeyId, journeyId: journeyId,
arrival: arrival,
), ),
), ),
); );
@@ -528,7 +543,31 @@ class _JourneyDetailPageState extends State<JourneyDetailPage> {
return Scaffold( return Scaffold(
appBar: AppBar(title: Text('Connection Details \n$duration')), appBar: AppBar(title: Text('Connection Details \n$duration')),
body: Center( body: Center(
child: SegmentList(journey: widget.singleJourney), child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: SizedBox(
height: 400,
child: SegmentList(journey: widget.singleJourney),
),
),
Container(
margin: const EdgeInsets.all(20.0),
child: ElevatedButton(
onPressed: () {
Navigator.pop(
context,
MaterialPageRoute(
builder: (context) => JourneyOverviewPage(),
),
);
},
child: const Text('View all Journeys'),
),
),
],
),
), ),
); );
} }
@@ -596,6 +635,16 @@ class _JourneyOverviewPageState extends State<JourneyOverviewPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container(
padding: EdgeInsets.only(left: 30, top: 50),
width: 500.0,
height: 100.0,
child: FittedBox(
fit: BoxFit.contain,
alignment: Alignment.centerLeft,
child: Text("Your Journeys"),
),
),
FutureBuilder( FutureBuilder(
future: loadJourneys(), future: loadJourneys(),
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -648,17 +697,17 @@ class _JourneyOverviewPageState extends State<JourneyOverviewPage> {
); );
}, },
), ),
ElevatedButton( // ElevatedButton(
onPressed: () { // onPressed: () {
Navigator.push( // Navigator.push(
context, // context,
MaterialPageRoute( // MaterialPageRoute(
builder: (context) => ChooseStationPage(), // builder: (context) => ChooseStationPage(),
), // ),
); // );
}, // },
child: const Text('Start a route'), // child: const Text('Start a route'),
), // ),
], ],
), ),
), ),
+43 -40
View File
@@ -41,11 +41,11 @@ class DbHelper {
} }
} }
/// this class covers two database tables: "route" and "routesegments" /// this class covers two database tables: "journey" and "routesegments"
/// while route will for now only hold an id, routesegments will store the /// while journey 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 /// m-n/ 1-n relations between journeys and segments - the segments contain the most
/// relevant info, while routesegments stores which route the segments belong to /// relevant info, while routesegments stores which journey the segments belong to
/// and in which order they make up a route /// and in which order they make up a journey
class Journey { class Journey {
static const String tableName = "journey"; static const String tableName = "journey";
// TODO: add last_viewed, last_modified and such // TODO: add last_viewed, last_modified and such
@@ -63,7 +63,7 @@ class Journey {
required this.duration, required this.duration,
}); });
/// toMap for Journey but it's just route ids for now /// toMap for Journey but it's just journey ids for now
Map<String, Object?> _toMapJourney() { Map<String, Object?> _toMapJourney() {
return {"id": id, "startstation": startStation, "endstation": endStation}; return {"id": id, "startstation": startStation, "endstation": endStation};
} }
@@ -74,7 +74,7 @@ class Journey {
List<Map<String, Object?>> tmp = []; List<Map<String, Object?>> tmp = [];
for (final (segmentIdx, segment) in segments.indexed) { for (final (segmentIdx, segment) in segments.indexed) {
tmp.add({ tmp.add({
"route": id, "journey": id,
"segment": segment.id, "segment": segment.id,
"segmentorder": segmentIdx, "segmentorder": segmentIdx,
}); });
@@ -91,13 +91,13 @@ class Journey {
} }
factory Journey.fromMap( factory Journey.fromMap(
Map<String, Object?> routeMaps, Map<String, Object?> journeyMaps,
List<Segment> segmentMaps, List<Segment> segmentMaps,
) { ) {
return Journey( return Journey(
id: routeMaps["id"] as int, id: journeyMaps["id"] as int,
startStation: routeMaps["startstation"] as String?, startStation: journeyMaps["startstation"] as String?,
endStation: routeMaps["endstation"] as String?, endStation: journeyMaps["endstation"] as String?,
segments: segmentMaps, segments: segmentMaps,
duration: Duration(minutes: 0), duration: Duration(minutes: 0),
); );
@@ -138,10 +138,10 @@ class Journey {
segments.last.end.arrivalTime, segments.last.end.arrivalTime,
); );
} else { } else {
// if the route has no segments yet, at least the startStation should remain, // if the journey has no segments yet, at least the startStation should remain,
// otherwise delete the thing // otherwise delete the thing
// having a startStation makes the first step faster at least // having a startStation makes the first step faster at least
// no need to put a mostly empty route into the db though // no need to put a mostly empty journey into the db though
endStation = null; endStation = null;
duration = Duration(minutes: 0); duration = Duration(minutes: 0);
} }
@@ -187,9 +187,7 @@ class Journey {
database.execute("""CREATE TABLE IF NOT EXISTS '$tableName'( database.execute("""CREATE TABLE IF NOT EXISTS '$tableName'(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
startstation TEXT, startstation TEXT,
endstation TEXT, endstation TEXT
FOREIGN KEY (startstation) REFERENCES station(name)
FOREIGN KEY (endstation) REFERENCES station(name)
); );
"""); """);
} }
@@ -199,7 +197,7 @@ class Journey {
String segment = Segment.tableName; String segment = Segment.tableName;
database.execute("""CREATE TABLE IF NOT EXISTS routesegments( database.execute("""CREATE TABLE IF NOT EXISTS routesegments(
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
route INTEGER, journey INTEGER,
segment INTEGER, segment INTEGER,
segmentorder INTEGER, -- TODO: implement sorting segmentorder INTEGER, -- TODO: implement sorting
FOREIGN KEY('$tableName') REFERENCES '$tableName'(id), FOREIGN KEY('$tableName') REFERENCES '$tableName'(id),
@@ -216,9 +214,9 @@ class Journey {
where: "id = ?", where: "id = ?",
whereArgs: [id], whereArgs: [id],
); );
assert(result.length < 2, "found more than 1 route for id $id"); assert(result.length < 2, "found more than 1 journey for id $id");
Journey route = Journey.fromMap(result.first, await routeSegments); Journey journey = Journey.fromMap(result.first, await routeSegments);
return route; return journey;
} }
/// gets routesegments and hands them to the Journey.fromMap factory, works async /// gets routesegments and hands them to the Journey.fromMap factory, works async
@@ -244,14 +242,14 @@ class Journey {
} }
static Future<List<Segment>> _dbGetJourneySegments( static Future<List<Segment>> _dbGetJourneySegments(
int routeId, int journeyId,
) async { ) async {
Database database = DbHelper.db; Database database = DbHelper.db;
String segment = Segment.tableName; String segment = Segment.tableName;
var tmp = await database.rawQuery(""" var tmp = await database.rawQuery("""
SELECT * FROM '$segment' SELECT * FROM '$segment'
JOIN routesegments ON routesegments.segment = '$segment'.id JOIN routesegments ON routesegments.segment = '$segment'.id
WHERE routesegments.route = '$routeId' WHERE routesegments.journey = '$journeyId'
ORDER BY routesegments.segmentorder ASC; ORDER BY routesegments.segmentorder ASC;
"""); """);
@@ -273,15 +271,15 @@ class Journey {
return id_tmp; return id_tmp;
} }
static void dbAppendJourneySegment(int routeId, int segmentid) { static void dbAppendJourneySegment(int journeyId, int segmentid) {
Database database = DbHelper.db; Database database = DbHelper.db;
// generate segmentorder to be 0 if no segment exists for route, otherwise // generate segmentorder to be 0 if no segment exists for journey, otherwise
// take highest segmentorder+1 for given route // take highest segmentorder+1 for given journey
database.rawInsert( database.rawInsert(
"""INSERT INTO routesegments(route, segment, segmentorder) VALUES """INSERT INTO routesegments(journey, segment, segmentorder) VALUES
($routeId, $segmentid, CASE WHEN EXISTS ( ($journeyId, $segmentid, CASE WHEN EXISTS (
SELECT segmentorder FROM routesegments r WHERE r.route=$routeId) THEN ( SELECT segmentorder FROM routesegments r WHERE r.journey=$journeyId) THEN (
SELECT segmentorder AS so FROM routesegments r WHERE r.route=$routeId SELECT segmentorder AS so FROM routesegments r WHERE r.journey=$journeyId
ORDER BY so DESC LIMIT 1) +1 ELSE 0 END); ORDER BY so DESC LIMIT 1) +1 ELSE 0 END);
""", """,
); );
@@ -322,7 +320,7 @@ class Journey {
database.update( database.update(
"routesegments", "routesegments",
routeSegments[i], routeSegments[i],
where: "route = ? AND segmentoder = ?", where: "journey = ? AND segmentoder = ?",
whereArgs: [id, i], whereArgs: [id, i],
); );
} }
@@ -342,7 +340,7 @@ class Journey {
Database database = DbHelper.db; Database database = DbHelper.db;
database.delete( database.delete(
"routesegments", "routesegments",
where: "route = ? AND segmentorder = ?", where: "journey = ? AND segmentorder = ?",
whereArgs: [id, idx], // TODO: test the index whereArgs: [id, idx], // TODO: test the index
); );
} }
@@ -351,7 +349,7 @@ class Journey {
Database database = DbHelper.db; Database database = DbHelper.db;
database.delete( database.delete(
"routesegments", "routesegments",
where: "route = ? AND segmentorder = ?", where: "journey = ? AND segmentorder = ?",
whereArgs: [id, segments.length - 1], // TODO: test the index whereArgs: [id, segments.length - 1], // TODO: test the index
); );
} }
@@ -361,7 +359,7 @@ class Journey {
if (position <= segments.length) { if (position <= segments.length) {
database.delete( database.delete(
"routesegments", "routesegments",
where: "route = ? AND segmentorder >= ?", where: "journey = ? AND segmentorder >= ?",
whereArgs: [id, position], whereArgs: [id, position],
); );
} }
@@ -493,7 +491,7 @@ class Segment {
FROM $tableName s FROM $tableName s
JOIN $departure start ON s.startid = start.id JOIN $departure start ON s.startid = start.id
JOIN $departure end ON s.endid = end.id JOIN $departure end ON s.endid = end.id
JOIN $vehicle v ON s.vehicleid = v.id JOIN $vehicle v ON s.vehicleid = v.vehicleid
where s.id = $id; where s.id = $id;
""", """,
); );
@@ -526,7 +524,7 @@ class Segment {
"departuretime": seg["end_departuretime"] as String, "departuretime": seg["end_departuretime"] as String,
}; };
Map<String, String> vehicleMap = { Map<String, String> vehicleMap = {
"id": seg["vehicleid"] as String, "vehicleid": seg["vehicleid"] as String,
"name": seg["vehiclename"] as String, "name": seg["vehiclename"] as String,
"line": seg["vehicleline"] as String, "line": seg["vehicleline"] as String,
}; };
@@ -735,7 +733,7 @@ class Vehicle {
Map<String, Object?> _toMap() { Map<String, Object?> _toMap() {
return { return {
"id": vehicleid, "vehicleid": vehicleid,
"name": name, "name": name,
"line": vehicleLineId, "line": vehicleLineId,
}; };
@@ -745,7 +743,7 @@ class Vehicle {
Map<String, Object?> map, Map<String, Object?> map,
) { ) {
return Vehicle( return Vehicle(
vehicleid: map["id"] as String, vehicleid: map["vehicleid"] as String,
name: map["name"] as String, name: map["name"] as String,
vehicleLineId: map["line"] as String, vehicleLineId: map["line"] as String,
); );
@@ -775,7 +773,7 @@ class Vehicle {
Database database = DbHelper.db; Database database = DbHelper.db;
var vehicleMap = await database.query( var vehicleMap = await database.query(
tableName, tableName,
where: "id = ?", where: "vehicleid = ?",
whereArgs: [vehicleId], whereArgs: [vehicleId],
); );
return Vehicle.fromMap(vehicleMap.first); return Vehicle.fromMap(vehicleMap.first);
@@ -792,7 +790,12 @@ class Vehicle {
void dbUpdate() async { void dbUpdate() async {
Database database = DbHelper.db; Database database = DbHelper.db;
database.update(tableName, _toMap(), where: "id=?", whereArgs: [vehicleid]); database.update(
tableName,
_toMap(),
where: "vehicleid=?",
whereArgs: [vehicleid],
);
} }
void dbDelete() async { void dbDelete() async {
@@ -1173,4 +1176,4 @@ enum VehicleType {
subway, subway,
regionalTrain, regionalTrain,
PLACEHOLDER, PLACEHOLDER,
} }