diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -51,7 +51,7 @@ defaultConfig { applicationId "it.reyboz.bustorino" - minSdkVersion 15 + minSdkVersion 16 targetSdkVersion 29 versionCode 35 versionName "1.15.4" @@ -103,12 +103,12 @@ implementation "androidx.work:work-runtime:$work_version" implementation "com.google.android.material:material:1.4.0" - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' - implementation 'com.android.volley:volley:1.2.0' + implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // ACRA diff --git a/res/layout/entry_bus_line_passage.xml b/res/layout/entry_bus_line_passage.xml --- a/res/layout/entry_bus_line_passage.xml +++ b/res/layout/entry_bus_line_passage.xml @@ -14,7 +14,9 @@ android:background="@drawable/route_background_bus" android:gravity="center" android:textColor="@color/grey_100" - android:textSize="21sp"> + android:textSize="21sp" + android:layout_marginEnd="4dp" + android:layout_marginRight="4dp"> diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -131,6 +131,8 @@ App GTT Sito GTT Sito 5T Torino + App Muoversi a Torino + Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari @@ -154,5 +156,13 @@ Mappa Ricerca fermate Versione app + Orari di arrivo + + Mostra direzioni in maiuscolo + + Non cambiare + Tutto in maiuscolo + Solo la prima lettera maiuscola + diff --git a/res/values/pref_keys.xml b/res/values/pref_keys.xml --- a/res/values/pref_keys.xml +++ b/res/values/pref_keys.xml @@ -4,4 +4,6 @@ pref_num_recents pref_radius_recents pref_exp_features + + pref_arrival_times_capitalize \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -51,6 +51,7 @@ \n Long press on Arrivals source to change the source of the arrival times GOT IT! + Arrival times Welcome! @@ -144,6 +145,7 @@ GTT App GTT Website 5T Torino website + Muoversi a Torino app Changing arrival times source… Long press to change the source of arrivals @@ -174,4 +176,17 @@ Search by stop + + Capitalize directions + + Do not change arrivals directions + Capitalize everything + Capitalize only first letter + + + KEEP + CAPITALIZE_ALL + CAPITALIZE_FIRST + + diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -49,4 +49,13 @@ android:key="version" /> --> + + + + diff --git a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java --- a/src/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/src/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -19,6 +19,10 @@ import android.content.Context; import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import android.content.SharedPreferences; +import android.os.Build; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -27,7 +31,9 @@ import android.widget.TextView; import java.util.List; +import java.util.Locale; +import it.reyboz.bustorino.BuildConfig; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; @@ -43,7 +49,7 @@ * @author Valerio Bozzolan * @author Ludovico Pavesi */ -public class PalinaAdapter extends ArrayAdapter { +public class PalinaAdapter extends ArrayAdapter implements SharedPreferences.OnSharedPreferenceChangeListener { private LayoutInflater li; private static int row_layout = R.layout.entry_bus_line_passage; private static final int metroBg = R.drawable.route_background_metro; @@ -52,6 +58,10 @@ private static final int busIcon = R.drawable.bus; private static final int trainIcon = R.drawable.subway; private static final int tramIcon = R.drawable.tram; + + private final String KEY_CAPITALIZE; + private Capitalize capit = Capitalize.DO_NOTHING; + //private static final int cityIcon = R.drawable.city; // hey look, a pattern! @@ -60,10 +70,29 @@ TextView rowRouteDestination; TextView rowRouteTimetable; } + private static Capitalize getCapitalize(SharedPreferences shPr, String key){ + String capitalize = shPr.getString(key, ""); + + switch (capitalize.trim()){ + case "KEEP": + return Capitalize.DO_NOTHING; + case "CAPITALIZE_ALL": + return Capitalize.ALL; + + case "CAPITALIZE_FIRST": + return Capitalize.FIRST; + } + return Capitalize.DO_NOTHING; + } public PalinaAdapter(Context context, Palina p) { super(context, row_layout, p.queryAllRoutes()); li = LayoutInflater.from(context); + sort(new RouteSorterByArrivalTime()); + KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); + SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); + defSharPref.registerOnSharedPreferenceChangeListener(this); + this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE); } /** @@ -101,10 +130,30 @@ vh.rowStopIcon.setText(route.getNameForDisplay()); if(route.destinazione==null || route.destinazione.length() == 0) { vh.rowRouteDestination.setVisibility(View.GONE); + // move around the route timetable + final ViewGroup.MarginLayoutParams pars = (ViewGroup.MarginLayoutParams) vh.rowRouteTimetable.getLayoutParams(); + if (pars!=null){ + pars.topMargin = 16; + if(Build.VERSION.SDK_INT >= 17) + pars.setMarginStart(20); + pars.leftMargin = 20; + } } else { // View Holder Pattern(R) renders each element from a previous one: if the other one had an invisible rowRouteDestination, we need to make it visible. vh.rowRouteDestination.setVisibility(View.VISIBLE); - vh.rowRouteDestination.setText(route.destinazione); + String dest = route.destinazione; + switch (capit){ + case ALL: + dest = route.destinazione.toUpperCase(Locale.ROOT); + break; + case FIRST: + dest = utils.toTitleCase(route.destinazione, true); + break; + case DO_NOTHING: + default: + + } + vh.rowRouteDestination.setText(dest); } switch (route.type) { @@ -138,10 +187,25 @@ List passaggi = route.passaggi; if(passaggi.size() == 0) { vh.rowRouteTimetable.setText(R.string.no_passages); + } else { vh.rowRouteTimetable.setText(route.getPassaggiToString()); } return convertView; } + + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(KEY_CAPITALIZE)){ + capit = getCapitalize(sharedPreferences, KEY_CAPITALIZE); + + notifyDataSetChanged(); + } + } + + enum Capitalize{ + DO_NOTHING, ALL, FIRST + } } diff --git a/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt b/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/adapters/RouteSorterByArrivalTime.kt @@ -0,0 +1,29 @@ +package it.reyboz.bustorino.adapters + +import it.reyboz.bustorino.backend.Route + +class RouteSorterByArrivalTime : Comparator { + + override fun compare(route1: Route?, route2: Route?): Int { + if (route1 == null){ + if(route2 == null) return 0 + else return 2; + } else if (route2 == null){ + return -2; + } + val passaggi1 = route1.passaggi + val passaggi2 = route2.passaggi + // handle the case of midnight + if (passaggi1 == null || passaggi1.size == 0){ + if (passaggi2 == null || passaggi2.size == 0) return 0 + else return 2 + } else if (passaggi2 == null || passaggi2.size == 0){ + return -2 + } + passaggi1.sort() + passaggi2.sort() + + return passaggi1[0].compareTo(passaggi2[0]) + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java --- a/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java +++ b/src/it/reyboz/bustorino/backend/ArrivalsFetcher.java @@ -22,20 +22,10 @@ import java.util.concurrent.atomic.AtomicReference; +/** + * Fetcher interface to describe ways to get information on arrival times + */ public interface ArrivalsFetcher extends Fetcher { -// /** -// * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. -// * Don't call this in UI thread! -// * -// * @param stopID stop ID, in normalized form. -// * @param routeID route ID, in normalized form. -// * @param res result code (will be set by this method) -// * @return arrival times -// * @see it.reyboz.bustorino.backend.Fetcher.result -// * @see FiveTNormalizer -// */ -// Palina ReadArrivalTimesRoute(String stopID, String routeID, AtomicReference res); - /** * Reads arrival times from a (hopefully) real-time source, e.g. the GTT website. * Don't call this in UI thread! diff --git a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java --- a/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTAPIFetcher.java @@ -18,6 +18,7 @@ package it.reyboz.bustorino.backend; import androidx.annotation.Nullable; + import android.util.Log; import it.reyboz.bustorino.data.GTTInfoInject; import org.json.JSONArray; diff --git a/src/it/reyboz/bustorino/backend/Palina.java b/src/it/reyboz/bustorino/backend/Palina.java --- a/src/it/reyboz/bustorino/backend/Palina.java +++ b/src/it/reyboz/bustorino/backend/Palina.java @@ -21,6 +21,7 @@ import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.Calendar; @@ -47,6 +48,15 @@ s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude()); } + public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, + @Nullable String location, + @Nullable Double lat, @Nullable Double lon) { + super(ID, name, userName, location, null, null, lat, lon); + } + + public Palina(@Nullable String name, @NonNull String ID, @Nullable String location, @Nullable Route.Type type, @Nullable List routesThatStopHere) { + super(name, ID, location, type, routesThatStopHere); + } /** * Adds a timetable entry to a route. @@ -373,5 +383,26 @@ if (found) mergeDuplicateRoutes(startidx); else mergeDuplicateRoutes(startidx+1); } + + public int getTotalNumberOfPassages(){ + + int tot = 0; + if(routes==null) + return tot; + for(Route r: routes){ + tot += r.numPassaggi(); + } + return tot; + } + public int getMinNumberOfPassages(){ + if (routes == null) return 0; + + int min = Integer.MAX_VALUE; + if( routes.size() == 0) min = 0; + else for (Route r : routes){ + min = Math.min(min,r.numPassaggi()); + } + return min; + } //private void mergeRoute } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Passaggio.java b/src/it/reyboz/bustorino/backend/Passaggio.java --- a/src/it/reyboz/bustorino/backend/Passaggio.java +++ b/src/it/reyboz/bustorino/backend/Passaggio.java @@ -19,8 +19,12 @@ package it.reyboz.bustorino.backend; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import android.util.Log; +import java.util.Locale; + public final class Passaggio implements Comparable { private static final int UNKNOWN_TIME = -3; @@ -28,6 +32,7 @@ private final String passaggioGTT; public final int hh,mm; + private @Nullable Integer realtimeDifference; public final boolean isInRealTime; public final Source source; @@ -88,6 +93,7 @@ this.hh = hour; this.mm = min; this.isInRealTime = realtime; + } } @@ -95,6 +101,7 @@ this.hh = hour; this.mm = minutes; this.isInRealTime = realtime; + if (!realtime) realtimeDifference = 0; this.source = sorgente; //Build the passaggio string StringBuilder sb = new StringBuilder(); @@ -113,6 +120,24 @@ 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) { @@ -133,16 +158,17 @@ // we should take into account if one is in real time and the other isn't, shouldn't we? if (other.isInRealTime) { - ++diff; + diff+=2; } if (this.isInRealTime) { - --diff; + diff -=2; } - //TODO: separate Realtime and Non-Realtime, especially for the GTTJSONFetcher return diff; } } + + // // @Override // public String toString() { @@ -154,6 +180,6 @@ // } // } public enum Source{ - FiveTAPI,GTTJSON,FiveTScraper, UNDETERMINED + FiveTAPI,GTTJSON,FiveTScraper,MatoAPI, UNDETERMINED } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/Route.java b/src/it/reyboz/bustorino/backend/Route.java --- a/src/it/reyboz/bustorino/backend/Route.java +++ b/src/it/reyboz/bustorino/backend/Route.java @@ -45,6 +45,7 @@ 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. @@ -242,6 +243,12 @@ return sb.toString(); } + public int numPassaggi(){ + if (passaggi==null) + return 0; + return passaggi.size(); + } + @Override public int compareTo(@NonNull Route other) { @@ -267,6 +274,12 @@ if (res != 0) { return res; } + // compare gtfsID + if (this.gtfsId != null && other.gtfsId!=null){ + res = this.gtfsId.compareTo(other.gtfsId); + if (res!=0) return 0; + } + } // try comparing their destination @@ -293,6 +306,15 @@ return 0; } + @Nullable + public String getGtfsId() { + return gtfsId; + } + + public void setGtfsId(@Nullable String gtfsId) { + this.gtfsId = gtfsId; + } + public boolean isBranchIdValid(){ return branchid!=BRANCHID_MISSING; } @@ -306,11 +328,14 @@ if(description!=null && r.description!=null) if(!description.trim().equals(r.description.trim())) return false; + if(destinazione!=null && r.destinazione!=null){ if(!this.destinazione.trim().equals(r.destinazione.trim())) // they are not the same return false; } + if(gtfsId!=null && r.gtfsId!=null && !(gtfsId.trim().equals(r.gtfsId.trim()))) + return false; //check stops list if(this.stopsList!=null && r.stopsList!=null){ int sizeDiff = this.stopsList.size()-r.stopsList.size(); diff --git a/src/it/reyboz/bustorino/backend/Stop.java b/src/it/reyboz/bustorino/backend/Stop.java --- a/src/it/reyboz/bustorino/backend/Stop.java +++ b/src/it/reyboz/bustorino/backend/Stop.java @@ -46,6 +46,9 @@ private @Nullable String routesThatStopHereString = null; private @Nullable String absurdGTTPlaceName = null; + // + public @Nullable String gtfsID = null; + /** * Hey, look, method overloading! */ diff --git a/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MapiArrivalRequest.java @@ -0,0 +1,109 @@ +package it.reyboz.bustorino.backend.mato; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.android.volley.AuthFailureError; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.HttpHeaderParser; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import it.reyboz.bustorino.backend.Palina; + +public class MapiArrivalRequest extends MapiVolleyRequest { + + private final String stopName; + private final Date startingTime; + private final int timeRange, numberOfDepartures; + + public MapiArrivalRequest(String stopName, Date startingTime, int timeRange, + int numberOfDepartures, + Response.Listener listener, + @Nullable Response.ErrorListener errorListener) { + super(MatoAPIFetcher.QueryType.ARRIVALS, listener, errorListener); + this.stopName = stopName; + this.startingTime = startingTime; + this.timeRange = timeRange; + this.numberOfDepartures = numberOfDepartures; + } + + @Nullable + @Override + public byte[] getBody() throws AuthFailureError { + JSONObject variables = new JSONObject(); + JSONObject data = new JSONObject(); + try { + data.put("operationName","AllStopsDirect"); + variables.put("name", stopName); + variables.put("startTime", (long) startingTime.getTime()/1000); + variables.put("timeRange", timeRange); + variables.put("numberOfDepartures", numberOfDepartures); + + + data.put("variables", variables); + data.put("query", MatoAPIFetcher.QUERY_ARRIVALS); + } catch (JSONException e) { + e.printStackTrace(); + throw new AuthFailureError("Error with JSON enconding",e); + } + String requestBody = data.toString(); + Log.d("MapiArrivalBusTO", "Request variables: "+ variables); + return requestBody.getBytes(); + } + + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + if(response.statusCode != 200) + return Response.error(new VolleyError("Response Error Code "+response.statusCode)); + final String stringResponse = new String(response.data); + Palina p = null; + + try { + JSONObject data = new JSONObject(stringResponse).getJSONObject("data"); + + JSONArray allStopsFound = data.getJSONArray("stops"); + + boolean haveManyResults = allStopsFound.length() > 1; + for (int i=0; i getParams() throws AuthFailureError { + return new HashMap<>(); + } +} diff --git a/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MapiVolleyRequest.java @@ -0,0 +1,40 @@ +package it.reyboz.bustorino.backend.mato; + +import androidx.annotation.Nullable; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Response; + +import java.util.Map; + +public abstract class MapiVolleyRequest extends Request { + private static final String API_URL="https://mapi.5t.torino.it/routing/v1/routers/mat/index/graphql"; + + protected final Response.Listener listener; + private final MatoAPIFetcher.QueryType type; + public MapiVolleyRequest( + MatoAPIFetcher.QueryType type, + Response.Listener listener, + @Nullable Response.ErrorListener errorListener) { + super(Method.POST, API_URL, errorListener); + this.type = type; + this.listener = listener; + + } + + + @Nullable + @Override + abstract protected Map getParams() throws AuthFailureError; + + @Override + protected void deliverResponse(T response) { + listener.onResponse(response); + } + + @Override + public Map getHeaders() throws AuthFailureError { + return MatoAPIFetcher.Companion.getREQ_PARAMETERS(); + } +} diff --git a/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt new file mode 100644 --- /dev/null +++ b/src/it/reyboz/bustorino/backend/mato/MatoAPIFetcher.kt @@ -0,0 +1,242 @@ +package it.reyboz.bustorino.backend.mato + +import android.content.Context +import android.util.Log +import com.android.volley.toolbox.RequestFuture +import it.reyboz.bustorino.backend.* +import org.json.JSONObject +import java.util.* +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicReference +import java.util.concurrent.TimeoutException + +import java.util.concurrent.ExecutionException + + + + +open class MatoAPIFetcher : ArrivalsFetcher { + var appContext: Context? = null + set(value) { + field = value!!.applicationContext + } + + override fun ReadArrivalTimesAll(stopID: String?, res: AtomicReference?): Palina { + stopID!! + val future = RequestFuture.newFuture() + val now = Calendar.getInstance().time; + var numMinutes = 30; + var palina = Palina(stopID) + var numPassaggi = 0 + var trials = 0 + while (numPassaggi < 2 && trials < 4) { + + numMinutes += 15 + val request = MapiArrivalRequest(stopID, now, numMinutes * 60, 10, 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(VOLLEY_TAG) + requestQueue.add(request) + + + try { + val palinaResult = future.get(5, TimeUnit.SECONDS) + if (palinaResult!=null) { + palina = palinaResult + if (palina.totalNumberOfPassages > 0) { + res.set(Fetcher.Result.OK) + } else res.set(Fetcher.Result.EMPTY_RESULT_SET) + numPassaggi = palina.totalNumberOfPassages + } else{ + res.set(Fetcher.Result.EMPTY_RESULT_SET) + } + } 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() + } + 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" + + val REQ_PARAMETERS = mapOf( + "Content-Type" to "application/json; charset=utf-8", + "DNT" to "1", + "Host" to "mapi.5t.torino.it") + + 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", 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 + ) + palina.gtfsID = jsonStop.getString("gtfsId") + + val routesStoppingJSON = jsonStop.getJSONArray("routes") + val baseRoutes = mutableListOf() + 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) + + } + + val routesStopTimes = jsonStop.getJSONArray("stoptimesForPatterns") + + for (i in 0 until routesStopTimes.length()){ + val patternJSON = routesStopTimes.getJSONObject(i) + val mRoute = parseRouteStoptimesJSON(patternJSON) + + //val directionId = patternJSON.getJSONObject("pattern").getInt("directionId") + //TODO: use directionId + palina.addRoute(mRoute) + for (r in baseRoutes) { + if (palina.gtfsID != null && r.gtfsId.equals(palina.gtfsID)) { + baseRoutes.remove(r) + break + } + } + } + for (noArrivalRoute in baseRoutes){ + palina.addRoute(noArrivalRoute) + } + //val gtfsRoutes = mutableListOf<>() + + + return palina + } + 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( + 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( + routeJSON.getString("shortName"), + patternJSON.getString("headsign"), + routeType, + passages, + ) + route.gtfsId = gtfsId + return route + } + + const val QUERY_ARRIVALS="""query AllStopsDirect( + ${'$'}name: String + ${'$'}startTime: Long + ${'$'}timeRange: Int + ${'$'}numberOfDepartures: Int + ) { + stops(name: ${'$'}name) { + __typename + lat + lon + gtfsId + code + name + desc + wheelchairBoarding + routes { + __typename + gtfsId + shortName + } + stoptimesForPatterns( + startTime: ${'$'}startTime + timeRange: ${'$'}timeRange + numberOfDepartures: ${'$'}numberOfDepartures + ) { + __typename + pattern { + __typename + headsign + directionId + route { + __typename + gtfsId + shortName + mode + } + } + stoptimes { + __typename + scheduledArrival + realtimeArrival + realtime + realtimeState + } + } + } + } + """ + } + + + enum class QueryType { + ARRIVALS, + } + +} \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/networkTools.java b/src/it/reyboz/bustorino/backend/networkTools.java --- a/src/it/reyboz/bustorino/backend/networkTools.java +++ b/src/it/reyboz/bustorino/backend/networkTools.java @@ -247,6 +247,11 @@ } + /** + * Parses string into int, return 0 if it fails + * @param str the string to parse + * @return the value as int, 0 if it fails + */ static int failsafeParseInt(String str) { try { return Integer.parseInt(str); diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -14,6 +14,9 @@ import java.io.StringWriter; import java.util.Arrays; import java.util.List; +import java.util.Locale; + +import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; public abstract class utils { private static final double EarthRadius = 6371e3; @@ -105,14 +108,19 @@ return busStopID; } - public static String toTitleCase(String givenString) { + public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.split(" "); - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (int i = 0; i < arr.length; i++) { - if (arr[i].length() > 1) - sb.append(Character.toUpperCase(arr[i].charAt(0))) - .append(arr[i].substring(1)).append(" "); + if (arr[i].length() > 1) { + sb.append(Character.toUpperCase(arr[i].charAt(0))); + if (lowercaseRest) + sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); + else + sb.append(arr[i].substring(1)); + sb.append(" "); + } else sb.append(arr[i]); } return sb.toString().trim(); @@ -132,6 +140,10 @@ } } + public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ + return new ArrivalsFetcher[]{ new MatoAPIFetcher(), + new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; + } /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace diff --git a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java --- a/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/src/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -56,6 +56,7 @@ import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; +import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.data.UserDB; @@ -69,7 +70,6 @@ private String DEBUG_TAG = DEBUG_TAG_ALL; private final static int loaderFavId = 2; private final static int loaderStopId = 1; - private final static ArrivalsFetcher[] defaultFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; static final String STOP_TITLE = "messageExtra"; private @Nullable String stopID,stopName; @@ -85,7 +85,7 @@ protected ImageButton addToFavorites; protected TextView timesSourceTextView; - private List fetchers = new ArrayList<>(Arrays.asList(defaultFetchers)); + private List fetchers = new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers())); private boolean reloadOnResume = true; @@ -324,14 +324,18 @@ case FiveTScraper: source_txt = getString(R.string.fivetscraper); break; + case MatoAPI: + source_txt = getString(R.string.source_mato); + break; case UNDETERMINED: //Don't show the view - timesSourceTextView.setVisibility(View.GONE); - return; + source_txt = ""; + break; default: throw new IllegalStateException("Unexpected value: " + source); } int count = 0; + if (source!= Passaggio.Source.UNDETERMINED) while (source != fetchers.get(0).getSourceForFetcher() && count < 100){ //we need to update the fetcher that is requested rotateFetchers(); diff --git a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java --- a/src/it/reyboz/bustorino/fragments/MainScreenFragment.java +++ b/src/it/reyboz/bustorino/fragments/MainScreenFragment.java @@ -90,7 +90,7 @@ private static final int SEARCH_BY_ROUTE = 2; // TODO: implement this -- https://gitpull.it/T12 private int searchMode; //private ImageButton addToFavorites; - private final ArrivalsFetcher[] arrivalsFetchers = new ArrivalsFetcher[]{new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; + private final ArrivalsFetcher[] arrivalsFetchers = utils.getDefaultArrivalsFetchers(); //// HIDDEN BUT IMPORTANT ELEMENTS //// FragmentManager fragMan; Handler mainHandler; @@ -334,6 +334,7 @@ @Override public void onAttach(@NonNull Context context) { super.onAttach(context); + Log.d(DEBUG_TAG, "OnAttach called, setupOnAttach: "+setupOnAttached); mainHandler = new Handler(); if (context instanceof CommonFragmentListener) { diff --git a/src/it/reyboz/bustorino/fragments/SettingsFragment.java b/src/it/reyboz/bustorino/fragments/SettingsFragment.java --- a/src/it/reyboz/bustorino/fragments/SettingsFragment.java +++ b/src/it/reyboz/bustorino/fragments/SettingsFragment.java @@ -61,6 +61,8 @@ }); */ + //ListPreference preference = findPreference(R.string.arrival_times) + } diff --git a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java --- a/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java +++ b/src/it/reyboz/bustorino/middleware/AsyncDataDownload.java @@ -17,6 +17,7 @@ */ package it.reyboz.bustorino.middleware; +import android.annotation.SuppressLint; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -32,6 +33,7 @@ import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; +import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; import it.reyboz.bustorino.data.AppDataProvider; import it.reyboz.bustorino.data.NextGenDB; import it.reyboz.bustorino.fragments.FragmentHelper; @@ -59,7 +61,8 @@ WeakReference helperRef; private final ArrayList otherActivities = new ArrayList<>(); private final Fetcher[] theFetchers; - private Context context; + @SuppressLint("StaticFieldLeak") + private final Context context; private final boolean replaceFragment; @@ -107,6 +110,9 @@ switch (t){ case ARRIVALS: ArrivalsFetcher f = (ArrivalsFetcher) r.getAndMoveForward(); + if (f instanceof MatoAPIFetcher){ + ((MatoAPIFetcher)f).setAppContext(context); + } Log.d(TAG,"Using the ArrivalsFetcher: "+f.getClass()); Stop lastSearchedBusStop = fh.getLastSuccessfullySearchedBusStop(); @@ -128,7 +134,7 @@ Log.e(DEBUG_TAG, "The stop number is not a valid integer, expect failures"); } p= f.ReadArrivalTimesAll(stopID,res); - publishProgress(res.get()); + //if (res.get()!= Fetcher.Result.OK) Log.d(DEBUG_TAG, "Arrivals fetcher: "+f+"\n\tProgress: "+res.get()); @@ -159,10 +165,12 @@ } } p.mergeDuplicateRoutes(0); - if(p.queryAllRoutes().size() == 0) - //skip the rest and go to the next fetcher - continue; + if (p.getTotalNumberOfPassages() == 0) + res.set(Fetcher.Result.EMPTY_RESULT_SET); + publishProgress(res.get()); + //p.sortRoutes(); result = p; + //TODO: find a way to avoid overloading the user with toasts break; case STOPS: