diff --git a/app/build.gradle b/app/build.gradle
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -59,6 +59,7 @@
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
+ coreLibraryDesugaringEnabled true
}
kotlin {
jvmToolchain 17
@@ -108,6 +109,7 @@
}
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"
diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
--- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTAPIFetcher.java
@@ -112,7 +112,7 @@
for(int j=0;j.
- */
-
-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
--- /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
--- a/app/src/main/java/it/reyboz/bustorino/backend/Route.java
+++ b/app/src/main/java/it/reyboz/bustorino/backend/Route.java
@@ -156,17 +156,28 @@
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) {
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
--- a/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
+++ b/app/src/main/java/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt
@@ -247,10 +247,19 @@
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')
diff --git a/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java b/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java
--- a/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java
+++ b/app/src/main/java/it/reyboz/bustorino/util/PassaggiSorter.java
@@ -11,28 +11,11 @@
@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
--- a/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java
+++ b/app/src/main/java/it/reyboz/bustorino/util/RoutePositionSorter.java
@@ -23,6 +23,7 @@
import it.reyboz.bustorino.backend.*;
+import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -54,12 +55,16 @@
} 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;
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
--- /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);
+ }
+}