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); + } +}