diff --git a/app/build.gradle b/app/build.gradle
index 6b4b9ea..b0e7315 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,188 +1,190 @@
plugins {
id 'com.google.protobuf'
id 'org.jetbrains.kotlin.android'
id 'com.android.application'
id 'com.google.devtools.ksp'
}
android {
compileSdk 36
namespace "it.reyboz.bustorino"
defaultConfig {
applicationId "it.reyboz.bustorino"
minSdkVersion 24
//noinspection EditedTargetSdkVersion
targetSdkVersion 36
buildToolsVersion = '36.0.0'
versionCode 74
versionName "2.5.1"
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()]
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
testOptions {
unitTests.returnDefaultValues = true
}
sourceSets {
androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString())
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-dev"
}
gitpull{
applicationIdSuffix ".gitdev"
versionNameSuffix "-gitdev"
}
}
repositories {
mavenCentral()
mavenLocal()
}
dependencies {
//new libraries
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
+ coreLibraryDesugaringEnabled true
}
kotlin {
jvmToolchain 21
}
lint {
abortOnError false
}
androidResources {
generateLocaleConfig true
}
buildFeatures{
buildConfig = true
}
splits{
abi{
enable true
reset()
// Specifies a list of ABIs for Gradle to create APKs for.
include "x86", "x86_64","armeabi-v7a", "arm64-v8a"
universalApk true
}
}
packagingOptions { resources.excludes.add("META-INF/*") }
}
protobuf {
protoc {
artifact = 'com.google.protobuf:protoc:3.22.3'
}
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
ksp {
arg("room.schemaLocation", "$projectDir/assets/schemas")
}
dependencies {
+ coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// Guava implementation for DBUpdateWorker
implementation 'com.google.guava:guava:33.5.0-android'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation "androidx.fragment:fragment-ktx:$fragment_version"
implementation "androidx.activity:activity-ktx:$activity_version"
implementation "androidx.annotation:annotation:1.9.1"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation "androidx.appcompat:appcompat-resources:$appcompat_version"
implementation "androidx.preference:preference-ktx:$preference_version"
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "com.google.android.material:material:1.13.0"
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "androidx.coordinatorlayout:coordinatorlayout:1.3.0"
implementation 'org.jsoup:jsoup:1.22.1'
implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1'
implementation 'com.android.volley:volley:1.2.1'
//maplibre
implementation 'org.maplibre.gl:android-sdk:12.0.1'
implementation 'org.maplibre.gl:android-sdk-turf:6.0.1'
implementation 'org.maplibre.gl:android-plugin-annotation-v9:3.0.2'
// remember to enable maven repo jitpack.io when wanting to use osmbonuspack
//implementation 'com.github.MKergall:osmbonuspack:6.9.0'
// ACRA
implementation "ch.acra:acra-mail:$acra_version"
implementation "ch.acra:acra-dialog:$acra_version"
// google transit realtime
implementation 'com.google.protobuf:protoc:4.34.1'
implementation 'com.google.protobuf:protobuf-javalite:4.34.1'
// mqtt library
//implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
//implementation 'com.github.hannesa2:paho.mqtt.android:4.4'
implementation("com.hivemq:hivemq-mqtt-client:1.3.13")
implementation(platform("com.hivemq:hivemq-mqtt-client-websocket:1.3.13"))
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Lifecycles only (without ViewModel or LiveData)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Legacy
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
// Room components
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
ksp "androidx.room:room-compiler:$room_version"
//multidex - we need this to build the app
implementation "androidx.multidex:multidex:$multidex_version"
implementation 'de.siegmar:fastcsv:4.2.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation "androidx.test.ext:junit:1.3.0"
androidTestImplementation "androidx.test:core:$androidXTestVersion"
androidTestImplementation "androidx.test:runner:$androidXTestVersion"
androidTestImplementation "androidx.test:rules:$androidXTestVersion"
androidTestImplementation "androidx.room:room-testing:$room_version"
}
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
index e0627dc..f7c6d42 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
@@ -1,430 +1,430 @@
/*
BusTO - Backend components
Copyright (C) 2018 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
import androidx.annotation.Nullable;
import android.util.Log;
import it.reyboz.bustorino.data.GTTInfoInject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
public class FiveTAPIFetcher implements ArrivalsFetcher{
private static final String DEBUG_NAME = "FiveTAPIFetcher";
private final Map defaultHeaders = getDefaultHeaders();
final static LinkedList apiDays = new LinkedList<>(Arrays.asList("dom","lun","mar","mer","gio","ven","sab"));
@Override
public Palina ReadArrivalTimesAll(String stopID, AtomicReference res) {
//set the date for the request as now
Palina p = new Palina(stopID);
//request parameters
String response = performAPIRequest(QueryType.ARRIVALS,stopID,res);
if(response==null) {
if(res.get()== Result.SERVER_ERROR_404) {
Log.w(DEBUG_NAME,"Got 404, either the server failed, or the stop was not found, or the address is wrong");
//res.set(Result.S);
}
return p;
}
List routes = parseArrivalsServerResponse(response, res);
if(res.get()==Result.OK) {
for (Route r : routes) {
p.addRoute(r);
}
p.sortRoutes();
}
return p;
}
@Override
public Passaggio.Source getSourceForFetcher() {
return Passaggio.Source.FiveTAPI;
}
List parseArrivalsServerResponse(String JSONresponse, AtomicReference res){
ArrayList routes = new ArrayList<>(3);
/*
Slight problem:
"longName": ==> DESCRIPTION
"name": "13N",
"departures": [
{
"arrivalTimeInt": 1272,
"time": "21:12",
"rt": false
}]
"lineType": "URBANO" ==> URBANO can be either bus or tram or METRO
*/
JSONArray arr;
try{
arr = new JSONArray(JSONresponse);
String type;
Route.Type routetype = Route.Type.UNKNOWN;
for(int i =0; i parseDirectionsFromResponse(String response) throws IllegalArgumentException,JSONException{
if(response == null || response.equals("null") || response.length()==0)
throw new IllegalArgumentException("Response string is null or void");
ArrayList routes = new ArrayList<>(10);
JSONArray lines =new JSONArray(response);
for(int i=0; i 1) {
String secondo = exploded[exploded.length-2];
if (secondo.contains("festivo")) {
festivo = Route.FestiveInfo.FESTIVO;
} else if (secondo.contains("feriale")) {
festivo = Route.FestiveInfo.FERIALE;
} else if(secondo.contains("lun. - ven")) {
serviceDays = Route.reduced_week;
} else if(secondo.contains("sab - fest.")){
serviceDays = Route.weekend;
festivo = Route.FestiveInfo.FESTIVO;
} else {
/*
Log.d(DEBUG_NAME,"Parsing details of line "+lineName+" branchid "+branchid+":\n\t"+
"Couldn't find a the service days\n"+
"Description: "+secondo+","+description
);
*/
}
if(exploded.length>2){
switch (exploded[exploded.length-3].trim()) {
case "bus":
t = Route.Type.BUS;
break;
case "tram":
//never happened, but if it could happen you can get it
t = Route.Type.TRAM;
break;
default:
//nothing
}
}
} else //only one piece
if(description.contains("festivo")){
festivo = Route.FestiveInfo.FESTIVO;
} else if(description.contains("feriale")){
festivo = Route.FestiveInfo.FERIALE;
}
if(t == Route.Type.UNKNOWN &&(lineName.trim().equals("10")|| lineName.trim().equals("15"))) t= Route.Type.TRAM;
//check for the presence of parenthesis
String preParenthesis, postParenthesis;
boolean hasParenth = false;
if (description.contains("(")){
hasParenth =true;
preParenthesis = description.split("\\(")[0];
postParenthesis = description.split("\\(")[1];
} else {
preParenthesis = description;
postParenthesis = "";
}
if(preParenthesis.contains("-")){
//Sometimes the actual filtered direction still remains the full line (including both extremes)
preParenthesis = preParenthesis.split("-")[1];
}
final String directionFinal = hasParenth? preParenthesis.trim() + " (" + postParenthesis : preParenthesis;
Route r = new Route(lineName.trim(),directionFinal.trim(),t,new ArrayList<>());
if(serviceDays.length>0) r.serviceDays = serviceDays;
r.festivo = festivo;
r.branchid = branchid;
r.description = description.trim();
//check if we have the stop list
if (branchJSON.has("branchDetail")) {
final String stops = branchJSON.getJSONObject("branchDetail").getString("stops");
r.setStopsList(Arrays.asList(stops.split(",")));
}
routes.add(r);
}
return routes;
}
public List getDirectionsForStop(String stopID, AtomicReference res) {
String response = performAPIRequest(QueryType.DETAILS,stopID,res);
List routes;
try{
routes = parseDirectionsFromResponse(response);
res.set(Result.OK);
} catch (JSONException | IllegalArgumentException e) {
e.printStackTrace();
res.set(Result.PARSER_ERROR);
routes = null;
}
return routes;
}
public ArrayList getAllStopsFromGTT(AtomicReference res){
String response = performAPIRequest(QueryType.STOPS_ALL,null,res);
if(response==null) return null;
ArrayList stopslist;
try{
//JSONObject responseJSON = new JSONObject(response);
JSONArray stops = new JSONArray(response);//responseJSON.getJSONArray("stops");
stopslist = new ArrayList<>(stops.length());
for (int i=0;i getAllLinesFromGTT(AtomicReference res){
String resp = performAPIRequest(QueryType.LINES,null,res);
if(resp==null) {
return null;
}
ArrayList routes = null;
try {
JSONArray lines = new JSONArray(resp);
routes = new ArrayList<>(lines.length());
for(int i = 0; i getDefaultHeaders(){
HashMap param = new HashMap<>();
param.put("Host","www.5t.torino.it");
param.put("Connection","Keep-Alive");
param.put("Accept-Encoding", "gzip");
return param;
}
/**
* Create and perform the network request. This method adds parameters and returns the result
* @param t type of request to be performed
* @param stopID optional parameter, stop ID which you need for passages and branches
* @param res result container
* @return a String which contains the result of the query, to be parsed
*/
@Nullable
public static String performAPIRequest(QueryType t,@Nullable String stopID, AtomicReference res){
URL u;
Map param;
try {
String address = getURLForOperation(t,stopID);
//Log.d(DEBUG_NAME,"The address to query is: "+address);
param = getDefaultHeaders();
u = new URL(address);
} catch (UnsupportedEncodingException |MalformedURLException e) {
e.printStackTrace();
res.set(Result.PARSER_ERROR);
return null;
}
return networkTools.queryURL(u,res,param);
}
/**
* Get the right url for the operation you are doing, to be fed into the queryURL method
* @param t type of operation
* @param stopID stop on which you are working on
* @return the Url to go to
* @throws UnsupportedEncodingException if it cannot be converted to utf-8
*/
public static String getURLForOperation(QueryType t,@Nullable String stopID) throws UnsupportedEncodingException {
final StringBuilder sb = new StringBuilder();
sb.append("http://www.5t.torino.it/ws2.1/rest/");
if(t!=QueryType.LINES) sb.append("stops/");
switch (t){
case ARRIVALS:
sb.append(URLEncoder.encode(stopID,"utf-8"));
sb.append("/departures");
break;
case DETAILS:
sb.append(URLEncoder.encode(stopID,"utf-8"));
sb.append("/branches/details");
break;
case STOPS_ALL:
sb.append("all");
break;
case STOPS_VERSION:
sb.append("version");
break;
case LINES:
sb.append("lines/all");
break;
}
return sb.toString();
}
public enum QueryType {
ARRIVALS, DETAILS,STOPS_ALL, STOPS_VERSION,LINES
}
}
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java b/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java
deleted file mode 100644
index ae4b734..0000000
--- a/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.java
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- BusTO (backend components)
- Copyright (C) 2016 Ludovico Pavesi
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
- */
-
-package it.reyboz.bustorino.backend;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import android.util.Log;
-
-import java.util.Locale;
-
-public final class Passaggio implements Comparable, Parcelable {
-
- private static final int UNKNOWN_TIME = -3;
- private static final String DEBUG_TAG = "BusTO-Passaggio";
-
- private final String passaggioGTT;
- public final int hh,mm;
- private @Nullable Integer realtimeDifference;
- public final boolean isInRealTime;
- public final Source source;
-
-
- /**
- * Useless constructor.
- *
- * //@param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
- */
-// public Passaggio(@NonNull String TimeGTT) {
-// this.passaggio = TimeGTT;
-// }
-
- @Override
- public String toString() {
- return this.passaggioGTT;
- }
-
-
- /**
- * Constructs a time (passaggio) for the timetable.
- *
- * @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
- * @throws IllegalArgumentException if nothing reasonable can be extracted from the string
- */
- public Passaggio(@NonNull String TimeGTT, @NonNull Source sorgente) {
- passaggioGTT = TimeGTT;
- source = sorgente;
- String[] parts = TimeGTT.split(":");
- String hh,mm;
- boolean realtime;
- if(parts.length != 2) {
- //throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
- Log.w(DEBUG_TAG,"The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
- this.hh = UNKNOWN_TIME;
- this.mm = UNKNOWN_TIME;
- this.isInRealTime = false;
- return;
- }
- hh = parts[0];
- if(parts[1].endsWith("*")) {
- mm = parts[1].substring(0, parts[1].length() - 1);
- realtime = true;
- } else {
- mm = parts[1];
- realtime = false;
- }
- int hour=-3,min=-3;
- try {
- hour = Integer.parseInt(hh);
- min = Integer.parseInt(mm);
- } catch (NumberFormatException ex){
- Log.w(DEBUG_TAG,"Cannot convert passaggio into hour and minutes");
- hour = UNKNOWN_TIME;
- min = UNKNOWN_TIME;
- realtime = false;
- } finally {
- this.hh = hour;
- this.mm = min;
- this.isInRealTime = realtime;
-
- }
- }
-
- public Passaggio(int hour, int minutes, boolean realtime, Source sorgente){
- this.hh = hour;
- this.mm = minutes;
- this.isInRealTime = realtime;
- if (!realtime) realtimeDifference = 0;
- this.source = sorgente;
- //Build the passaggio string
- StringBuilder sb = new StringBuilder();
- sb.append(hour).append(":").append(minutes);
- if(realtime) sb.append("*");
- this.passaggioGTT = sb.toString();
- }
-
- public static String createPassaggioGTT(String timeInput, boolean realtime){
- final String time = timeInput.trim();
- if(time.contains("*")){
- if(realtime) return time;
- else return time.substring(0,time.length()-1);
- } else{
- if(realtime) return time.concat("*");
- else return time;
- }
- }
- public Passaggio(int numSeconds, boolean realtime, int timeDiff, Source source){
- int minutes = numSeconds / 60;
- int hours = minutes / 60;
- //this.hh = hours;
- this.mm = minutes - hours*60;
- this.hh = hours % 24;
- this.realtimeDifference = timeDiff/60;
- this.isInRealTime = realtime;
- this.source = source;
- this.passaggioGTT = makePassaggioGTT(this.hh, this.mm, this.isInRealTime);
- }
-
- private static String makePassaggioGTT(int hour, int minutes, boolean realtime){
- StringBuilder sb = new StringBuilder();
- sb.append(String.format(Locale.ITALIAN,"%02d", hour)).append(":").append(String.format(Locale.ITALIAN,"%02d", minutes));
- if(realtime) sb.append("*");
- return sb.toString();
- }
-
- @Override
- public int compareTo(@NonNull Passaggio other) {
- if(this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME)
- return 0;
- else {
- int diff = getMinutesDiff(other);
-
- // we should take into account if one is in real time and the other isn't, shouldn't we?
- if (other.isInRealTime) {
- diff+=2;
- }
- if (this.isInRealTime) {
- diff -=2;
- }
-
- return diff;
- }
- }
- public int getMinutesDiff(Passaggio other){
- int diff = this.hh - other.hh;
- // an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01)
- if (diff > 12) { // untested
- diff -= 24;
- } else if (diff < -12) {
- diff += 24;
- }
-
- diff *= 60;
-
- diff += this.mm - other.mm;
- return diff;
- }
-
-
- protected Passaggio(Parcel in) {
- passaggioGTT = in.readString();
- hh = in.readInt();
- mm = in.readInt();
- if (in.readByte() == 0) {
- realtimeDifference = null;
- } else {
- realtimeDifference = in.readInt();
- }
- isInRealTime = in.readByte() != 0;
- source = Source.valueOf(in.readString());
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(passaggioGTT);
- dest.writeInt(hh);
- dest.writeInt(mm);
- if (realtimeDifference == null) {
- dest.writeByte((byte) 0);
- } else {
- dest.writeByte((byte) 1);
- dest.writeInt(realtimeDifference);
- }
- dest.writeByte((byte) (isInRealTime ? 1 : 0));
- dest.writeString(source.name());
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- public static final Creator CREATOR = new Creator() {
- @Override
- public Passaggio createFromParcel(Parcel in) {
- return new Passaggio(in);
- }
-
- @Override
- public Passaggio[] newArray(int size) {
- return new Passaggio[size];
- }
- };
-
-//
-// @Override
-// public String toString() {
-// String resultString = (this.hh).concat(":").concat(this.mm);
-// if(this.isInRealTime) {
-// return resultString.concat("*");
-// } else {
-// return resultString;
-// }
-// }
- public enum Source{
- FiveTAPI,GTTJSON,FiveTScraper,MatoAPI, UNDETERMINED
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.kt b/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.kt
new file mode 100644
index 0000000..926ebb4
--- /dev/null
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Passaggio.kt
@@ -0,0 +1,257 @@
+/*
+ BusTO (backend components)
+ Copyright (C) 2016 Ludovico Pavesi
+ Copyright (C) 2026 Fabio Mazza
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+package it.reyboz.bustorino.backend
+
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.Log
+
+import java.time.LocalDate
+import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoUnit
+import java.util.*
+
+data class Passaggio(
+ val arrivalTime: ZonedDateTime,
+ val isInRealTime: Boolean,
+ @JvmField
+ val source: Source,
+ val realtimeDifference: Int? = null,
+) : Comparable, Parcelable {
+ private val passaggioGTT: String = arrivalTime.format(DATEFORMATTER) + (if (isInRealTime) "*" else "")
+
+ override fun toString(): String {
+ return this.passaggioGTT
+ }
+
+
+ /*override fun compareTo(other: Passaggio?): Int {
+ if (this.hh == UNKNOWN_TIME || other.hh == UNKNOWN_TIME) return 0
+ else {
+ var diff = getMinutesDiff(other)
+
+ // we should take into account if one is in real time and the other isn't, shouldn't we?
+ if (other.isInRealTime) {
+ diff += 2
+ }
+ if (this.isInRealTime) {
+ diff -= 2
+ }
+
+ return diff
+ }
+ }
+
+ */
+ override fun compareTo(other: Passaggio): Int {
+ //DO NOT PUT REAL TIME FIRST (PassaggiSorter exists for this reason)
+ /*if (isInRealTime != other.isInRealTime) {
+ return if (isInRealTime) -1 else 1
+ }
+
+ */
+ return arrivalTime.compareTo(other.arrivalTime)
+ }
+
+
+ /*fun getMinutesDiff(other: Passaggio): Int {
+ var diff = this.hh - other.hh
+ // an attempt to correctly sort arrival times around midnight (e.g. 23.59 should come before 00.01)
+ if (diff > 12) { // untested
+ diff -= 24
+ } else if (diff < -12) {
+ diff += 24
+ }
+
+ diff *= 60
+
+ diff += this.mm - other.mm
+ return diff
+ }
+
+ */
+ /**
+ * Calculate difference in minutes, positive is this arrives after the other one, negative if it arrives before
+ */
+ fun getDifferenceMinutes(other: Passaggio): Long {
+ val res = ChronoUnit.MINUTES.between(other.arrivalTime, this.arrivalTime)
+ return res
+ }
+
+
+ enum class Source {
+ FiveTAPI, GTTJSON, FiveTScraper, MatoAPI, UNDETERMINED
+ }
+
+ constructor(parcel: Parcel) : this(
+ arrivalTime = ZonedDateTime.parse(
+ parcel.readString(),
+ DateTimeFormatter.ISO_ZONED_DATE_TIME
+ ),
+ isInRealTime = parcel.readByte() != 0.toByte(),
+ source = Source.valueOf(parcel.readString()?: Source.UNDETERMINED.name) ?: Source.FiveTAPI,
+ realtimeDifference = parcel.readValue(Int::class.java.classLoader) as? Int
+ )
+
+ override fun writeToParcel(parcel: Parcel, flags: Int) {
+ parcel.writeString(arrivalTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME))
+ parcel.writeByte(if (isInRealTime) 1 else 0)
+ parcel.writeString(source.name)
+ parcel.writeValue(realtimeDifference)
+ }
+
+ override fun describeContents(): Int = 0
+
+ companion object {
+ private val UNKNOWN_TIME = -3
+ private const val DEBUG_TAG = "BusTO-Passaggio"
+
+ @JvmField
+ val CREATOR = object: Parcelable.Creator {
+ override fun createFromParcel(parcel: Parcel): Passaggio = Passaggio(parcel)
+ override fun newArray(size: Int): Array = arrayOfNulls(size)
+ }
+
+ @JvmStatic
+ private fun parseHourMin(hour: Int, minutes: Int, slackMin: Long = 30): ZonedDateTime {
+ val zona = ZoneId.of("Europe/Rome")
+ val timeNow = ZonedDateTime.now(zona)
+ val newTime = LocalTime.of(hour, minutes)
+
+ var possibleTime = ZonedDateTime.of(LocalDate.now(zona), newTime, zona)
+
+ // Se è già passato (o è esattamente adesso e vuoi escluderlo), vado al giorno dopo
+ if (possibleTime.isBefore(timeNow.minusMinutes(slackMin))) {
+ possibleTime = possibleTime.plusDays(1)
+ }
+
+ return possibleTime
+ }
+
+ @JvmStatic
+ private val DATEFORMATTER = DateTimeFormatter.ofPattern("HH:mm")
+ /**
+ * Constructs a time (passaggio) for the timetable.
+ *
+ * @param TimeGTT time in GTT format (e.g. "11:22*"), already trimmed from whitespace.
+ * @throws IllegalArgumentException if nothing reasonable can be extracted from the string
+ */
+ @JvmStatic
+ fun newInstance(TimeGTT: String, sorgente: Source) : Passaggio? {
+ val parts = TimeGTT.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+ val hh: String
+ val mm: String
+ var realtime: Boolean
+ if (parts.size != 2) {
+ //throw new IllegalArgumentException("The string " + TimeGTT + " doesn't follow the sacred format of time according to GTT!");
+ Log.w(DEBUG_TAG, "The string $TimeGTT doesn't follow the sacred format of time according to GTT!")
+ return null;
+ }
+ hh = parts[0]
+ if (parts[1].endsWith("*")) {
+ mm = parts[1].substring(0, parts[1].length - 1)
+ realtime = true
+ } else {
+ mm = parts[1]
+ realtime = false
+ }
+ var time: ZonedDateTime? = null
+ try {
+ val hour = hh.toInt()
+ val min = mm.toInt()
+ time = parseHourMin(hour, min)
+ } catch (ex: Exception) {
+ Log.w(DEBUG_TAG, "Cannot convert passaggio into hour and minutes:\n$ex")
+ return null
+ }
+ return Passaggio(time, realtime, sorgente)
+ }
+
+
+ /**
+ * General constructor for the case hour & minutes
+ */
+ @JvmStatic
+ fun newInstance(hour: Int, minutes: Int, realtime: Boolean, sorgente: Source, realtimeDifference: Int?): Passaggio? {
+ /*this.hh = hour
+ this.mm = minutes
+ this.isInRealTime = realtime
+ if (!realtime) realtimeDifference = 0
+ this.source = sorgente
+ //Build the passaggio string
+ val sb = StringBuilder()
+ sb.append(hour).append(":").append(minutes)
+ if (realtime) sb.append("*")
+ this.passaggioGTT = sb.toString()
+
+ */
+ var time: ZonedDateTime? = null
+ try{
+ time = parseHourMin(hour, minutes)
+ } catch (ex: Exception) {
+ Log.e(DEBUG_TAG, "Cannot parse hour $hour and minutes:$minutes into time:\n$ex")
+ return null
+ }
+ return Passaggio(time, realtime, sorgente, realtimeDifference)
+ }
+
+ @JvmStatic
+ fun newInstance(numSeconds: Int, realtime: Boolean, timeDiffSeconds: Int, source: Source) : Passaggio? {
+ var minutes: Int = numSeconds / 60
+ var hours :Int = minutes / 60
+ //this.hh = hours;
+ minutes -= hours * 60
+ hours %= 24
+ var timeDiffMins:Int = timeDiffSeconds / 60
+ /*
+ this.
+ this.isInRealTime = realtime
+ this.source = source
+ this.passaggioGTT = makePassaggioGTT(this.hh, this.mm, this.isInRealTime)
+
+ */
+ return newInstance(hours, minutes,realtime, source, timeDiffMins)
+ }
+
+ @JvmStatic
+ fun createPassaggioGTTString(timeInput: String, realtime: Boolean): String {
+ val time = timeInput.trim { it <= ' ' }
+ if (time.contains("*")) {
+ if (realtime) return time
+ else return time.substring(0, time.length - 1)
+ } else {
+ if (realtime) return time + "*"
+ else return time
+ }
+ }
+
+ private fun makePassaggioGTT(hour: Int, minutes: Int, realtime: Boolean): String {
+ val sb = StringBuilder()
+ sb.append(String.format(Locale.ITALIAN, "%02d", hour)).append(":")
+ .append(String.format(Locale.ITALIAN, "%02d", minutes))
+ if (realtime) sb.append("*")
+ return sb.toString()
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Route.java b/app/src/main/java/it/reyboz/bustorino/backend/Route.java
index 8a13092..42725f4 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/Route.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Route.java
@@ -1,510 +1,521 @@
/*
BusTO (backend components)
Copyright (C) 2016 Ludovico Pavesi
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.List;
public class Route implements Comparable, Parcelable {
final static int[] reduced_week = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY};
//final static int[] feriali = {Calendar.MONDAY,Calendar.TUESDAY,Calendar.WEDNESDAY,Calendar.THURSDAY,Calendar.FRIDAY,Calendar.SATURDAY};
final static int[] weekend = {Calendar.SUNDAY,Calendar.SATURDAY};
private final static int BRANCHID_MISSING = -1;
private final String name;
private @Nullable String displayCode = null;
public String destinazione;
public final List passaggi;
//create a copy of the list, so that
private List sortedPassaggi;
public final Type type;
public String description;
//ordered list of stops, from beginning to end of line
private List stopsList = null;
public int branchid = BRANCHID_MISSING;
public int[] serviceDays ={};
//0=>feriale, 1=>festivo -2=>unknown
public FestiveInfo festivo = FestiveInfo.UNKNOWN;
private @Nullable String gtfsId;
public enum Type { // "long distance" sono gli extraurbani.
BUS(1), LONG_DISTANCE_BUS(2), METRO(3), RAILWAY(4), TRAM(5), UNKNOWN(-2);
//TODO: decide to give some special parameter to each field
private final int code;
Type(int code){
this.code = code;
}
public int getCode(){
return this.code;
}
@Nullable
public static Type fromCode(int i){
return switch (i) {
case 1 -> BUS;
case 2 -> LONG_DISTANCE_BUS;
case 3 -> METRO;
case 4 -> RAILWAY;
case 5 -> TRAM;
case -2 -> UNKNOWN;
default -> null;
};
}
}
public enum FestiveInfo{
FESTIVO(1),FERIALE(0),UNKNOWN(-2);
private final int code;
FestiveInfo(int code){
this.code = code;
}
public int getCode() {
return code;
}
public static FestiveInfo fromCode(int i){
return switch (i) {
case -2 -> UNKNOWN;
case 0 -> FERIALE;
case 1 -> FESTIVO;
default -> UNKNOWN;
};
}
}
/**
* Constructor.
*
* @param name route ID
* @param destinazione terminus\end of line
* @param type bus, long distance bus, underground, and so on
* @param passaggi timetable, a good choice is an ArrayList of size 6
* @param description the description of the line, usually given by the FiveTAPIFetcher
* @see Palina Palina.addRoute() method
*/
public Route(String name, String destinazione, List passaggi, Type type, String description) {
this.name = name;
this.destinazione = parseDestinazione(destinazione);
this.passaggi = passaggi;
this.type = type;
this.description = description;
}
/**
* Constructor used in GTTJSONFetcher, see above
*/
public Route(String name, String destinazione, Type type, List passaggi) {
this(name,destinazione,passaggi,type,null);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,Type t,String description){
this(name,null,new ArrayList<>(),t,description);
}
/**
* Constructor used by the FiveTAPIFetcher
* @param name stop Name
* @param t optional type
* @param description line rough description
*/
public Route(String name,String destinazione, String description, Type t){
this(name,destinazione,new ArrayList<>(),t,description);
}
/**
* Exactly what it says on the tin.
*
* @return times from the timetable
*/
public List getPassaggi() {
return this.passaggi;
}
public void setStopsList(List stopsList) {
this.stopsList = Collections.unmodifiableList(stopsList);
}
public List getStopsList(){
return this.stopsList;
}
+ /**
+ * Add internally a passaggio if it is not null
+ * @param passaggio
+ * @return 1 if it is added, 0 if not
+ */
+ private int addPassaggioCheck(Passaggio passaggio){
+ if (passaggio!=null) {
+ this.passaggi.add(passaggio);
+ return 1;
+ } else return 0;
+ }
/**
* Adds a time (passaggio) to the timetable for this route
*
* @param TimeGTT time in GTT format (e.g. "11:22*")
*/
- public void addPassaggio(String TimeGTT, Passaggio.Source source) {
- this.passaggi.add(new Passaggio(TimeGTT, source));
+ public int addPassaggio(String TimeGTT, Passaggio.Source source) {
+ return addPassaggioCheck(Passaggio.newInstance(TimeGTT, source));
}
//Overloaded
- public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) {
- this.passaggi.add(new Passaggio(hour, minutes, realtime, source));
+ public int addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) {
+ return addPassaggioCheck(Passaggio.newInstance(hour, minutes, realtime, source, null));
}
public static Route.Type getTypeFromSymbol(String route) {
switch (route) {
case "M":
return Route.Type.METRO;
case "T":
return Route.Type.RAILWAY;
}
// default with case "B"
return Route.Type.BUS;
}
private String parseDestinazione(String direzione){
if(direzione==null) return null;
//trial to add space to the parenthesis
String[] exploded = direzione.split("\\(");
if(exploded.length>1){
StringBuilder sb = new StringBuilder();
sb.append(exploded[0]);
for(int i=1; i arrivals;
int max;
if(sort){
if(sortedPassaggi==null){
sortedPassaggi = new ArrayList<>(passaggi.size());
sortedPassaggi.addAll(passaggi);
Collections.sort(sortedPassaggi);
}
arrivals = sortedPassaggi;
} else arrivals = passaggi;
max = Math.min(start_idx + number, arrivals.size());
for(int j= start_idx; j0){
this.passaggi.addAll(other.passaggi);
}
if(this.destinazione == null && other.destinazione!=null) {
this.destinazione = other.destinazione;
adjusted = true;
}
if(!this.isBranchIdValid() && other.isBranchIdValid()) {
this.branchid = other.branchid;
adjusted = true;
}
if(this.festivo == Route.FestiveInfo.UNKNOWN && other.festivo!= Route.FestiveInfo.UNKNOWN){
this.festivo = other.festivo;
adjusted = true;
}
if(other.description!=null && (this.description==null ||
(this.festivo == FestiveInfo.FERIALE && this.description.contains("festivo")) ||
(this.festivo == FestiveInfo.FESTIVO && this.description.contains("feriale")) ) ) {
this.description = other.description;
}
return adjusted;
}
public String getRouteLongDisplayName() {
String routeName = FiveTNormalizer.routeInternalToDisplay(this.name);
if (routeName == null) {
routeName = this.displayCode;
}
return routeName;
}
// ---- Parcelable implem ---
protected Route(Parcel in) {
name = in.readString();
displayCode = in.readByte() == 0 ? null : in.readString();
destinazione = in.readString();
passaggi = in.createTypedArrayList(Passaggio.CREATOR);
type = Type.valueOf(in.readString());
description = in.readString();
if (in.readByte() == 0) {
stopsList = null;
} else {
stopsList = in.createStringArrayList();
}
branchid = in.readInt();
serviceDays = in.createIntArray();
festivo = FestiveInfo.valueOf(in.readString());
gtfsId = in.readByte() == 0 ? null : in.readString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
if (displayCode == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
dest.writeString(displayCode);
}
dest.writeString(destinazione);
dest.writeTypedList(passaggi);
dest.writeString(type.name());
dest.writeString(description);
if (stopsList == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
dest.writeStringList(stopsList);
}
dest.writeInt(branchid);
dest.writeIntArray(serviceDays);
dest.writeString(festivo.name());
if (gtfsId == null) {
dest.writeByte((byte) 0);
} else {
dest.writeByte((byte) 1);
dest.writeString(gtfsId);
}
}
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator CREATOR = new Creator<>() {
@Override
public Route createFromParcel(Parcel in) {
return new Route(in);
}
@Override
public Route[] newArray(int size) {
return new Route[size];
}
};
}
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
index 55545dc..9d0de14 100644
--- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
+++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
@@ -1,421 +1,430 @@
/*
BusTO - Backend components
Copyright (C) 2021 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.backend.mato
import android.content.Context
import android.util.Log
import com.android.volley.DefaultRetryPolicy
import com.android.volley.toolbox.RequestFuture
import it.reyboz.bustorino.backend.*
import it.reyboz.bustorino.data.gtfs.GtfsAgency
import it.reyboz.bustorino.data.gtfs.GtfsFeed
import it.reyboz.bustorino.data.gtfs.GtfsRoute
import it.reyboz.bustorino.data.gtfs.MatoPattern
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicReference
import kotlin.collections.ArrayList
open class MatoAPIFetcher(
private val minNumPassaggi: Int
) : ArrivalsFetcher {
var appContext: Context? = null
set(value) {
field = value!!.applicationContext
}
constructor(): this(DEF_MIN_NUMPASSAGGI)
override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina {
stopID!!
val now = Calendar.getInstance().time
var numMinutes = 60
var palina = Palina(stopID)
var trials = 0
val numDepartures = 8
var moreTime = false
var palinaOK = false
while (trials <20 && !palinaOK) {
//numDepartures+=2
if (moreTime) numMinutes *= 2 //duplicate time
val future = RequestFuture.newFuture()
val request = MapiArrivalRequest(stopID, now, numMinutes * 60, numDepartures, res, future, future)
if (appContext == null || res == null) {
Log.e("BusTO:MatoAPIFetcher", "ERROR: Given null context or null result ref")
return Palina(stopID)
}
val requestQueue = NetworkVolleyManager.getInstance(appContext).requestQueue
request.setTag(getVolleyReqTag(MatoQueries.QueryType.ARRIVALS))
requestQueue.add(request)
moreTime = false
try {
val palinaResult = future.get(15, TimeUnit.SECONDS)
if (palinaResult!=null) {
palina = palinaResult
if(palina.totalNumberOfPassages < minNumPassaggi && numMinutes < MAX_MINUTES_SEARCH) {
moreTime = true
} else{
palinaOK = true
}
} else{
Log.d(DEBUG_TAG, "Result palina is null")
}
} catch (e: InterruptedException) {
e.printStackTrace()
res.set(Fetcher.Result.PARSER_ERROR)
} catch (e: ExecutionException) {
e.printStackTrace()
if (res.get() == Fetcher.Result.OK)
res.set(Fetcher.Result.SERVER_ERROR)
} catch (e: TimeoutException) {
res.set(Fetcher.Result.CONNECTION_ERROR)
e.printStackTrace()
}
trials++
}
return palina
}
override fun getSourceForFetcher(): Passaggio.Source {
return Passaggio.Source.MatoAPI
}
companion object{
const val VOLLEY_TAG = "MatoAPIFetcher"
const val DEBUG_TAG = "BusTO:MatoAPIFetcher"
const val DEF_MIN_NUMPASSAGGI = 5
const val MAX_MINUTES_SEARCH = 24*60 // a day in minutes
val REQ_PARAMETERS = mapOf(
"Content-Type" to "application/json; charset=utf-8",
"DNT" to "1",
"Host" to "mapi.5t.torino.it")
private val longRetryPolicy = DefaultRetryPolicy(10000,5,DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)
fun getVolleyReqTag(type: MatoQueries.QueryType): String{
return when (type){
MatoQueries.QueryType.ALL_STOPS -> VOLLEY_TAG +"_AllStops"
MatoQueries.QueryType.ARRIVALS -> VOLLEY_TAG+"_Arrivals"
MatoQueries.QueryType.FEEDS -> VOLLEY_TAG +"_Feeds"
MatoQueries.QueryType.ROUTES -> VOLLEY_TAG +"_AllRoutes"
MatoQueries.QueryType.PATTERNS_FOR_ROUTES -> VOLLEY_TAG + "_PatternsForRoute"
MatoQueries.QueryType.TRIP -> VOLLEY_TAG+"_Trip"
}
}
/**
* Get stops from the MatoAPI, set [res] accordingly
*/
fun getAllStopsGTT(context: Context, res: AtomicReference?): List{
val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue
val future = RequestFuture.newFuture>()
val request = VolleyAllStopsRequest(future, future)
request.tag = getVolleyReqTag(MatoQueries.QueryType.ALL_STOPS)
request.retryPolicy = longRetryPolicy
requestQueue.add(request)
var palinaList:List = mutableListOf()
try {
palinaList = future.get(120, TimeUnit.SECONDS)
res?.set(Fetcher.Result.OK)
}catch (e: InterruptedException) {
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
} catch (e: ExecutionException) {
e.printStackTrace()
res?.set(Fetcher.Result.SERVER_ERROR)
} catch (e: TimeoutException) {
res?.set(Fetcher.Result.CONNECTION_ERROR)
e.printStackTrace()
}
return palinaList
}
/*
fun makeRequest(type: QueryType?, variables: JSONObject) : String{
type.let {
val requestData = JSONObject()
when (it){
QueryType.ARRIVALS ->{
requestData.put("operationName","AllStopsDirect")
requestData.put("variables", variables)
requestData.put("query", MatoQueries.QUERY_ARRIVALS)
}
else -> {
//TODO all other cases
}
}
//todo make the request...
//https://pablobaxter.github.io/volley-docs/com/android/volley/toolbox/RequestFuture.html
//https://stackoverflow.com/questions/16904741/can-i-do-a-synchronous-request-with-volley
}
return ""
}
*/
fun parseStopJSON(jsonStop: JSONObject): Palina{
val latitude = jsonStop.getDouble("lat")
val longitude = jsonStop.getDouble("lon")
val palina = Palina(
jsonStop.getString("code"),
jsonStop.getString("name"),
null, null, latitude, longitude,
jsonStop.getString("gtfsId")
)
val routesStoppingJSON = jsonStop.getJSONArray("routes")
val baseRoutes = mutableListOf()
// get all the possible routes
for (i in 0 until routesStoppingJSON.length()){
val routeBaseInfo = routesStoppingJSON.getJSONObject(i)
val r = Route(routeBaseInfo.getString("shortName"), Route.Type.UNKNOWN,"")
r.gtfsId = routeBaseInfo.getString("gtfsId").trim()
baseRoutes.add(r)
}
if (jsonStop.has("desc")){
palina.location = jsonStop.getString("desc")
}
//there is also "zoneId" which is the zone of the stop (0-> city, etc)
if(jsonStop.has("stoptimesForPatterns")) {
val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns")
for (i in 0 until routesStopTimes.length()) {
val patternJSON = routesStopTimes.getJSONObject(i)
val mRoute = parseRouteStoptimesJSON(patternJSON)
//Log.d("BusTO-MapiFetcher")
//val directionId = patternJSON.getJSONObject("pattern").getInt("directionId")
//TODO: use directionId
palina.addRoute(mRoute)
for (r in baseRoutes) {
if (mRoute.gtfsId != null && r.gtfsId.equals(mRoute.gtfsId)) {
baseRoutes.remove(r)
break
}
}
}
}
for (noArrivalRoute in baseRoutes){
palina.addRoute(noArrivalRoute)
}
//val gtfsRoutes = mutableListOf<>()
return palina
}
private fun parseRouteStoptimesJSON(jsonPatternWithStops: JSONObject): Route{
val patternJSON = jsonPatternWithStops.getJSONObject("pattern")
val routeJSON = patternJSON.getJSONObject("route")
val passaggiJSON = jsonPatternWithStops.getJSONArray("stoptimes")
val gtfsId = routeJSON.getString("gtfsId").trim()
val passages = mutableListOf()
for( i in 0 until passaggiJSON.length()){
val stoptime = passaggiJSON.getJSONObject(i)
val scheduledTime = stoptime.getInt("scheduledArrival")
val realtimeTime = stoptime.getInt("realtimeArrival")
val realtime = stoptime.getBoolean("realtime")
- passages.add(
+
+ val passaggio = Passaggio.newInstance(realtimeTime, realtime,
+ (realtimeTime-scheduledTime), Passaggio.Source.MatoAPI)
+
+ passaggio?.let{
+ passages.add(it)
+ }
+ /*passages.add(
Passaggio(realtimeTime,realtime, realtimeTime-scheduledTime,
Passaggio.Source.MatoAPI)
)
+
+ */
}
var routeType = Route.Type.UNKNOWN
if (gtfsId[gtfsId.length-1] == 'E')
routeType = Route.Type.LONG_DISTANCE_BUS
else when( routeJSON.getString("mode").trim()){
"BUS" -> routeType = Route.Type.BUS
"TRAM" -> routeType = Route.Type.TRAM
}
val route = Route(
FiveTNormalizer.filterFullStarName(routeJSON.getString("shortName")),
patternJSON.getString("headsign"),
routeType,
passages,
)
route.setGtfsId(gtfsId)
return route
}
fun makeRequestParameters(requestName:String, variables: JSONObject, query: String): JSONObject{
val data = JSONObject()
data.put("operationName", requestName)
data.put("variables", variables)
data.put("query", query)
return data
}
fun getFeedsAndAgencies(context: Context, res: AtomicReference?):
Pair, ArrayList> {
val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue
val future = RequestFuture.newFuture()
val request = MatoVolleyJSONRequest(MatoQueries.QueryType.FEEDS, JSONObject(), future, future)
request.setRetryPolicy(longRetryPolicy)
request.tag = getVolleyReqTag(MatoQueries.QueryType.FEEDS)
requestQueue.add(request)
val feeds = ArrayList()
val agencies = ArrayList()
var outObj = ""
try {
val resObj = future.get(120,TimeUnit.SECONDS)
outObj = resObj.toString(1)
val feedsJSON = resObj.getJSONArray("feeds")
for (i in 0 until feedsJSON.length()){
val resTup = ResponseParsing.parseFeedJSON(feedsJSON.getJSONObject(i))
feeds.add(resTup.first)
agencies.addAll(resTup.second)
}
} catch (e: InterruptedException) {
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
} catch (e: ExecutionException) {
e.printStackTrace()
res?.set(Fetcher.Result.SERVER_ERROR)
} catch (e: TimeoutException) {
res?.set(Fetcher.Result.CONNECTION_ERROR)
e.printStackTrace()
} catch (e: JSONException){
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
Log.e(DEBUG_TAG, "Downloading feeds: $outObj")
}
return Pair(feeds,agencies)
}
fun getRoutes(context: Context, res: AtomicReference?):
ArrayList{
val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue
val future = RequestFuture.newFuture()
val params = JSONObject()
params.put("feeds","gtt")
val request = MatoVolleyJSONRequest(MatoQueries.QueryType.ROUTES, params, future, future)
request.tag = getVolleyReqTag(MatoQueries.QueryType.ROUTES)
request.retryPolicy = longRetryPolicy
requestQueue.add(request)
val routes = ArrayList()
var outObj = ""
try {
val resObj = future.get(120,TimeUnit.SECONDS)
outObj = resObj.toString(1)
val routesJSON = resObj.getJSONArray("routes")
for (i in 0 until routesJSON.length()){
val route = ResponseParsing.parseRouteJSON(routesJSON.getJSONObject(i))
routes.add(route)
}
} catch (e: InterruptedException) {
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
} catch (e: ExecutionException) {
e.printStackTrace()
res?.set(Fetcher.Result.SERVER_ERROR)
} catch (e: TimeoutException) {
res?.set(Fetcher.Result.CONNECTION_ERROR)
e.printStackTrace()
} catch (e: JSONException){
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
Log.e(DEBUG_TAG, "Downloading feeds: $outObj")
}
return routes
}
fun getPatternsWithStops(context: Context, routesGTFSIds: MutableCollection, res: AtomicReference?): ArrayList{
val requestQueue = NetworkVolleyManager.getInstance(context).requestQueue
val future = RequestFuture.newFuture()
val params = JSONObject()
for (r in routesGTFSIds){
if(r.isEmpty()) routesGTFSIds.remove(r)
}
val routes = JSONArray(routesGTFSIds)
params.put("routes",routes)
val request = MatoVolleyJSONRequest(MatoQueries.QueryType.PATTERNS_FOR_ROUTES, params, future, future)
request.retryPolicy = longRetryPolicy
request.tag = getVolleyReqTag(MatoQueries.QueryType.PATTERNS_FOR_ROUTES)
requestQueue.add(request)
val patterns = ArrayList()
var resObj = JSONObject()
try {
resObj = future.get(60,TimeUnit.SECONDS)
//outObj = resObj.toString(1)
val routesJSON = resObj.getJSONArray("routes")
for (i in 0 until routesJSON.length()){
val patternList = ResponseParsing.parseRoutePatternsStopsJSON(routesJSON.getJSONObject(i))
patterns.addAll(patternList)
}
} catch (e: InterruptedException) {
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
} catch (e: ExecutionException) {
e.printStackTrace()
res?.set(Fetcher.Result.SERVER_ERROR)
} catch (e: TimeoutException) {
res?.set(Fetcher.Result.CONNECTION_ERROR)
e.printStackTrace()
} catch (e: JSONException){
e.printStackTrace()
res?.set(Fetcher.Result.PARSER_ERROR)
Log.e(DEBUG_TAG, "Got result: $resObj")
}
return patterns
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java b/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java
index 9fc4adf..30f1dfb 100644
--- a/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java
+++ b/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java
@@ -1,38 +1,21 @@
package it.reyboz.bustorino.util;
import java.util.Comparator;
import it.reyboz.bustorino.backend.Passaggio;
/**
* Sorter of passaggi, giving the arrival times that are in real time first
*/
public class PassaggiSorter implements Comparator {
@Override
public int compare(Passaggio p1, Passaggio p2) {
- if (p1.isInRealTime){
- if(p2.isInRealTime){
- //compare times
- return p1.getMinutesDiff(p2);
- }
- else {
- return -2;
- }
- } else{
- if(p2.isInRealTime){
- // other should come first
- return 2;
- } else return p1.getMinutesDiff(p2);
+ if (p1.isInRealTime() != p2.isInRealTime()){
+ if(p1.isInRealTime()) return -1;
+ else return 1;
}
+ return p1.getArrivalTime().compareTo(p2.getArrivalTime());
}
- @Override
- public boolean equals(Object o) {
- boolean equal= this.equals(o);
- if (equal) return true;
- else{
- return o instanceof PassaggiSorter;
- }
- }
}
diff --git a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java
index 2eb0f23..7012d7d 100644
--- a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java
+++ b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java
@@ -1,72 +1,77 @@
/*
BusTO (util)
Copyright (C) 2019 Fabio Mazza
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
package it.reyboz.bustorino.util;
import android.location.Location;
import androidx.core.util.Pair;
import android.util.Log;
import it.reyboz.bustorino.backend.*;
+import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class RoutePositionSorter implements Comparator> {
private final double latPos, longPos;
private final double minutialmetro = 6.0/100; //v = 5km/h
private final double distancemultiplier = 2./3;
public RoutePositionSorter(double latitude, double longitude){
latPos = latitude;
longPos = longitude;
}
public RoutePositionSorter(GPSPoint position){
this(position.getLatitude(), position.getLongitude());
}
@Override
public int compare(Pair pair1, Pair pair2) throws NullPointerException{
int delta = 0;
final Stop stop1 = pair1.first, stop2 = pair2.first;
double dist1 = utils.measuredistanceBetween(latPos,longPos,
stop1.getLatitude(),stop1.getLongitude());
double dist2 = utils.measuredistanceBetween(latPos,longPos,
stop2.getLatitude(),stop2.getLongitude());
final List passaggi1 = pair1.second.passaggi,
passaggi2 = pair2.second.passaggi;
if(passaggi1.size()<=0 || passaggi2.size()<=0){
Log.e("ArrivalsStopAdapter","Cannot compare: No arrivals in one of the stops");
} else {
Collections.sort(passaggi1);
Collections.sort(passaggi2);
- int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh;
+ /*int deltaOre = passaggi1.get(0).hh-passaggi2.get(0).hh;
if(deltaOre>12)
deltaOre -= 24;
else if (deltaOre<-12)
deltaOre += 24;
delta+=deltaOre*60 + passaggi1.get(0).mm-passaggi2.get(0).mm;
+
+ */
+
+ delta = (int) passaggi1.get(0).getDifferenceMinutes(passaggi2.get(0));
}
delta += (int)((dist1 -dist2)*minutialmetro*distancemultiplier);
return delta;
}
@Override
public boolean equals(Object obj) {
return obj instanceof RoutePositionSorter;
}
}
diff --git a/app/src/test/java/it/reyboz/bustorino/util/ArrivalTimesTest.java b/app/src/test/java/it/reyboz/bustorino/util/ArrivalTimesTest.java
new file mode 100644
index 0000000..87652da
--- /dev/null
+++ b/app/src/test/java/it/reyboz/bustorino/util/ArrivalTimesTest.java
@@ -0,0 +1,29 @@
+package it.reyboz.bustorino.util;
+
+import it.reyboz.bustorino.backend.Passaggio;
+import org.junit.Test;
+import static org.junit.Assert.*;
+public class ArrivalTimesTest {
+
+ @Test
+ public void arrivalTimesTest(){
+ Passaggio pass1 = Passaggio.newInstance(20,12,true, Passaggio.Source.GTTJSON,null);
+
+ Passaggio pass2 = Passaggio.newInstance(1,12,true, Passaggio.Source.GTTJSON,null);
+
+ assertNotNull(pass1);
+ assertNotNull(pass2);
+ assertTrue(pass1.compareTo(pass2) < 0);
+ }
+
+ @Test
+ public void arrivalTimesWithTimeGTT(){
+ Passaggio pass1 = Passaggio.newInstance("23:10*", Passaggio.Source.GTTJSON);
+
+ Passaggio pass2 = Passaggio.newInstance(1,12,true, Passaggio.Source.GTTJSON,null);
+
+ assertNotNull(pass1);
+ assertNotNull(pass2);
+ assertTrue(pass2.getDifferenceMinutes(pass1) > 0);
+ }
+}