You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1067 lines
30 KiB
1067 lines
30 KiB
import 'dart:async'; |
|
import 'dart:convert'; |
|
|
|
import 'package:flutter/widgets.dart'; |
|
import 'package:info_tren/hidden_webview.dart'; |
|
import 'package:info_tren/utils/string.dart'; |
|
import 'package:info_tren/utils/webview_invoke.dart'; |
|
import 'package:json_annotation/json_annotation.dart'; |
|
import 'package:webview_flutter/webview_flutter.dart'; |
|
|
|
part 'train_data.g.dart'; |
|
|
|
enum TrainLookupResult { |
|
FOUND, |
|
NOT_FOUND, |
|
OTHER |
|
} |
|
|
|
class OnDemandInvalidatedException implements Exception { |
|
final String propertyName; |
|
final OnDemand onDemandClass; |
|
|
|
OnDemandInvalidatedException({this.propertyName, this.onDemandClass}); |
|
|
|
@override |
|
String toString() { |
|
return "OnDemandInvalidatedException: An attempt to get $propertyName from ${onDemandClass.runtimeType} failed because the source was invalidated."; |
|
} |
|
} |
|
|
|
class OnDemand { |
|
bool valid; |
|
|
|
final Function onInvalidation; |
|
|
|
void invalidate() { |
|
if (valid) { |
|
valid = false; |
|
if (onInvalidation != null) onInvalidation(); |
|
} |
|
} |
|
|
|
OnDemand(this.onInvalidation): valid = true; |
|
} |
|
|
|
class OnDemandTrainData extends OnDemand { |
|
final WebViewController _controller; |
|
|
|
OnDemandTrainData({ |
|
WebViewController controller, |
|
Function onInvalidation |
|
}) |
|
: _controller = controller, |
|
_route = OnDemandTrainRoute(controller: controller), |
|
_lastInfo = OnDemandLastInfo(controller: controller), |
|
_destination = OnDemandDestination(controller: controller), |
|
_nextStop = OnDemandNextStop(controller: controller), |
|
_stations = OnDemandStations(controller: controller), |
|
super(onInvalidation); |
|
|
|
@override |
|
invalidate() { |
|
super.invalidate(); |
|
route.invalidate(); |
|
lastInfo.invalidate(); |
|
destination.invalidate(); |
|
nextStop.invalidate(); |
|
stations.invalidate(); |
|
} |
|
|
|
Future<String> get _originalDepartureDate async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "_originalDepartureDate"); |
|
|
|
final tempRes = await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let field = table.querySelector("caption"); |
|
return field.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
|
|
return tempRes.split(" ").last; |
|
} |
|
|
|
Future<DateTime> get departureDate async { |
|
final str = await _originalDepartureDate; |
|
|
|
final parts = str.split(".").map((str) => int.parse(str)).toList(); |
|
|
|
return DateTime(parts[2], parts[1], parts[0]); |
|
} |
|
|
|
Future<String> get rang async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "rang"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[0]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get trainNumber async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "trainNumber"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[1]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get operator async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "operator"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[2]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
final OnDemandTrainRoute _route; |
|
OnDemandTrainRoute get route => _route; |
|
|
|
Future<String> get state async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "state"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[4]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
final OnDemandLastInfo _lastInfo; |
|
OnDemandLastInfo get lastInfo => _lastInfo; |
|
|
|
final OnDemandDestination _destination; |
|
OnDemandDestination get destination => _destination; |
|
|
|
final OnDemandNextStop _nextStop; |
|
OnDemandNextStop get nextStop => _nextStop; |
|
|
|
Future<String> get routeDistance async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "routeDistance"); |
|
|
|
final result = (await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[12]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)).trim(); |
|
|
|
return result.takeWhile((char) => char != ' '.codeUnitAt(0)); |
|
} |
|
|
|
Future<String> get _routeDuration async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "_routeDuration"); |
|
|
|
var result = (await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[13]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)).trim(); |
|
|
|
if (result[result.length - 1] == '.') result = result.substring(0, result.length - 1); |
|
|
|
return result; |
|
} |
|
|
|
Future<Duration> get routeDuration async { |
|
final input = await _routeDuration; |
|
|
|
var result = Duration(); |
|
|
|
StringBuffer buffer = StringBuffer(); |
|
|
|
for (var i = 0; i < input.length; i++) { |
|
if ('0'.codeUnitAt(0) <= input.codeUnitAt(i) && input.codeUnitAt(i) <= '9'.codeUnitAt(0)) { |
|
buffer.writeCharCode(input.codeUnitAt(i)); |
|
} |
|
else if (input.startsWith("min", i)) { |
|
result += Duration(minutes: int.parse(buffer.toString())); |
|
buffer = StringBuffer(); |
|
i += 2; |
|
} |
|
else if (input.startsWith("h", i)) { |
|
result += Duration(hours: int.parse(buffer.toString())); |
|
buffer = StringBuffer(); |
|
} |
|
else throw FormatException("Unrecognised!"); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
final OnDemandStations _stations; |
|
OnDemandStations get stations => _stations; |
|
} |
|
|
|
class OnDemandTrainRoute extends OnDemand { |
|
final WebViewController _controller; |
|
|
|
OnDemandTrainRoute({ |
|
WebViewController controller, |
|
Function onInvalidation |
|
}) : _controller = controller, super(onInvalidation); |
|
|
|
Future<String> get original async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "original"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[3]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get from async { |
|
final original = await this.original; |
|
return original.split("-")[0]; |
|
} |
|
|
|
Future<String> get to async { |
|
final original = await this.original; |
|
return original.split("-")[1]; |
|
} |
|
} |
|
|
|
class OnDemandLastInfo extends OnDemand { |
|
final WebViewController _controller; |
|
|
|
OnDemandLastInfo({ |
|
WebViewController controller, |
|
Function onInvalidation |
|
}) : _controller = controller, super(onInvalidation); |
|
|
|
Future<String> get _lastInfoOriginal async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "_lastInfoOriginal"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[5]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get station async { |
|
final original = await _lastInfoOriginal; |
|
|
|
return original |
|
.split("[")[0] |
|
.trim(); |
|
} |
|
|
|
Future<String> get event async { |
|
final original = await _lastInfoOriginal; |
|
|
|
return original |
|
.split("[")[1] |
|
.split("]")[0] |
|
.trim(); |
|
} |
|
|
|
Future<String> get originalDateAndTime async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "originalDateAndTime"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[6]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<DateTime> get dateAndTime async => parseCFRDateTime(await originalDateAndTime); |
|
|
|
Future<int> get delay async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "delay"); |
|
|
|
return int.parse( |
|
await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[7]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
) |
|
); |
|
} |
|
} |
|
|
|
class OnDemandDestination extends OnDemand { |
|
final WebViewController _controller; |
|
|
|
OnDemandDestination({ |
|
WebViewController controller, |
|
Function onInvalidation |
|
}) : _controller = controller, super(onInvalidation); |
|
|
|
Future<String> get stationName async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "destinationStation"); |
|
|
|
final result = (await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[8]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)).trim(); |
|
|
|
if (result.isEmpty) return null; |
|
else return result; |
|
} |
|
|
|
Future<String> get _originalDestinationArrival async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "_originalDestinationArrival"); |
|
|
|
final result = (await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[9]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)).trim(); |
|
|
|
if (result.isEmpty) return null; |
|
else return result; |
|
} |
|
|
|
Future<DateTime> get arrival => _originalDestinationArrival.then((value) => parseCFRDateTime(value)); |
|
} |
|
|
|
class OnDemandNextStop extends OnDemand { |
|
final WebViewController _controller; |
|
|
|
OnDemandNextStop({ |
|
WebViewController controller, |
|
Function onInvalidation |
|
}) : _controller = controller, super(onInvalidation); |
|
|
|
Future<String> get stationName async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "destinationStation"); |
|
|
|
final result = (await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[10]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)).trim(); |
|
|
|
if (result.isEmpty) return null; |
|
else return result; |
|
} |
|
|
|
Future<String> get _originalNextStopArrival async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "_originalDestinationArrival"); |
|
|
|
final result = (await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
let rows = table.querySelectorAll("tr"); |
|
let currentRow = rows[11]; |
|
let currentDataCell = currentRow.querySelectorAll("td")[1]; |
|
return currentDataCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)).trim(); |
|
|
|
if (result.isEmpty) return null; |
|
else return result; |
|
} |
|
|
|
Future<DateTime> get arrival => _originalNextStopArrival.then((value) => parseCFRDateTime(value)); |
|
} |
|
|
|
class OnDemandStations extends OnDemand { |
|
final WebViewController _controller; |
|
List<OnDemand> issuedOnDemands = []; |
|
|
|
@override |
|
void invalidate() { |
|
issuedOnDemands.map((od) => od.invalidate()); |
|
super.invalidate(); |
|
} |
|
|
|
OnDemandStations({ |
|
@required WebViewController controller, |
|
Function onInvalidation |
|
}) : _controller = controller, super(onInvalidation); |
|
|
|
Future<bool> get _stationsLoaded async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "_stationsLoaded"); |
|
|
|
final result = await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => JSON.stringify(document.querySelector("#GridView1") == null))() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
|
|
final decoder = JsonDecoder(); |
|
return !(decoder.convert(result) as bool); |
|
} |
|
|
|
Stream<OnDemandStation> call({@required Future pageLoadFuture}) async* { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "call"); |
|
|
|
if (!await _stationsLoaded) { |
|
await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const button = document.querySelector("#Button2"); |
|
button.click(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
await pageLoadFuture; |
|
} |
|
|
|
final count = int.parse(await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const count = rowsArray.length - 1; |
|
return String(count); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)); |
|
|
|
for (int i = 1; i <= count; i++) { |
|
final ods = OnDemandStation( |
|
controller: _controller, |
|
index: i, |
|
); |
|
issuedOnDemands.add(ods); |
|
yield ods; |
|
} |
|
} |
|
} |
|
|
|
class OnDemandStation extends OnDemand { |
|
final WebViewController _controller; |
|
final int index; |
|
|
|
OnDemandStation({ |
|
@required WebViewController controller, |
|
@required this.index, |
|
Function onInvalidation |
|
}) : _controller = controller, super(onInvalidation); |
|
|
|
Future<int> get km async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "km"); |
|
|
|
return int.parse(await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[0]; |
|
return kmCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)); |
|
} |
|
|
|
Future<String> get stationName async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "stationName"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[1]; |
|
return kmCell.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get arrivalTime async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "arrivalTime"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[2]; |
|
return kmCell.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get stopsFor async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "stopsFor"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[3]; |
|
return kmCell.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<String> get departureTime async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "departureTime"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[4]; |
|
return kmCell.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
|
|
Future<RealOrEstimate> get realOrEstimate async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "realOrEstimate"); |
|
|
|
final value = await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[5]; |
|
return kmCell.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
|
|
if (value == "Real") return RealOrEstimate.real; |
|
else if (value == "Estimat") return RealOrEstimate.estimate; |
|
else return RealOrEstimate.UNKNOWN; |
|
} |
|
|
|
Future<int> get delay async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "delay"); |
|
|
|
final value = await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[6]; |
|
return kmCell.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
|
|
if (value.isEmpty) return 0; |
|
else return int.parse(value); |
|
} |
|
|
|
Future<String> get observations async { |
|
if (!valid) throw OnDemandInvalidatedException(onDemandClass: this, propertyName: "observations"); |
|
|
|
return await wInvoke( |
|
webViewController: _controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
const table = document.querySelector("#GridView1"); |
|
const rows = table.querySelectorAll("tr"); |
|
const rowsArray = Array.from(rows); |
|
const row = rowsArray[$index]; |
|
const columns = row.querySelectorAll("td"); |
|
const kmCell = columns[7]; |
|
return kmCell.textContent.trim(); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
} |
|
} |
|
|
|
enum RealOrEstimate { |
|
real, |
|
estimate, |
|
UNKNOWN |
|
} |
|
|
|
class TrainDataWebViewAdapter extends StatefulWidget { |
|
final WidgetBuilder builder; |
|
|
|
TrainDataWebViewAdapter({@required this.builder}); |
|
|
|
@override |
|
State<StatefulWidget> createState() { |
|
return _TrainDataWebViewAdapterState(); |
|
} |
|
|
|
static _TrainDataWebViewAdapterState of(BuildContext context) => |
|
(context.findAncestorWidgetOfExactType<_TrainDataWebViewAdapterInheritedWidget>()) |
|
.state; |
|
} |
|
|
|
class ProgressReport { |
|
final int current; |
|
final int total; |
|
final String description; |
|
|
|
ProgressReport({@required this.current, @required this.total, this.description}); |
|
|
|
@override |
|
String toString() { |
|
return description == null ? "ProgressReport($current/$total)" : "ProgressReport($current/$total: $description)"; |
|
} |
|
} |
|
|
|
class _TrainDataWebViewAdapterState extends State<TrainDataWebViewAdapter> { |
|
Completer<WebViewController> _webViewControllerCompleter = Completer(); |
|
Future<WebViewController> get webViewController => _webViewControllerCompleter.future; |
|
|
|
StreamController<String> _pageLoadController; |
|
Stream<String> pageLoadStream; |
|
Future<String> get nextLoadFuture => pageLoadStream.take(1).first; |
|
|
|
StreamController<ProgressReport> _progressController; |
|
Stream<ProgressReport> progressStream; |
|
|
|
Future<TrainLookupResult> loadTrain(int trainNo) async { |
|
currentDatas.removeWhere((ondemand) { |
|
ondemand.invalidate(); |
|
return true; |
|
}); |
|
|
|
final controller = await webViewController; |
|
var nlf; |
|
|
|
nlf = nextLoadFuture; |
|
await controller.loadUrl("https://appiris.infofer.ro/MytrainRO.aspx"); |
|
await nlf; |
|
_reportStatus( |
|
current: 2, |
|
description: "Loaded Informatica Feroviară webpage" |
|
); |
|
|
|
nlf = nextLoadFuture; |
|
await controller.evaluateJavascript(""" |
|
( () => { |
|
let inputField = document.querySelector("#TextTrnNo"); |
|
inputField.value = $trainNo; |
|
let submitButton = document.querySelector("#Button1"); |
|
submitButton.click(); |
|
} ) () |
|
"""); |
|
await nlf; |
|
|
|
_reportStatus( |
|
current: 3, |
|
description: "Loaded train information" |
|
); |
|
|
|
var result = await wInvoke( |
|
webViewController: controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let errorMessage = document.querySelector("#Lblx"); |
|
return errorMessage.textContent; |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
); |
|
|
|
if (result.isNotEmpty) { |
|
return TrainLookupResult.NOT_FOUND; |
|
} |
|
|
|
final jsonDecoder = JsonDecoder(); |
|
|
|
final foundTable = jsonDecoder.convert(await wInvoke( |
|
webViewController: controller, |
|
jsFunctionContent: """ |
|
(() => { |
|
let table = document.querySelector("#DetailsView1"); |
|
return JSON.stringify(table !== null); |
|
})() |
|
""", |
|
isFunctionAlready: true, |
|
)) as bool; |
|
|
|
if (foundTable) { |
|
return TrainLookupResult.FOUND; |
|
} |
|
|
|
/// Should not happen, report error in this case |
|
return TrainLookupResult.OTHER; |
|
} |
|
|
|
List<OnDemand> currentDatas = []; |
|
|
|
Future<OnDemandTrainData> trainData({Function onInvalidation}) async { |
|
final controller = await webViewController; |
|
|
|
final result = OnDemandTrainData( |
|
controller: controller, |
|
onInvalidation: onInvalidation |
|
); |
|
|
|
currentDatas.add(result); |
|
|
|
return result; |
|
} |
|
|
|
int lastStatusReported; |
|
|
|
_reportStatus({@required int current, String description}) { |
|
lastStatusReported = current; |
|
_progressController.add(ProgressReport( |
|
current: current, |
|
total: TOTAL_PROGRESS, |
|
description: description |
|
)); |
|
} |
|
|
|
recallLastReport() { |
|
_progressController.add(ProgressReport(current: lastStatusReported, total: TOTAL_PROGRESS)); |
|
} |
|
|
|
restartProgressReport() { |
|
lastStatusReported = 0; |
|
webViewController.then((_) { |
|
_reportStatus(current: 1, description: "WebView created"); |
|
}); |
|
} |
|
|
|
@override |
|
void initState() { |
|
_pageLoadController = StreamController(); |
|
pageLoadStream = _pageLoadController.stream.asBroadcastStream(); |
|
|
|
_progressController = StreamController(); |
|
progressStream = _progressController.stream.asBroadcastStream(); |
|
|
|
lastStatusReported = 0; |
|
|
|
super.initState(); |
|
} |
|
|
|
@override |
|
void dispose() { |
|
_pageLoadController.close(); |
|
_progressController.close(); |
|
|
|
super.dispose(); |
|
} |
|
|
|
static const int TOTAL_PROGRESS = 3; |
|
|
|
@override |
|
Widget build(BuildContext context) { |
|
return HiddenWebView( |
|
webView: WebView( |
|
javascriptMode: JavascriptMode.unrestricted, |
|
onWebViewCreated: (controller) { |
|
_webViewControllerCompleter.complete(controller); |
|
_reportStatus( |
|
current: 1, |
|
description: "WebView created" |
|
); |
|
}, |
|
onPageFinished: (url) { |
|
_pageLoadController.add(url); |
|
}, |
|
), |
|
child: _TrainDataWebViewAdapterInheritedWidget( |
|
child: Builder( |
|
builder: widget.builder, |
|
), |
|
state: this, |
|
), |
|
); |
|
} |
|
} |
|
|
|
class _TrainDataWebViewAdapterInheritedWidget extends InheritedWidget { |
|
final _TrainDataWebViewAdapterState state; |
|
|
|
_TrainDataWebViewAdapterInheritedWidget({this.state, Widget child, Key key}) |
|
:super(key: key, child: child); |
|
|
|
@override |
|
bool updateShouldNotify(InheritedWidget oldWidget) { |
|
return true; |
|
} |
|
} |
|
|
|
@JsonSerializable() |
|
class TrainData { |
|
final String rang; |
|
@JsonKey(name: "tren") |
|
final String trainNumber; |
|
final String operator; |
|
@JsonKey(name: "relatia") |
|
final String route; |
|
@JsonKey(name: "stare") |
|
final String state; |
|
@JsonKey(name: "ultima_informatie") |
|
final LastInfo lastInfo; |
|
@JsonKey(name: "destinatie") |
|
final StopInfo destination; |
|
@JsonKey(name: "urmatoarea_oprire") |
|
final StopInfo nextStop; |
|
@JsonKey(name: "durata_calatoriei") |
|
final String tripLength; |
|
@JsonKey(name: "distanta") |
|
final String distance; |
|
|
|
@JsonKey(name: "stations") |
|
List<StationEntry> stations; |
|
|
|
TrainData({this.rang, this.trainNumber, this.operator, this.lastInfo, |
|
this.state, this.route, this.tripLength, this.stations, this.nextStop, |
|
this.distance, this.destination}); |
|
factory TrainData.fromJson(Map<String, dynamic> json) { |
|
var result = _$TrainDataFromJson(json); |
|
var foundEstimat = false; |
|
result.stations = result.stations.map((station) { |
|
if (station.realOrEstimate == "Estimat") { |
|
foundEstimat = true; |
|
} |
|
|
|
station.realOrEstimate = foundEstimat ? "Estimat" : "Real"; |
|
|
|
return station; |
|
}).toList(); |
|
return result; |
|
} |
|
Map<String, dynamic> toJson() => _$TrainDataToJson(this); |
|
} |
|
|
|
@JsonSerializable() |
|
class LastInfo { |
|
@JsonKey(name: "statia") |
|
final String station; |
|
@JsonKey(name: "eveniment") |
|
final String event; |
|
@JsonKey(name: "data_si_ora") |
|
final String dateAndTime; |
|
DateTime get formattedDateAndTime { |
|
return parseCFRDateTime(dateAndTime); |
|
} |
|
@JsonKey(name: "intarziere") |
|
final int delay; |
|
|
|
|
|
LastInfo({this.dateAndTime, this.delay, this.event, this.station}); |
|
|
|
factory LastInfo.fromJson(Map<String, dynamic> json) => _$LastInfoFromJson(json); |
|
Map<String, dynamic> toJson() => _$LastInfoToJson(this); |
|
} |
|
|
|
@JsonSerializable() |
|
class StopInfo { |
|
@JsonKey(name: "statia") |
|
final String station; |
|
@JsonKey(name: "data_si_ora") |
|
final String dateAndTime; |
|
DateTime get formattedDateAndTime { |
|
return parseCFRDateTime(dateAndTime); |
|
} |
|
|
|
StopInfo({this.station, this.dateAndTime}); |
|
|
|
factory StopInfo.fromJson(Map<String, dynamic> json) => _$StopInfoFromJson(json); |
|
Map<String, dynamic> toJson() => _$StopInfoToJson(this); |
|
} |
|
|
|
@JsonSerializable() |
|
class StationEntry { |
|
final String km; |
|
@JsonKey(name: "statia") |
|
final String name; |
|
@JsonKey(name: "sosire") |
|
final String arrivalTime; |
|
@JsonKey(name: "stationeaza_pentru") |
|
final String waitTime; |
|
@JsonKey(name: "plecare") |
|
final String departureTime; |
|
@JsonKey(name: "real/estimat") |
|
String realOrEstimate; |
|
bool get real { |
|
return realOrEstimate == "Real"; |
|
} |
|
@JsonKey(name: "intarziere") |
|
final int delay; |
|
@JsonKey(name: "observatii") |
|
final String observations; |
|
|
|
StationEntry({this.name, this.delay, this.realOrEstimate, |
|
this.arrivalTime, this.departureTime, this.km, this.observations, |
|
this.waitTime}); |
|
|
|
factory StationEntry.fromJson(Map<String, dynamic> json) => _$StationEntryFromJson(json); |
|
Map<String, dynamic> toJson() => _$StationEntryToJson(this); |
|
} |
|
|
|
DateTime parseCFRDateTime(String dateAndTime) { |
|
if (dateAndTime == null || dateAndTime.isEmpty) return null; |
|
|
|
final parts = dateAndTime.split(" "); |
|
|
|
final dateParts = parts[0].split("."); |
|
final day = int.parse(dateParts[0]); |
|
final month = int.parse(dateParts[1]); |
|
final year = int.parse(dateParts[2]); |
|
|
|
final timeParts = parts[1].split(":"); |
|
final hour = int.parse(timeParts[0]); |
|
final minute = int.parse(timeParts[1]); |
|
|
|
return DateTime(year, month, day, hour, minute); |
|
}
|
|
|