diff --git a/app/build.gradle b/app/build.gradle index c7c1aa9..fb2e1a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,138 +1,134 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android' +apply plugin: 'com.android.application' + android { compileSdkVersion 33 buildToolsVersion '33.0.2' namespace "it.reyboz.bustorino" defaultConfig { applicationId "it.reyboz.bustorino" minSdkVersion 21 targetSdkVersion 33 versionCode 50 versionName "2.1.0" vectorDrawables.useSupportLibrary = true multiDexEnabled true javaCompileOptions { annotationProcessorOptions { arguments = ["room.schemaLocation": "$projectDir/assets/schemas/".toString()] } } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } testOptions { unitTests.returnDefaultValues = true } sourceSets { androidTest.assets.srcDirs += files("$projectDir/assets/schemas/".toString()) } buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-dev" } gitpull{ applicationIdSuffix ".gitdev" versionNameSuffix "-gitdev" } } lintOptions { abortOnError false } repositories { mavenCentral() mavenLocal() } dependencies { //new libraries } } dependencies { - 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' implementation "androidx.fragment:fragment-ktx:$fragment_version" implementation "androidx.activity:activity:$activity_version" implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.appcompat:appcompat:$appcompat_version" implementation "androidx.appcompat:appcompat-resources:$appcompat_version" implementation "androidx.preference:preference:$preference_version" implementation "androidx.work:work-runtime:$work_version" implementation "androidx.work:work-runtime-ktx:$work_version" implementation "com.google.android.material:material:1.9.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation 'org.jsoup:jsoup:1.13.1' implementation 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1' implementation 'com.android.volley:volley:1.2.1' implementation 'org.osmdroid:osmdroid-android:6.1.10' // 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:protobuf-java:3.17.2' // mqtt library implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'com.github.hannesa2:paho.mqtt.android:3.5.3' // 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" kapt "androidx.room:room-compiler:$room_version" //multidex - we need this to build the app implementation "androidx.multidex:multidex:$multidex_version" implementation 'de.siegmar:fastcsv:2.0.0' testImplementation 'junit:junit:4.12' implementation 'junit:junit:4.12' implementation "androidx.test.ext:junit:1.1.5" implementation "androidx.test:core:$androidXTestVersion" implementation "androidx.test:runner:$androidXTestVersion" implementation "androidx.room:room-testing:$room_version" androidTestImplementation "androidx.test.ext:junit:1.1.5" 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/adapters/ArrivalsStopAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java index 83872f2..ab65954 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/ArrivalsStopAdapter.java @@ -1,294 +1,294 @@ /* BusTO - UI components Copyright (C) 2017 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.adapters; import android.content.Context; import android.content.SharedPreferences; import android.location.Location; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.*; import it.reyboz.bustorino.fragments.FragmentListenerMain; import it.reyboz.bustorino.util.RoutePositionSorter; import it.reyboz.bustorino.util.StopSorterByDistance; import java.util.*; public class ArrivalsStopAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private final static int layoutRes = R.layout.arrivals_nearby_card; //private List stops; private @NonNull GPSPoint userPosition; private FragmentListenerMain listener; private List< Pair > routesPairList; private final Context context; //Maximum number of stops to keep private final int MAX_STOPS = 20; //TODO: make it programmable private String KEY_CAPITALIZE; private NameCapitalize capit; public ArrivalsStopAdapter(@Nullable List< Pair > routesPairList, FragmentListenerMain fragmentListener, Context con, @NonNull GPSPoint pos) { listener = fragmentListener; userPosition = pos; this.routesPairList = routesPairList; context = con.getApplicationContext(); resetListAndPosition(); // if(paline!=null) //resetRoutesPairList(paline); KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); String capitalizeKey = defSharPref.getString(KEY_CAPITALIZE, ""); this.capit = NameCapitalize.getCapitalize(capitalizeKey); } @NonNull @Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { final View view = LayoutInflater.from(parent.getContext()).inflate(layoutRes, parent, false); return new ViewHolder(view); } @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { //DO THE ACTUAL WORK TO PUT THE DATA if(routesPairList==null || routesPairList.size() == 0) return; //NO STOPS final Pair stopRoutePair = routesPairList.get(position); if(stopRoutePair!=null && stopRoutePair.first!=null){ final Stop stop = stopRoutePair.first; final Route r = stopRoutePair.second; final Double distance = stop.getDistanceFromLocation(userPosition.getLatitude(), userPosition.longitude); if(distance!=Double.POSITIVE_INFINITY){ holder.distancetextView.setText(distance.intValue()+" m"); } else { holder.distancetextView.setVisibility(View.GONE); } final String stopText = String.format(context.getResources().getString(R.string.two_strings_format),stop.getStopDisplayName(),stop.ID); holder.stopNameView.setText(stopText); //final String routeName = String.format(context.getResources().getString(R.string.two_strings_format),r.getNameForDisplay(),r.destinazione); if (r!=null) { - holder.lineNameTextView.setText(r.getNameForDisplay()); + holder.lineNameTextView.setText(r.getDisplayCode()); holder.lineDirectionTextView.setText(NameCapitalize.capitalizePass(r.destinazione, capit)); holder.arrivalsTextView.setText(r.getPassaggiToString(0,2,true)); } else { holder.lineNameTextView.setVisibility(View.INVISIBLE); holder.lineDirectionTextView.setVisibility(View.INVISIBLE); //holder.arrivalsTextView.setVisibility(View.INVISIBLE); } /* EXPERIMENTS if(r.destinazione==null || r.destinazione.trim().isEmpty()){ holder.lineDirectionTextView.setVisibility(View.GONE); RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.addRule(RelativeLayout.RIGHT_OF,holder.lineNameTextView.getId()); holder.arrivalsDescriptionTextView.setLayoutParams(params); } else { RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.arrivalsDescriptionTextView.getLayoutParams(); params.removeRule(RelativeLayout.RIGHT_OF); holder.arrivalsDescriptionTextView.setLayoutParams(params); holder.lineDirectionTextView.setVisibility(View.VISIBLE); } */ holder.stopID =stop.ID; } else { Log.w("SquareStopAdapter","!! The selected stop is null !!"); } } @Override public int getItemCount() { return routesPairList.size(); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if(key.equals(KEY_CAPITALIZE)){ String k = sharedPreferences.getString(KEY_CAPITALIZE, ""); capit = NameCapitalize.getCapitalize(k); notifyDataSetChanged(); } } class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { TextView lineNameTextView; TextView lineDirectionTextView; TextView stopNameView; TextView arrivalsDescriptionTextView; TextView arrivalsTextView; TextView distancetextView; String stopID; ViewHolder(View holdView){ super(holdView); holdView.setOnClickListener(this); lineNameTextView = (TextView) holdView.findViewById(R.id.lineNameTextView); lineDirectionTextView = (TextView) holdView.findViewById(R.id.lineDirectionTextView); stopNameView = (TextView) holdView.findViewById(R.id.arrivalStopName); arrivalsTextView = (TextView) holdView.findViewById(R.id.arrivalsTimeTextView); arrivalsDescriptionTextView = (TextView) holdView.findViewById(R.id.arrivalsDescriptionTextView); distancetextView = (TextView) holdView.findViewById(R.id.arrivalsDistanceTextView); } @Override public void onClick(View v) { listener.requestArrivalsForStopID(stopID); } } public void resetRoutesPairList(List stopList){ Collections.sort(stopList,new StopSorterByDistance(userPosition)); this.routesPairList = new ArrayList<>(stopList.size()); int maxNum = Math.min(MAX_STOPS, stopList.size()); for(Palina p: stopList.subList(0,maxNum)){ //if there are no routes available, skip stop if(p.queryAllRoutes().size() == 0) continue; for(Route r: p.queryAllRoutes()){ //if there are no routes, should not do anything routesPairList.add(new Pair<>(p,r)); } } } public void setUserPosition(@Nullable GPSPoint userPosition) { this.userPosition = userPosition; } public void setRoutesPairListAndPosition(List> mRoutesPairList, @Nullable GPSPoint pos) { if(pos!=null){ this.userPosition = pos; } if(mRoutesPairList!=null){ //this.routesPairList = routesPairList; //remove duplicates sortAndRemoveDuplicates(mRoutesPairList, this.userPosition); //routesPairList = mRoutesPairList; //STUPID CODE if (this.routesPairList == null || routesPairList.size() == 0){ routesPairList = mRoutesPairList; notifyDataSetChanged(); } else{ final HashMap, Integer> indexMapIn = getRouteIndexMap(mRoutesPairList); final HashMap, Integer> indexMapExisting = getRouteIndexMap(routesPairList); //List> oldList = routesPairList; routesPairList = mRoutesPairList; /* for (Pair pair: indexMapIn.keySet()){ final Integer posIn = indexMapIn.get(pair); if (posIn == null) continue; if (indexMapExisting.containsKey(pair)){ final Integer posExisting = indexMapExisting.get(pair); //THERE IS ALREADY //routesPairList.remove(posExisting.intValue()); //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemMoved(posExisting, posIn); indexMapExisting.remove(pair); } else{ //INSERT IT //routesPairList.add(posIn,mRoutesPairList.get(posIn)); notifyItemInserted(posIn); } }// //REMOVE OLD STOPS for (Pair pair: indexMapExisting.keySet()) { final Integer posExisting = indexMapExisting.get(pair); if (posExisting == null) continue; //routesPairList.remove(posExisting.intValue()); notifyItemRemoved(posExisting); } //*/notifyDataSetChanged(); } //remove and join the } } /** * Sort and remove the repetitions for the routesPairList */ private void resetListAndPosition(){ Collections.sort(this.routesPairList,new RoutePositionSorter(userPosition)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } /** * Sort and remove the repetitions in the list */ private static void sortAndRemoveDuplicates(List< Pair > routesPairList, GPSPoint positionToSort ){ Collections.sort(routesPairList,new RoutePositionSorter(positionToSort)); //All of this to get only the first occurrences of a line (name & direction) ListIterator> iterator = routesPairList.listIterator(); Set> allRoutesDirections = new HashSet<>(); while(iterator.hasNext()){ final Pair stopRoutePair = iterator.next(); if (stopRoutePair.second != null) { final Pair routeNameDirection = new Pair<>(stopRoutePair.second.getName(), stopRoutePair.second.destinazione); if (allRoutesDirections.contains(routeNameDirection)) { iterator.remove(); } else { allRoutesDirections.add(routeNameDirection); } } } } private static HashMap, Integer> getRouteIndexMap(List> routesPairList){ final HashMap, Integer> myMap = new HashMap<>(); for (int i=0; i(name.toLowerCase(Locale.ROOT).trim(),destination.toLowerCase(Locale.ROOT).trim()), i); } return myMap; } } diff --git a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java index 9a9735c..edb78b3 100644 --- a/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java +++ b/app/src/main/java/it/reyboz/bustorino/adapters/PalinaAdapter.java @@ -1,242 +1,272 @@ /* 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.adapters; import android.content.Context; +import android.content.res.Resources; import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.core.content.res.ResourcesCompat; import androidx.preference.PreferenceManager; import android.content.SharedPreferences; import android.os.Build; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import java.util.*; import androidx.recyclerview.widget.RecyclerView; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Passaggio; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.utils; import it.reyboz.bustorino.util.PassaggiSorter; import it.reyboz.bustorino.util.RouteSorterByArrivalTime; import org.jetbrains.annotations.NotNull; /** * This once was a ListView Adapter for BusLine[]. * * Thanks to Framentos developers for the guide: * http://www.framentos.com/en/android-tutorial/2012/07/16/listview-in-android-using-custom-listadapter-and-viewcache/# * * @author Valerio Bozzolan * @author Ludovico Pavesi + * @author Fabio Mazza */ public class PalinaAdapter extends RecyclerView.Adapter implements SharedPreferences.OnSharedPreferenceChangeListener { private static final int ROW_LAYOUT = R.layout.entry_bus_line_passage; private static final int metroBg = R.drawable.route_background_metro; private static final int busBg = R.drawable.route_background_bus; private static final int extraurbanoBg = R.drawable.route_background_bus_long_distance; + 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; private final List mRoutes; - private final AdapterClickListener mRouteListener; + private final PalinaClickListener mRouteListener; @NonNull @NotNull @Override public PalinaViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(ROW_LAYOUT, parent, false); return new PalinaViewHolder(view); } @Override public void onBindViewHolder(@NonNull @NotNull PalinaViewHolder vh, int position) { final Route route = mRoutes.get(position); + final Context con = vh.itemView.getContext(); + final Resources res = con.getResources(); - vh.rowStopIcon.setText(route.getNameForDisplay()); + vh.routeIDTextView.setText(route.getDisplayCode()); + vh.routeCard.setOnClickListener(view -> mRouteListener.requestShowingRoute(route)); 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); 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); //set click listener vh.itemView.setOnClickListener(view -> { - mRouteListener.onAdapterClickListener(route); + mRouteListener.showRouteFullDirection(route); }); } switch (route.type) { //UNKNOWN = BUS for the moment case UNKNOWN: case BUS: default: // convertView could contain another background, reset it - vh.rowStopIcon.setBackgroundResource(busBg); + //vh.rowStopIcon.setBackgroundResource(busBg); + vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: - vh.rowStopIcon.setBackgroundResource(extraurbanoBg); + //vh.rowStopIcon.setBackgroundResource(extraurbanoBg); + vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.extraurban_bus_bg, null)); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: - vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); - vh.rowStopIcon.setBackgroundResource(metroBg); + //vh.rowStopIcon.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + //vh.rowStopIcon.setBackgroundResource(metroBg); + vh.routeIDTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + vh.routeCard.setCardBackgroundColor(ResourcesCompat.getColor(res, R.color.metro_bg, null)); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case RAILWAY: - vh.rowStopIcon.setBackgroundResource(busBg); + //vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: // never used but whatever. - vh.rowStopIcon.setBackgroundResource(busBg); + //vh.rowStopIcon.setBackgroundResource(busBg); vh.rowRouteDestination.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; } List passaggi = route.passaggi; //TODO: Sort the passaggi with realtime first if source is GTTJSONFetcher if(passaggi.size() == 0) { vh.rowRouteTimetable.setText(R.string.no_passages); } else { vh.rowRouteTimetable.setText(route.getPassaggiToString()); } } @Override public int getItemCount() { return mRoutes.size(); } //private static final int cityIcon = R.drawable.city; // hey look, a pattern! public static class PalinaViewHolder extends RecyclerView.ViewHolder { - final TextView rowStopIcon; + //final TextView rowStopIcon; + final TextView routeIDTextView; + final CardView routeCard; final TextView rowRouteDestination; final TextView rowRouteTimetable; public PalinaViewHolder(@NonNull @NotNull View view) { super(view); /* convertView.findViewById(R.id.routeID); vh.rowRouteDestination = (TextView) convertView.findViewById(R.id.routeDestination); vh.rowRouteTimetable = (TextView) convertView.findViewById(R.id.routesThatStopHere); */ - rowStopIcon = view.findViewById(R.id.routeID); + //rowStopIcon = view.findViewById(R.id.routeID); + routeIDTextView = view.findViewById(R.id.routeNameTextView); + routeCard = view.findViewById(R.id.routeCard); rowRouteDestination = view.findViewById(R.id.routeDestination); rowRouteTimetable = view.findViewById(R.id.routesThatStopHere); } } 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, AdapterClickListener listener, boolean hideEmptyRoutes) { + public PalinaAdapter(Context context, Palina p, PalinaClickListener listener, boolean hideEmptyRoutes) { Comparator sorter = null; if (p.getPassaggiSourceIfAny()== Passaggio.Source.GTTJSON){ sorter = new PassaggiSorter(); } final List routes; if (hideEmptyRoutes){ // build the routes by filtering them routes = new ArrayList<>(); for(Route r: p.queryAllRoutes()){ //add only if there is at least one passage if (r.numPassaggi()>0){ routes.add(r); } } } else routes = p.queryAllRoutes(); for(Route r: routes){ if (sorter==null) Collections.sort(r.passaggi); else Collections.sort(r.passaggi, sorter); } Collections.sort(routes,new RouteSorterByArrivalTime()); mRoutes = routes; KEY_CAPITALIZE = context.getString(R.string.pref_arrival_times_capit); SharedPreferences defSharPref = PreferenceManager.getDefaultSharedPreferences(context); defSharPref.registerOnSharedPreferenceChangeListener(this); this.capit = getCapitalize(defSharPref, KEY_CAPITALIZE); this.mRouteListener = listener; } @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 } + + public interface PalinaClickListener{ + /** + * Simple click listener for the whole line (show info) + * @param route for toast + */ + void showRouteFullDirection(Route route); + + /** + * Show the line with all the stops in the app + * @param route partial line info + */ + void requestShowingRoute(Route route); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java index ddab8be..ac72fa3 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/FiveTNormalizer.java @@ -1,319 +1,378 @@ /* 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.util.Log; /** * Converts some weird stop IDs found on the 5T website to the form used everywhere else (including GTT website). *

* A stop ID in normalized form is:
* - a string containing a number without leading zeros
* - a string beginning with "ST" and then a number
* - whatever the GTT website uses.
*

* A bus route ID in normalized form is:
* - a string containing a number and optionally: "B", "CS", "CD", "N", "S", "C" at the end
* - the string "METRO"
* - "ST1" or "ST2" (Star 1 and Star 2)
* - a string beginning with E, N, S or W, followed by two digits (with a leading zero)
* - "RV2", "OB1"
* - "CAN" or "FTC" (railway lines)
* - ...screw it, let's just hope all the websites and APIs return something sane as route IDs.
*

* This class exists because Java doesn't support traits.
*
* Note: this class also just became useless, as 5T now uses the same format as GTT website. */ public abstract class FiveTNormalizer { public static String FiveTNormalizeRoute(String RouteID) { while (RouteID.startsWith("0")) { RouteID = RouteID.substring(1); } return RouteID; } // public static String FiveTNormalizeStop(String StopID) { // StopID = FiveTNormalizeRoute(StopID); // // is this faster than a regex? // if (StopID.length() == 5 && StopID.startsWith("ST") && Character.isLetter(StopID.charAt(2)) && Character.isLetter(StopID.charAt(3)) && Character.isLetter(StopID.charAt(4))) { // switch (StopID) { // case "STFER": // return "8210"; // case "STPAR": // return "8211"; // case "STMAR": // return "8212"; // case "STMAS": // return "8213"; // case "STPOS": // return "8214"; // case "STMGR": // return "8215"; // case "STRIV": // return "8216"; // case "STRAC": // return "8217"; // case "STBER": // return "8218"; // case "STPDA": // return "8219"; // case "STDOD": // return "8220"; // case "STPSU": // return "8221"; // case "STVIN": // return "8222"; // case "STREU": // return "8223"; // case "STPNU": // return "8224"; // case "STMCI": // return "8225"; // case "STNIZ": // return "8226"; // case "STDAN": // return "8227"; // case "STCAR": // return "8228"; // case "STSPE": // return "8229"; // case "STLGO": // return "8230"; // } // } // return StopID; // } // // public static String NormalizedToFiveT(final String StopID) { // if(StopID.startsWith("82") && StopID.length() == 4) { // switch (StopID) { // case "8230": // return "STLGO"; // case "8229": // return "STSPE"; // case "8228": // return "STCAR"; // case "8227": // return "STDAN"; // case "8226": // return "STNIZ"; // case "8225": // return "STMCI"; // case "8224": // return "STPNU"; // case "8223": // return "STREU"; // case "8222": // return "STVIN"; // case "8221": // return "STPSU"; // case "8220": // return "STDOD"; // case "8219": // return "STPDA"; // case "8218": // return "STBER"; // case "8217": // return "STRAC"; // case "8216": // return "STRIV"; // case "8215": // return "STMGR"; // case "8214": // return "STPOS"; // case "8213": // return "STMAS"; // case "8212": // return "STMAR"; // case "8211": // return "STPAR"; // case "8210": // return "STFER"; // } // } // // return StopID; // } public static Route.Type decodeType(final String routename, final String bacino) { if(routename.equals("METRO")) { return Route.Type.METRO; } else if(routename.equals("79")) { return Route.Type.RAILWAY; } switch (bacino) { case "U": return Route.Type.BUS; case "F": return Route.Type.RAILWAY; case "E": return Route.Type.LONG_DISTANCE_BUS; default: return Route.Type.BUS; } } /** * Converts a route ID from internal format to display format, returns null if it has the same name. * * @param routeID ID in "internal" and normalized format * @return string with display name, null if unchanged */ public static String routeInternalToDisplay(final String routeID) { if(routeID.length() == 3 && routeID.charAt(2) == 'B') { return routeID.substring(0,2).concat("/"); } switch(routeID) { case "1C": return "1 Chieri"; case "1N": return "1 Nichelino"; case "OB1": return "1 Orbassano"; case "2C": return "2 Chieri"; case "RV2": return "2 Rivalta"; case "CO1": return "Circolare Collegno"; case "SE1": // I wonder why GTT calls this "SE1" while other absurd names have a human readable name too. return "1 Settimo"; case "16CD": - return "16 Circolare Destra"; + return "16 CD"; case "16CS": - return "16 Circolare Sinistra"; + return "16 CS"; case "79": return "Cremagliera Sassi-Superga"; case "W01": return "Night Buster 1 Arancio"; case "N10": return "Night Buster 10 Gialla"; case "W15": return "Night Buster 15 Rosa"; case "S18": return "Night Buster 18 Blu"; case "S04": return "Night Buster 4 Azzurra"; case "N4": return "Night Buster 4 Rossa"; case "N57": return "Night Buster 57 Oro"; case "W60": return "Night Buster 60 Argento"; case "E68": return "Night Buster 68 Verde"; case "S05": return "Night Buster 5 Viola"; case "ST1": return "Star 1"; case "ST2": return "Star 2"; case "4N": return "4 Navetta"; case "10N": return "10 Navetta"; case "13N": return "13 Navetta"; case "35N": return "35 Navetta"; case "36N": return "36 Navetta"; case "36S": return "36 Speciale"; case "38S": return "38 Speciale"; case "44S": return "44 Scolastico"; case "46N": return "46 Navetta"; + case "M1S": + return "MetroBus sostitutivo"; default: return null; } } + public static String fixShortNameForDisplay(String routeID, boolean withBarratoSpace) { + /*if (routeID.length() == 3 && routeID.charAt(2) == 'B') { + return routeID.substring(0, 2).concat("/"); + } else if (routeID.charAt(routeID.length() - 1) == '/' && routeID.charAt(routeID.length() - 2) == ' ') { + //remove last space + return routeID.substring(0, routeID.length() - 2).concat("/"); + } else return routeID; + */ + int len = routeID.length(); + final boolean isBarrato = (routeID.charAt(len-1) == 'B') || (routeID.charAt(len-1) == '/'); + if(isBarrato) { + String output; + if ((routeID.charAt(len - 2) == ' ')) + output = routeID.substring(0, len - 2); + else output = routeID.substring(0, len - 1); + if(withBarratoSpace) + output = output.concat(" /"); + else + output = output.concat("/"); + return output; + } else return routeID; + } + + public static String fixShortNameForDisplay(String routeID){ + return fixShortNameForDisplay(routeID, false); + } public static String routeDisplayToInternal(String displayName){ String name = displayName.trim(); if(name.charAt(displayName.length()-1)=='/'){ return displayName.replace(" ","").replace("/","B"); } switch (name.toLowerCase()){ //DEFAULT CASES case "star 1": return "ST1"; case "star 2": return "ST2"; case "night buster 1 arancio": return "W01"; case "night buster 10 gialla": return "N10"; case "night buster 15 rosa": return "W15"; case "night buster 18 blu": return "S18"; case "night buster 4 azzurra": return "S04"; case "night buster 4 rossa": return "N4"; case "night buster 57 oro": return "N57"; case "night buster 60 argento": return "W60"; case "night buster 68 verde": return "E68"; case "night buster 5 viola": return "S05"; case "1 nichelino": return "1N"; case "1 chieri": return "1C"; case "1 orbassano": return "OB1"; case "2 chieri": return "2C"; case "2 rivalta": return "RV2"; default: // return displayName.trim(); } String[] arr = name.toLowerCase().split("\\s+"); try { if (arr.length == 2 && arr[1].trim().equals("navetta") && Integer.decode(arr[0]) > 0) return arr[0].trim().concat("N"); } catch (NumberFormatException e){ //It's not "# navetta" Log.w("FivetNorm","checking number when it's not"); } if(name.toLowerCase().contains("night buster")){ if(name.toLowerCase().contains("viola")) return "S05"; else if(name.toLowerCase().contains("verde")) return "E68"; } //Everything failed, let's at least compact the the (probable) code return name.replace(" ",""); } + public static String getGtfsRouteID(Route route){ + String routeName = route.getName(); + String cutName = routeName.replace("\\s", ""); + + int len = cutName.length(); + StringBuilder sb = new StringBuilder("gtt:"); + if (cutName.charAt(len-1) == '/'){ + sb.append(cutName.substring(0, len-2)); + sb.append("B"); + //cutName = cutName.substring(0, len-2).concat("B"); + } else { + sb.append(cutName); + } + //determine service kind + switch (route.type){ + case UNKNOWN: + case BUS: + case TRAM: + //tourist lines have "U" in the routeid + sb.append("U"); + break; + case RAILWAY: + sb.append("F"); + break; + case LONG_DISTANCE_BUS: + sb.append("E"); + } + + + return sb.toString(); + } } diff --git a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java index b25393d..f8aef94 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Palina.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Palina.java @@ -1,417 +1,417 @@ /* 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.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.Comparator; import java.util.List; import it.reyboz.bustorino.util.LinesNameSorter; /** * Timetable for multiple routes.
*
* Apparently "palina" and a bunch of other terms can't really be translated into English.
* Not in a way that makes sense and keeps the code readable, at least. */ public class Palina extends Stop { private ArrayList routes = new ArrayList<>(); private boolean routesModified = false; private Passaggio.Source allSource = null; public Palina(String stopID) { super(stopID); } public Palina(Stop s){ super(s.ID,s.getStopDefaultName(),s.getStopUserName(),s.location,s.type, s.getRoutesThatStopHere(),s.getLatitude(),s.getLongitude(), null); } public Palina(@NonNull String ID, @Nullable String name, @Nullable String userName, @Nullable String location, @Nullable Double lat, @Nullable Double lon, @Nullable String gtfsID) { super(ID, name, userName, location, null, null, lat, lon, gtfsID); } 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. * * @param TimeGTT time in GTT format (e.g. "11:22*") * @param arrayIndex position in the array for this route (returned by addRoute) */ public void addPassaggio(String TimeGTT, Passaggio.Source src,int arrayIndex) { this.routes.get(arrayIndex).addPassaggio(TimeGTT,src); routesModified = true; } /** * Count routes with missing directions * @return number */ public int countRoutesWithMissingDirections(){ int i = 0; for (Route r : routes){ if(r.destinazione==null||r.destinazione.equals("")) i++; } return i; } /** * Adds a route to the timetable. * * @param routeID name * @param type bus, underground, railway, ... * @param destinazione end of line\terminus (underground stations have the same ID for both directions) * @return array index for this route */ public int addRoute(String routeID, String destinazione, Route.Type type) { return addRoute(new Route(routeID, destinazione, type, new ArrayList<>(6))); } public int addRoute(Route r){ this.routes.add(r); routesModified = true; buildRoutesString(); return this.routes.size()-1; // last inserted element and pray that direct access to ArrayList elements really is direct } public void setRoutes(List routeList){ routes = new ArrayList<>(routeList); } @Nullable @Override protected String buildRoutesString() { // no routes => no string if(routes == null || routes.size() == 0) { return ""; } /*final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(routes, (o1, o2) -> nameSorter.compare(o1.getName().trim(), o2.getName().trim())); int i, lenMinusOne = routes.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(routes.get(i).getName().trim()).append(", "); } // last one: sb.append(routes.get(i).getName()); */ ArrayList names = new ArrayList<>(); for (Route r: routes){ names.add(r.getName()); } final String routesThatStopHere = buildRoutesStringFromNames(names); setRoutesThatStopHereString(routesThatStopHere); return routesThatStopHereToString(); } /** * Sort the names of the routes for the string "routes stopping here" and make the string * @param names of the Routes that pass in the stop * @return the full string of routes stopping (eg, "10, 13, 42" ecc) */ public static String buildRoutesStringFromNames(List names){ final StringBuilder sb = new StringBuilder(); final LinesNameSorter nameSorter = new LinesNameSorter(); Collections.sort(names, nameSorter); int i, lenMinusOne = names.size() - 1; for (i = 0; i < lenMinusOne; i++) { sb.append(names.get(i).trim()).append(", "); } //last one sb.append(names.get(i).trim()); return sb.toString(); } protected void checkPassaggi(){ Passaggio.Source mSource = null; for (Route r: routes){ for(Passaggio pass: r.passaggi){ if (mSource == null) { mSource = pass.source; } else if (mSource != pass.source){ Log.w("BusTO-CheckPassaggi", "Cannot determine the source, have got "+mSource +" so far, the next one is "+pass.source ); mSource = Passaggio.Source.UNDETERMINED; break; } } if(mSource == Passaggio.Source.UNDETERMINED) break; } // if the Source is still null, set undetermined if (mSource == null) mSource = Passaggio.Source.UNDETERMINED; //finished with the check, setting flags routesModified = false; allSource = mSource; } @NonNull public Passaggio.Source getPassaggiSourceIfAny(){ if(allSource==null || routesModified){ checkPassaggi(); } assert allSource != null; return allSource; } /** * Gets every route and its timetable. * * @return routes and timetables. */ public List queryAllRoutes() { return this.routes; } public void sortRoutes() { Collections.sort(this.routes); } /** * Add info about the routes already found from another source * @param additionalRoutes ArrayList of routes to get the info from * @return the number of routes modified */ public int addInfoFromRoutes(List additionalRoutes){ if(routes == null || routes.size()==0) { this.routes = new ArrayList<>(additionalRoutes); buildRoutesString(); return routes.size(); } int count=0; final Calendar c = Calendar.getInstance(); final int todaysInt = c.get(Calendar.DAY_OF_WEEK); for(Route r:routes) { int j = 0; boolean correct = false; Route selected = null; //TODO: rewrite this as a simple loop //MADNESS begins here while (!correct) { //find the correct route to merge to // scan routes and find the first which has the same name while (j < additionalRoutes.size() && !r.getName().equals(additionalRoutes.get(j).getName())) { j++; } if (j == additionalRoutes.size()) break; //no match has been found //should have found the first occurrence of the line selected = additionalRoutes.get(j); //move forward j++; if (selected.serviceDays != null && selected.serviceDays.length > 0) { //check if it is in service for (int d : selected.serviceDays) { if (d == todaysInt) { correct = true; break; } } } else if (r.festivo != null) { switch (r.festivo) { case FERIALE: //Domenica = 1 --> Saturday=7 if (todaysInt <= 7 && todaysInt > 1) correct = true; break; case FESTIVO: if (todaysInt == 1) correct = true; //TODO: implement way to recognize all holidays break; case UNKNOWN: correct = true; } } else { //case a: there is no info because the line is always active //case b: there is no info because the information is missing correct = true; } } if (!correct || selected == null) { Log.w("Palina_mergeRoutes","Cannot match the route with name "+r.getName()); continue; //we didn't find any match } //found the correct correspondance //MERGE INFO if(r.mergeRouteWithAnother(selected)) count++; } if (count> 0) buildRoutesString(); return count; } // /** // * Route with terminus (destinazione) and timetables (passaggi), internal implementation. // * // * Contains mostly the same data as the Route public class, but methods are quite different and extending Route doesn't really work, here. // */ // private final class RouteInternal { // public final String name; // public final String destinazione; // private boolean updated; // private List passaggi; // // /** // * Creates a new route and marks it as "updated", since it's new. // * // * @param routeID name // * @param destinazione end of line\terminus // */ // public RouteInternal(String routeID, String destinazione) { // this.name = routeID; // this.destinazione = destinazione; // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * 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) { // this.passaggi.add(new Passaggio(TimeGTT)); // } // // /** // * Deletes al times (passaggi) from the timetable. // */ // public void deletePassaggio() { // this.passaggi = new LinkedList<>(); // this.updated = true; // } // // /** // * Sets the "updated" flag to false. // * // * @return previous state // */ // public boolean unupdateFlag() { // if(this.updated) { // this.updated = false; // return true; // } else { // return false; // } // } // // /** // * Sets the "updated" flag to true. // * // * @return previous state // */ // public boolean updateFlag() { // if(this.updated) { // return true; // } else { // this.updated = true; // return false; // } // } // // /** // * Exactly what it says on the tin. // * // * @return times from the timetable // */ // public List getPassaggi() { // return this.passaggi; // } // } //remove duplicates public void mergeDuplicateRoutes(int startidx){ //ArrayList routesCopy = new ArrayList<>(routes); //for if(routes.size()<=1|| startidx >= routes.size()) //we have finished return; Route routeCheck = routes.get(startidx); boolean found = false; for(int i=startidx+1; i0) min = Math.min(min,r.numPassaggi()); } if (min == Integer.MAX_VALUE) return 0; else return min; } public ArrayList getRoutesNamesWithNoPassages(){ ArrayList mList = new ArrayList<>(); if(routes==null || routes.size() == 0){ return mList; } for(Route r: routes){ if(r.numPassaggi()==0) - mList.add(r.getNameForDisplay()); + mList.add(r.getDisplayCode()); } return mList; } //private void mergeRoute } \ 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 cc953f6..ad6722d 100644 --- a/app/src/main/java/it/reyboz/bustorino/backend/Route.java +++ b/app/src/main/java/it/reyboz/bustorino/backend/Route.java @@ -1,437 +1,446 @@ /* 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.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 { 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){ switch (i){ case 1: return BUS; case 2: return LONG_DISTANCE_BUS; case 3: return METRO; case 4: return RAILWAY; case 5: return TRAM; case -2: return UNKNOWN; default: return 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){ switch (i){ case -2: return UNKNOWN; case 0: return FERIALE; case 1: return FESTIVO; default: return 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; } /** * 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)); } //Overloaded public void addPassaggio(int hour, int minutes, boolean realtime, Passaggio.Source source) { this.passaggi.add(new Passaggio(hour, minutes, realtime, source)); } 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; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java index fca2a6c..7ef303b 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ArrivalsFragment.java @@ -1,693 +1,706 @@ /* BusTO - Fragments 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.fragments; -import android.annotation.SuppressLint; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.widget.*; import androidx.annotation.Nullable; import androidx.annotation.NonNull; -import androidx.core.widget.NestedScrollView; import androidx.loader.app.LoaderManager; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Collections; import java.util.List; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import it.reyboz.bustorino.R; -import it.reyboz.bustorino.adapters.AdapterClickListener; import it.reyboz.bustorino.adapters.PalinaAdapter; import it.reyboz.bustorino.adapters.RouteOnlyLineAdapter; import it.reyboz.bustorino.backend.ArrivalsFetcher; import it.reyboz.bustorino.backend.DBStatusManager; import it.reyboz.bustorino.backend.Fetcher; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.Palina; 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; import it.reyboz.bustorino.middleware.AsyncStopFavoriteAction; import it.reyboz.bustorino.util.LinesNameSorter; -import it.reyboz.bustorino.util.ViewUtils; import static it.reyboz.bustorino.fragments.ScreenBaseFragment.setOption; public class ArrivalsFragment extends ResultBaseFragment implements LoaderManager.LoaderCallbacks { private static final String OPTION_SHOW_LEGEND = "show_legend"; private final static String KEY_STOP_ID = "stopid"; private final static String KEY_STOP_NAME = "stopname"; private final static String DEBUG_TAG_ALL = "BUSTOArrivalsFragment"; private String DEBUG_TAG = DEBUG_TAG_ALL; private final static int loaderFavId = 2; private final static int loaderStopId = 1; static final String STOP_TITLE = "messageExtra"; private final static String SOURCES_TEXT="sources_textview_message"; private @Nullable String stopID,stopName; private DBStatusManager prefs; private DBStatusManager.OnDBUpdateStatusChangeListener listener; private boolean justCreated = false; private Palina lastUpdatedPalina = null; private boolean needUpdateOnAttach = false; private boolean fetchersChangeRequestPending = false; private boolean stopIsInFavorites = false; //Views protected ImageButton addToFavorites; protected TextView timesSourceTextView; protected TextView messageTextView; protected RecyclerView arrivalsRecyclerView; private PalinaAdapter mListAdapter = null; private TextView howDoesItWorkTextView; private Button hideHintButton; //private NestedScrollView theScrollView; protected RecyclerView noArrivalsRecyclerView; private RouteOnlyLineAdapter noArrivalsAdapter; private TextView noArrivalsTitleView; private GridLayoutManager layoutManager; //private View canaryEndView; private List fetchers = null; //new ArrayList<>(Arrays.asList(utils.getDefaultArrivalsFetchers())); private boolean reloadOnResume = true; - private final AdapterClickListener mRouteClickListener = route -> { - String routeName; + private final PalinaAdapter.PalinaClickListener palinaClickListener = new PalinaAdapter.PalinaClickListener() { + @Override + public void showRouteFullDirection(Route route) { + String routeName; + Log.d(DEBUG_TAG, "Make toast for line "+route.getName()); - routeName = FiveTNormalizer.routeInternalToDisplay(route.getNameForDisplay()); - if (routeName == null) { - routeName = route.getNameForDisplay(); - } - if(getContext()==null) - Log.e(DEBUG_TAG, "Touched on a route but Context is null"); - else if (route.destinazione == null || route.destinazione.length() == 0) { - Toast.makeText(getContext(), - getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(getContext(), - getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT).show(); + + routeName = FiveTNormalizer.routeInternalToDisplay(route.getName()); + if (routeName == null) { + routeName = route.getDisplayCode(); + } + if(getContext()==null) + Log.e(DEBUG_TAG, "Touched on a route but Context is null"); + else if (route.destinazione == null || route.destinazione.length() == 0) { + Toast.makeText(getContext(), + getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), + getString(R.string.route_towards_destination, routeName, route.destinazione), Toast.LENGTH_SHORT).show(); + } } + @Override + public void requestShowingRoute(Route route) { + Log.d(DEBUG_TAG, "Need to show line for route:\ngtfsID "+route.getGtfsId()+ " name "+route.getName()); + if(route.getGtfsId()!=null){ + mListener.showLineOnMap(route.getGtfsId()); + } else { + String gtfsID = FiveTNormalizer.getGtfsRouteID(route); + Log.d(DEBUG_TAG, "GtfsID for route is: " + gtfsID); + mListener.showLineOnMap(gtfsID); + } + } }; + public static ArrivalsFragment newInstance(String stopID){ return newInstance(stopID, null); } public static ArrivalsFragment newInstance(@NonNull String stopID, @Nullable String stopName){ ArrivalsFragment fragment = new ArrivalsFragment(); Bundle args = new Bundle(); args.putString(KEY_STOP_ID,stopID); //parameter for ResultListFragmentrequestArrivalsForStopID //args.putSerializable(LIST_TYPE,FragmentKind.ARRIVALS); if (stopName != null){ args.putString(KEY_STOP_NAME,stopName); } fragment.setArguments(args); return fragment; } public static String getFragmentTag(Palina p) { return "palina_"+p.ID; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); stopID = getArguments().getString(KEY_STOP_ID); DEBUG_TAG = DEBUG_TAG_ALL+" "+stopID; //this might really be null stopName = getArguments().getString(KEY_STOP_NAME); final ArrivalsFragment arrivalsFragment = this; listener = new DBStatusManager.OnDBUpdateStatusChangeListener() { @Override public void onDBStatusChanged(boolean updating) { if(!updating){ getLoaderManager().restartLoader(loaderFavId,getArguments(),arrivalsFragment); } else { final LoaderManager lm = getLoaderManager(); lm.destroyLoader(loaderFavId); lm.destroyLoader(loaderStopId); } } @Override public boolean defaultStatusValue() { return true; } }; prefs = new DBStatusManager(getContext().getApplicationContext(),listener); justCreated = true; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_arrivals, container, false); messageTextView = root.findViewById(R.id.messageTextView); addToFavorites = root.findViewById(R.id.addToFavorites); // "How does it work part" howDoesItWorkTextView = root.findViewById(R.id.howDoesItWorkTextView); hideHintButton = root.findViewById(R.id.hideHintButton); hideHintButton.setOnClickListener(this::onHideHint); //theScrollView = root.findViewById(R.id.arrivalsScrollView); // recyclerview holding the arrival times arrivalsRecyclerView = root.findViewById(R.id.arrivalsRecyclerView); final LinearLayoutManager manager = new LinearLayoutManager(getContext()); arrivalsRecyclerView.setLayoutManager(manager); final DividerItemDecoration mDividerItemDecoration = new DividerItemDecoration(arrivalsRecyclerView.getContext(), manager.getOrientation()); arrivalsRecyclerView.addItemDecoration(mDividerItemDecoration); timesSourceTextView = root.findViewById(R.id.timesSourceTextView); timesSourceTextView.setOnLongClickListener(view -> { if(!fetchersChangeRequestPending){ rotateFetchers(); //Show we are changing provider timesSourceTextView.setText(R.string.arrival_source_changing); mListener.requestArrivalsForStopID(stopID); fetchersChangeRequestPending = true; return true; } return false; }); timesSourceTextView.setOnClickListener(view -> { Toast.makeText(getContext(), R.string.change_arrivals_source_message, Toast.LENGTH_SHORT) .show(); }); //Button addToFavorites.setClickable(true); addToFavorites.setOnClickListener(v -> { // add/remove the stop in the favorites toggleLastStopToFavorites(); }); String displayName = getArguments().getString(STOP_TITLE); if(displayName!=null) setTextViewMessage(String.format( getString(R.string.passages), displayName)); String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } //no arrivals stuff noArrivalsRecyclerView = root.findViewById(R.id.noArrivalsRecyclerView); layoutManager = new GridLayoutManager(getContext(),60); layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { return 12; } }); noArrivalsRecyclerView.setLayoutManager(layoutManager); noArrivalsTitleView = root.findViewById(R.id.noArrivalsMessageTextView); //canaryEndView = root.findViewById(R.id.canaryEndView); /*String sourcesTextViewData = getArguments().getString(SOURCES_TEXT); if (sourcesTextViewData!=null){ timesSourceTextView.setText(sourcesTextViewData); }*/ //need to do this when we recreate the fragment but we haven't updated the arrival times if (lastUpdatedPalina!=null) showArrivalsSources(lastUpdatedPalina); return root; } @Override public void onResume() { super.onResume(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "OnResume, justCreated "+justCreated+", lastUpdatedPalina is: "+lastUpdatedPalina); /*if(needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach=false; }*/ /*if(lastUpdatedPalina!=null){ updateFragmentData(null); showArrivalsSources(lastUpdatedPalina); }*/ mListener.readyGUIfor(FragmentKind.ARRIVALS); if (mListAdapter!=null) resetListAdapter(mListAdapter); if(noArrivalsAdapter!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); } if(stopID!=null){ if(!justCreated){ fetchers = utils.getDefaultArrivalsFetchers(getContext()); adjustFetchersToSource(); if (reloadOnResume) mListener.requestArrivalsForStopID(stopID); } else justCreated = false; //start the loader if(prefs.isDBUpdating(true)){ prefs.registerListener(); } else { Log.d(DEBUG_TAG, "Restarting loader for stop"); loaderManager.restartLoader(loaderFavId, getArguments(), this); } updateMessage(); } if (ScreenBaseFragment.getOption(requireContext(),OPTION_SHOW_LEGEND, true)) { showHints(); } } @Override public void onStart() { super.onStart(); if (needUpdateOnAttach){ updateFragmentData(null); needUpdateOnAttach = false; } } @Override public void onPause() { if(listener!=null) prefs.unregisterListener(); super.onPause(); LoaderManager loaderManager = getLoaderManager(); Log.d(DEBUG_TAG, "onPause, have running loaders: "+loaderManager.hasRunningLoaders()); loaderManager.destroyLoader(loaderFavId); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); //get fetchers fetchers = utils.getDefaultArrivalsFetchers(context); } @Nullable public String getStopID() { return stopID; } public boolean reloadsOnResume() { return reloadOnResume; } public void setReloadOnResume(boolean reloadOnResume) { this.reloadOnResume = reloadOnResume; } // HINT "HOW TO USE" private void showHints() { howDoesItWorkTextView.setVisibility(View.VISIBLE); hideHintButton.setVisibility(View.VISIBLE); //actionHelpMenuItem.setVisible(false); } private void hideHints() { howDoesItWorkTextView.setVisibility(View.GONE); hideHintButton.setVisibility(View.GONE); //actionHelpMenuItem.setVisible(true); } public void onHideHint(View v) { hideHints(); setOption(requireContext(),OPTION_SHOW_LEGEND, false); } /** * Give the fetchers * @return the list of the fetchers */ public ArrayList getCurrentFetchers(){ return new ArrayList<>(this.fetchers); } public ArrivalsFetcher[] getCurrentFetchersAsArray(){ ArrivalsFetcher[] arr = new ArrivalsFetcher[fetchers.size()]; fetchers.toArray(arr); return arr; } private void rotateFetchers(){ Log.d(DEBUG_TAG, "Rotating fetchers, before: "+fetchers); Collections.rotate(fetchers, -1); Log.d(DEBUG_TAG, "Rotating fetchers, afterwards: "+fetchers); } /** * Update the UI with the new data * @param p the full Palina */ public void updateFragmentData(@Nullable Palina p){ if (p!=null) lastUpdatedPalina = p; if (!isAdded()){ //defer update at next show if (p==null) Log.w(DEBUG_TAG, "Asked to update the data, but we're not attached and the data is null"); else needUpdateOnAttach = true; } else { - final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina, mRouteClickListener, true); + final PalinaAdapter adapter = new PalinaAdapter(getContext(), lastUpdatedPalina, palinaClickListener, true); showArrivalsSources(lastUpdatedPalina); resetListAdapter(adapter); final ArrayList routesWithNoPassages = lastUpdatedPalina.getRoutesNamesWithNoPassages(); Collections.sort(routesWithNoPassages, new LinesNameSorter()); noArrivalsAdapter = new RouteOnlyLineAdapter(routesWithNoPassages, null); if(noArrivalsRecyclerView!=null){ noArrivalsRecyclerView.setAdapter(noArrivalsAdapter); //hide the views if there are no empty routes if(routesWithNoPassages.isEmpty()){ noArrivalsRecyclerView.setVisibility(View.GONE); noArrivalsTitleView.setVisibility(View.GONE); } else { noArrivalsRecyclerView.setVisibility(View.VISIBLE); noArrivalsTitleView.setVisibility(View.VISIBLE); } } //canaryEndView.setVisibility(View.VISIBLE); //check if canaryEndView is visible //boolean isCanaryVisibile = ViewUtils.Companion.isViewPartiallyVisibleInScroll(canaryEndView, theScrollView); //Log.d(DEBUG_TAG, "Canary view fully visibile: "+isCanaryVisibile); } } /** * Set the message of the arrival times source * @param p Palina with the arrival times */ protected void showArrivalsSources(Palina p){ final Passaggio.Source source = p.getPassaggiSourceIfAny(); if (source == null){ Log.e(DEBUG_TAG, "NULL SOURCE"); return; } String source_txt; switch (source){ case GTTJSON: source_txt = getString(R.string.gttjsonfetcher); break; case FiveTAPI: source_txt = getString(R.string.fivetapifetcher); break; 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 source_txt = getString(R.string.undetermined_source); break; default: throw new IllegalStateException("Unexpected value: " + source); } // final boolean updatedFetchers = adjustFetchersToSource(source); if(!updatedFetchers) Log.w(DEBUG_TAG, "Tried to update the source fetcher but it didn't work"); final String base_message = getString(R.string.times_source_fmt, source_txt); timesSourceTextView.setText(base_message); timesSourceTextView.setVisibility(View.VISIBLE); if (p.getTotalNumberOfPassages() > 0) { timesSourceTextView.setVisibility(View.VISIBLE); } else { timesSourceTextView.setVisibility(View.INVISIBLE); } fetchersChangeRequestPending = false; } protected boolean adjustFetchersToSource(Passaggio.Source source){ if (source == null) return false; int count = 0; if (source!= Passaggio.Source.UNDETERMINED) while (source != fetchers.get(0).getSourceForFetcher() && count < 200){ //we need to update the fetcher that is requested rotateFetchers(); count++; } return count < 200; } protected boolean adjustFetchersToSource(){ if (lastUpdatedPalina == null) return false; final Passaggio.Source source = lastUpdatedPalina.getPassaggiSourceIfAny(); return adjustFetchersToSource(source); } /** * Update the message in the fragment * * It may eventually change the "Add to Favorite" icon */ private void updateMessage(){ String message = null; if (stopName != null && stopID != null && stopName.length() > 0) { message = (stopID.concat(" - ").concat(stopName)); } else if(stopID!=null) { message = stopID; } else { Log.e("ArrivalsFragm"+getTag(),"NO ID FOR THIS FRAGMENT - something went horribly wrong"); } if(message!=null) { setTextViewMessage(getString(R.string.passages,message)); } // whatever is the case, update the star icon //updateStarIconFromLastBusStop(); } @NonNull @Override public Loader onCreateLoader(int id, Bundle args) { if(args.getString(KEY_STOP_ID)==null) return null; final String stopID = args.getString(KEY_STOP_ID); final Uri.Builder builder = AppDataProvider.getUriBuilderToComplete(); CursorLoader cl; switch (id){ case loaderFavId: builder.appendPath("favorites").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),UserDB.getFavoritesColumnNamesAsArray,null,null,null); break; case loaderStopId: builder.appendPath("stop").appendPath(stopID); cl = new CursorLoader(getContext(),builder.build(),new String[]{NextGenDB.Contract.StopsTable.COL_NAME}, null,null,null); break; default: return null; } cl.setUpdateThrottle(500); return cl; } @Override public void onLoadFinished(Loader loader, Cursor data) { switch (loader.getId()){ case loaderFavId: final int colUserName = data.getColumnIndex(UserDB.getFavoritesColumnNamesAsArray[1]); if(data.getCount()>0){ // IT'S IN FAVORITES data.moveToFirst(); final String probableName = data.getString(colUserName); stopIsInFavorites = true; stopName = probableName; //update the message in the textview updateMessage(); } else { stopIsInFavorites =false; } updateStarIcon(); if(stopName == null){ //stop is not inside the favorites and wasn't provided Log.d("ArrivalsFragment"+getTag(),"Stop wasn't in the favorites and has no name, looking in the DB"); getLoaderManager().restartLoader(loaderStopId,getArguments(),this); } break; case loaderStopId: if(data.getCount()>0){ data.moveToFirst(); int index = data.getColumnIndex( NextGenDB.Contract.StopsTable.COL_NAME ); if (index == -1){ Log.e(DEBUG_TAG, "Index is -1, column not present. App may explode now..."); } stopName = data.getString(index); updateMessage(); } else { Log.w("ArrivalsFragment"+getTag(),"Stop is not inside the database... CLOISTER BELL"); } } } @Override public void onLoaderReset(Loader loader) { //NOTHING TO DO } protected void resetListAdapter(PalinaAdapter adapter) { mListAdapter = adapter; if (arrivalsRecyclerView != null) { arrivalsRecyclerView.setAdapter(adapter); arrivalsRecyclerView.setVisibility(View.VISIBLE); } } /** * Set the message textView * @param message the whole message to write in the textView */ public void setTextViewMessage(String message) { messageTextView.setText(message); messageTextView.setVisibility(View.VISIBLE); } public void toggleLastStopToFavorites() { Stop stop = lastUpdatedPalina; if (stop != null) { // toggle the status in background new AsyncStopFavoriteAction(getContext().getApplicationContext(), AsyncStopFavoriteAction.Action.TOGGLE, v->updateStarIconFromLastBusStop(v)).execute(stop); } else { // this case have no sense, but just immediately update the favorite icon updateStarIconFromLastBusStop(true); } } /** * Update the star "Add to favorite" icon */ public void updateStarIconFromLastBusStop(Boolean toggleDone) { if (stopIsInFavorites) stopIsInFavorites = !toggleDone; else stopIsInFavorites = toggleDone; updateStarIcon(); // check if there is a last Stop /* if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (isStopInFavorites(stopID)) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } */ } /** * Update the star icon according to `stopIsInFavorites` */ public void updateStarIcon() { // no favorites no party! // check if there is a last Stop if (stopID == null) { addToFavorites.setVisibility(View.INVISIBLE); } else { // filled or outline? if (stopIsInFavorites) { addToFavorites.setImageResource(R.drawable.ic_star_filled); } else { addToFavorites.setImageResource(R.drawable.ic_star_outline); } addToFavorites.setVisibility(View.VISIBLE); } } @Override public void onDestroyView() { arrivalsRecyclerView = null; if(getArguments()!=null) { getArguments().putString(SOURCES_TEXT, timesSourceTextView.getText().toString()); getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString()); } super.onDestroyView(); } public boolean isFragmentForTheSameStop(Palina p) { if (getTag() != null) return getTag().equals(getFragmentTag(p)); else return false; } } diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt index 9c26417..5f09240 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt +++ b/app/src/main/java/it/reyboz/bustorino/fragments/LinesDetailFragment.kt @@ -1,805 +1,812 @@ /* BusTO - Fragments components Copyright (C) 2023 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.fragments import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.graphics.Paint import android.os.Bundle import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import it.reyboz.bustorino.R import it.reyboz.bustorino.adapters.NameCapitalize import it.reyboz.bustorino.adapters.StopAdapterListener import it.reyboz.bustorino.adapters.StopRecyclerAdapter +import it.reyboz.bustorino.backend.FiveTNormalizer import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.backend.gtfs.GtfsUtils import it.reyboz.bustorino.backend.gtfs.LivePositionUpdate import it.reyboz.bustorino.backend.gtfs.PolylineParser import it.reyboz.bustorino.backend.utils import it.reyboz.bustorino.data.MatoTripsDownloadWorker import it.reyboz.bustorino.data.PreferencesHolder import it.reyboz.bustorino.data.gtfs.MatoPattern import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.TripAndPatternWithStops import it.reyboz.bustorino.map.* import it.reyboz.bustorino.map.CustomInfoWindow.TouchResponder import it.reyboz.bustorino.viewmodels.LinesViewModel import it.reyboz.bustorino.viewmodels.LivePositionsViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.osmdroid.config.Configuration import org.osmdroid.tileprovider.tilesource.TileSourceFactory import org.osmdroid.util.BoundingBox import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.FolderOverlay import org.osmdroid.views.overlay.Marker import org.osmdroid.views.overlay.Polyline import org.osmdroid.views.overlay.advancedpolyline.MonochromaticPaintList class LinesDetailFragment() : ScreenBaseFragment() { private var lineID = "" private lateinit var patternsSpinner: Spinner private var patternsAdapter: ArrayAdapter? = null //private var patternsSpinnerState: Parcelable? = null private lateinit var currentPatterns: List private lateinit var map: MapView private var viewingPattern: MatoPatternWithStops? = null private val viewModel: LinesViewModel by viewModels() private val mapViewModel: MapViewModel by viewModels() private var firstInit = true private var pausedFragment = false private lateinit var switchButton: ImageButton private var favoritesButton: ImageButton? = null private var isLineInFavorite = false private var appContext: Context? = null private val lineSharedPrefMonitor = SharedPreferences.OnSharedPreferenceChangeListener { pref, keychanged -> if(keychanged!=PreferencesHolder.PREF_FAVORITE_LINES || lineID.isEmpty()) return@OnSharedPreferenceChangeListener val newFavorites = pref.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) newFavorites?.let {favorites-> isLineInFavorite = favorites.contains(lineID) //if the button has been intialized, change the icon accordingly favoritesButton?.let { button-> if(isLineInFavorite) { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) appContext?.let { Toast.makeText(it,R.string.favorites_line_add,Toast.LENGTH_SHORT).show()} } else { button.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_outline, null)) appContext?.let {Toast.makeText(it,R.string.favorites_line_remove,Toast.LENGTH_SHORT).show()} } } } } private lateinit var stopsRecyclerView: RecyclerView private lateinit var descripTextView: TextView //adapter for recyclerView private val stopAdapterListener= object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { if(viewModel.shouldShowMessage) { Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() viewModel.shouldShowMessage=false } stop?.let { fragmentListener.requestArrivalsForStopID(it.ID) } if(stop == null){ Log.e(DEBUG_TAG,"Passed wrong stop") } if(fragmentListener == null){ Log.e(DEBUG_TAG, "Fragment listener is null") } } override fun onLongPressOnStop(stop: Stop?): Boolean { TODO("Not yet implemented") } } private var polyline: Polyline? = null //private var stopPosList = ArrayList() private lateinit var stopsOverlay: FolderOverlay private lateinit var locationOverlay: LocationOverlay //fragment actions private lateinit var fragmentListener: CommonFragmentListener private val stopTouchResponder = TouchResponder { stopID, stopName -> Log.d(DEBUG_TAG, "Asked to show arrivals for stop ID: $stopID") fragmentListener.requestArrivalsForStopID(stopID) } private var showOnTopOfLine = true private var recyclerInitDone = false private var useMQTTPositions = true //position of live markers private val busPositionMarkersByTrip = HashMap() private var busPositionsOverlay = FolderOverlay() private val tripMarkersAnimators = HashMap() private val liveBusViewModel: LivePositionsViewModel by viewModels() @SuppressLint("SetTextI18n") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rootView = inflater.inflate(R.layout.fragment_lines_detail, container, false) lineID = requireArguments().getString(LINEID_KEY, "") switchButton = rootView.findViewById(R.id.switchImageButton) favoritesButton = rootView.findViewById(R.id.favoritesButton) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) descripTextView = rootView.findViewById(R.id.lineDescripTextView) descripTextView.visibility = View.INVISIBLE val titleTextView = rootView.findViewById(R.id.titleTextView) - titleTextView.text = getString(R.string.line)+" "+GtfsUtils.getLineNameFromGtfsID(lineID) + titleTextView.text = getString(R.string.line)+" "+FiveTNormalizer.fixShortNameForDisplay( + GtfsUtils.getLineNameFromGtfsID(lineID), true) favoritesButton?.isClickable = true favoritesButton?.setOnClickListener { if(lineID.isNotEmpty()) PreferencesHolder.addOrRemoveLineToFavorites(requireContext(),lineID,!isLineInFavorite) } val preferences = PreferencesHolder.getMainSharedPreferences(requireContext()) val favorites = preferences.getStringSet(PreferencesHolder.PREF_FAVORITE_LINES, HashSet()) if(favorites!=null && favorites.contains(lineID)){ favoritesButton?.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_star_filled, null)) isLineInFavorite = true } appContext = requireContext().applicationContext preferences.registerOnSharedPreferenceChangeListener(lineSharedPrefMonitor) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) patternsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter initializeMap(rootView) initializeRecyclerView() switchButton.setOnClickListener{ if(map.visibility == View.VISIBLE){ map.visibility = View.GONE stopsRecyclerView.visibility = View.VISIBLE viewModel.setMapShowing(false) liveBusViewModel.stopMatoUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_map_white_30)) } else{ stopsRecyclerView.visibility = View.GONE map.visibility = View.VISIBLE viewModel.setMapShowing(true) if(useMQTTPositions) liveBusViewModel.requestMatoPosUpdates(lineID) else liveBusViewModel.requestGTFSUpdates() switchButton.setImageDrawable(AppCompatResources.getDrawable(requireContext(), R.drawable.ic_list_30)) } } viewModel.setRouteIDQuery(lineID) val keySourcePositions = getString(R.string.pref_positions_source) useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") viewModel.patternsWithStopsByRouteLiveData.observe(viewLifecycleOwner){ patterns -> savePatternsToShow(patterns) } /* We have the pattern and the stops here, time to display them */ viewModel.stopsForPatternLiveData.observe(viewLifecycleOwner) { stops -> if(map.visibility ==View.VISIBLE) showPatternWithStopsOnMap(stops) else{ if(stopsRecyclerView.visibility==View.VISIBLE) showStopsAsList(stops) } } viewModel.gtfsRoute.observe(viewLifecycleOwner){route-> + if(route == null){ + //need to close the fragment + activity?.supportFragmentManager?.popBackStack() + return@observe + } descripTextView.text = route.longName descripTextView.visibility = View.VISIBLE } if(pausedFragment && viewModel.selectedPatternLiveData.value!=null){ val patt = viewModel.selectedPatternLiveData.value!! Log.d(DEBUG_TAG, "Recreating views on resume, setting pattern: ${patt.pattern.code}") showPattern(patt) pausedFragment = false } Log.d(DEBUG_TAG,"Data ${viewModel.stopsForPatternLiveData.value}") //listeners patternsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) //viewModel.setPatternToDisplay(patternWithStops) setPatternAndReqStops(patternWithStops) Log.d(DEBUG_TAG, "item Selected, cleaning bus markers") if(map?.visibility == View.VISIBLE) { busPositionsOverlay.closeAllInfoWindows() busPositionsOverlay.items.clear() busPositionMarkersByTrip.clear() stopAnimations() tripMarkersAnimators.clear() liveBusViewModel.retriggerPositionUpdate() } } override fun onNothingSelected(p0: AdapterView<*>?) { } } //live bus positions liveBusViewModel.updatesWithTripAndPatterns.observe(viewLifecycleOwner){ if(map.visibility == View.GONE || viewingPattern ==null){ //DO NOTHING return@observe } val filtdLineID = GtfsUtils.stripGtfsPrefix(lineID) //filter buses with direction, show those only with the same direction val outmap = HashMap>() val currentPattern = viewingPattern!!.pattern val numUpds = it.entries.size Log.d(DEBUG_TAG, "Got $numUpds updates, current pattern is: ${currentPattern.name}, directionID: ${currentPattern.directionId}") val patternsDirections = HashMap() for((tripId, pair) in it.entries){ //remove trips with wrong line ideas if(pair.first.routeID!=filtdLineID) continue if(pair.second!=null && pair.second?.pattern !=null){ val dir = pair.second?.pattern?.directionId if(dir !=null && dir == currentPattern.directionId){ outmap[tripId] = pair } patternsDirections.set(tripId,if (dir!=null) dir else -10) } else{ outmap[tripId] = pair //Log.d(DEBUG_TAG, "No pattern for tripID: $tripId") patternsDirections[tripId] = -10 } } Log.d(DEBUG_TAG, " Filtered updates are ${outmap.keys.size}") // Original updates directs: $patternsDirections\n updateBusPositionsInMap(outmap) //if not using MQTT positions if(!useMQTTPositions){ liveBusViewModel.requestDelayedGTFSUpdates(2000) } } //download missing tripIDs liveBusViewModel.tripsGtfsIDsToQuery.observe(viewLifecycleOwner){ //gtfsPosViewModel.downloadTripsFromMato(dat); MatoTripsDownloadWorker.downloadTripsFromMato( it, requireContext().applicationContext, "BusTO-MatoTripDownload" ) } return rootView } private fun initializeMap(rootView : View){ val ctx = requireContext().applicationContext Configuration.getInstance().load(ctx, PreferenceManager.getDefaultSharedPreferences(ctx)) map = rootView.findViewById(R.id.lineMap) map.let { it.setTileSource(TileSourceFactory.MAPNIK) locationOverlay = LocationOverlay.createLocationOverlay(true, it, requireContext(), object : LocationOverlay.OverlayCallbacks{ override fun onDisableFollowMyLocation() { Log.d(DEBUG_TAG, "Follow location disabled") } override fun onEnableFollowMyLocation() { Log.d(DEBUG_TAG, "Follow location enabled") } }) locationOverlay.disableFollowLocation() stopsOverlay = FolderOverlay() busPositionsOverlay = FolderOverlay() //map.setTilesScaledToDpi(true); //map.setTilesScaledToDpi(true); it.setFlingEnabled(true) it.setUseDataConnection(true) // add ability to zoom with 2 fingers it.setMultiTouchControls(true) it.minZoomLevel = 12.0 //map controller setup val mapController = it.controller var zoom = 12.0 var centerMap = GeoPoint(DEFAULT_CENTER_LAT, DEFAULT_CENTER_LON) if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") zoom = mapViewModel.currentZoom.value!! centerMap = GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!) /*viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) */ } mapController.setZoom(zoom) mapController.setCenter(centerMap) Log.d(DEBUG_TAG, "Initializing map, first init $firstInit") //map.invalidate() it.overlayManager.add(stopsOverlay) it.overlayManager.add(locationOverlay) it.overlayManager.add(busPositionsOverlay) zoomToCurrentPattern() firstInit = false } } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener){ fragmentListener = context } else throw RuntimeException("$context must implement CommonFragmentListener") } private fun stopAnimations(){ for(anim in tripMarkersAnimators.values){ anim.cancel() } } private fun savePatternsToShow(patterns: List){ currentPatterns = patterns.sortedBy { p-> p.pattern.code } patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } viewingPattern?.let { showPattern(it) } } /** * Called when the position of the spinner is updated */ private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") viewModel.selectedPatternLiveData.value = patternWithStops viewModel.currentPatternStops.value = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewingPattern = patternWithStops viewModel.requestStopsForPatternWithStops(patternWithStops) } private fun showPattern(patternWs: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Finding pattern to show: ${patternWs.pattern.code}") var pos = -2 val code = patternWs.pattern.code.trim() for(k in currentPatterns.indices){ if(currentPatterns[k].pattern.code.trim() == code){ pos = k break } } Log.d(DEBUG_TAG, "Found pattern $code in position: $pos") if(pos>=0) patternsSpinner.setSelection(pos) //set pattern setPatternAndReqStops(patternWs) } private fun zoomToCurrentPattern(){ var pointsList: List if(viewingPattern==null) { Log.e(DEBUG_TAG, "asked to zoom to pattern but current viewing pattern is null") if(polyline!=null) pointsList = polyline!!.actualPoints else { Log.d(DEBUG_TAG, "The polyline is null") return } }else{ val pattern = viewingPattern!!.pattern pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) } var maxLat = -4000.0 var minLat = -4000.0 var minLong = -4000.0 var maxLong = -4000.0 for (p in pointsList){ // get max latitude if(maxLat == -4000.0) maxLat = p.latitude else if (maxLat < p.latitude) maxLat = p.latitude // find min latitude if (minLat == -4000.0) minLat = p.latitude else if (minLat > p.latitude) minLat = p.latitude if(maxLong == -4000.0 || maxLong < p.longitude ) maxLong = p.longitude if (minLong == -4000.0 || minLong > p.longitude) minLong = p.longitude } val del = 0.008 //map.controller.c Log.d(DEBUG_TAG, "Setting limits of bounding box of line: $minLat -> $maxLat, $minLong -> $maxLong") map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), false) } private fun showPatternWithStopsOnMap(stops: List){ Log.d(DEBUG_TAG, "Got the stops: ${stops.map { s->s.gtfsID }}}") if(viewingPattern==null || map == null) return val pattern = viewingPattern!!.pattern val pointsList = PolylineParser.decodePolyline(pattern.patternGeometryPoly, pattern.patternGeometryLength) var maxLat = -4000.0 var minLat = -4000.0 var minLong = -4000.0 var maxLong = -4000.0 for (p in pointsList){ // get max latitude if(maxLat == -4000.0) maxLat = p.latitude else if (maxLat < p.latitude) maxLat = p.latitude // find min latitude if (minLat == -4000.0) minLat = p.latitude else if (minLat > p.latitude) minLat = p.latitude if(maxLong == -4000.0 || maxLong < p.longitude ) maxLong = p.longitude if (minLong == -4000.0 || minLong > p.longitude) minLong = p.longitude } //val polyLine=Polyline(map) //polyLine.setPoints(pointsList) //save points if(map.overlayManager.contains(polyline)){ map.overlayManager.remove(polyline) } polyline = Polyline(map, false) polyline!!.setPoints(pointsList) //polyline.color = ContextCompat.getColor(context!!,R.color.brown_vd) polyline!!.infoWindow = null val paint = Paint() paint.color = ContextCompat.getColor(requireContext(),R.color.line_drawn_poly) paint.isAntiAlias = true paint.strokeWidth = 16f paint.style = Paint.Style.FILL_AND_STROKE paint.strokeJoin = Paint.Join.ROUND paint.strokeCap = Paint.Cap.ROUND polyline!!.outlinePaintLists.add(MonochromaticPaintList(paint)) map.overlayManager.add(0,polyline!!) stopsOverlay.closeAllInfoWindows() stopsOverlay.items.clear() val stopIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ball) for(s in stops){ val gp = if (showOnTopOfLine) findOptimalPosition(s,pointsList) else GeoPoint(s.latitude!!,s.longitude!!) val marker = MarkerUtils.makeMarker( gp, s.ID, s.stopDefaultName, s.routesThatStopHereToString(), map,stopTouchResponder, stopIcon, R.layout.linedetail_stop_infowindow, R.color.line_drawn_poly ) marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) stopsOverlay.add(marker) } //POINTS LIST IS NOT IN ORDER ANY MORE //if(!map.overlayManager.contains(stopsOverlay)){ // map.overlayManager.add(stopsOverlay) //} polyline!!.setOnClickListener(Polyline.OnClickListener { polyline, mapView, eventPos -> Log.d(DEBUG_TAG, "clicked") true }) //map.controller.zoomToB//#animateTo(pointsList[0]) val del = 0.008 map.zoomToBoundingBox(BoundingBox(maxLat+del, maxLong+del, minLat-del, minLong-del), true) //map.invalidate() } private fun initializeRecyclerView(){ val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager } private fun showStopsAsList(stops: List){ Log.d(DEBUG_TAG, "Setting stops from: "+viewModel.currentPatternStops.value) val orderBy = viewModel.currentPatternStops.value!!.withIndex().associate{it.value.stopGtfsId to it.index} val stopsSorted = stops.sortedBy { s -> orderBy[s.gtfsID] } val numStops = stopsSorted.size Log.d(DEBUG_TAG, "RecyclerView adapter is: ${stopsRecyclerView.adapter}") val setNewAdapter = true if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, stopAdapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } /** * Remove bus marker from overlay associated with tripID */ private fun removeBusMarker(tripID: String){ if(!busPositionMarkersByTrip.containsKey(tripID)){ Log.e(DEBUG_TAG, "Asked to remove veh with tripID $tripID but it's supposedly not shown") return } val marker = busPositionMarkersByTrip[tripID] busPositionsOverlay.remove(marker) busPositionMarkersByTrip.remove(tripID) val animator = tripMarkersAnimators[tripID] animator?.let{ it.cancel() tripMarkersAnimators.remove(tripID) } } private fun showPatternWithStop(patternId: String){ //var index = 0 Log.d(DEBUG_TAG, "Showing pattern with code $patternId ") for (i in currentPatterns.indices){ val pattStop = currentPatterns[i] if(pattStop.pattern.code == patternId){ Log.d(DEBUG_TAG, "Pattern found in position $i") //setPatternAndReqStops(pattStop) patternsSpinner.setSelection(i) break } } } /** * draw the position of the buses in the map. Copied from MapFragment */ private fun updateBusPositionsInMap(tripsPatterns: java.util.HashMap> ) { //Log.d(MapFragment.DEBUG_TAG, "Updating positions of the buses") //if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); val noPatternsTrips = ArrayList() for (tripID in tripsPatterns.keys) { val (update, tripWithPatternStops) = tripsPatterns[tripID] ?: continue var marker: Marker? = null //check if Marker is already created if (busPositionMarkersByTrip.containsKey(tripID)) { //check if the trip direction ID is the same, if not remove if(tripWithPatternStops?.pattern != null && tripWithPatternStops.pattern.directionId != viewingPattern?.pattern?.directionId){ removeBusMarker(tripID) } else { //need to change the position of the marker marker = busPositionMarkersByTrip.get(tripID)!! BusPositionUtils.updateBusPositionMarker(map, marker, update, tripMarkersAnimators, false) // Set the pattern to add the info if (marker.infoWindow != null && marker.infoWindow is BusInfoWindow) { val window = marker.infoWindow as BusInfoWindow if (window.pattern == null && tripWithPatternStops != null) { //Log.d(DEBUG_TAG, "Update pattern for trip: "+tripID); window.setPatternAndDraw(tripWithPatternStops.pattern) } } } } else { //marker is not there, need to make it //if (mapView == null) Log.e(MapFragment.DEBUG_TAG, "Creating marker with null map, things will explode") marker = Marker(map) //String route = GtfsUtils.getLineNameFromGtfsID(update.getRouteID()); val mdraw = ResourcesCompat.getDrawable(getResources(), R.drawable.map_bus_position_icon, null)!! //mdraw.setBounds(0,0,28,28); marker.icon = mdraw var markerPattern: MatoPattern? = null if (tripWithPatternStops != null) { if (tripWithPatternStops.pattern != null) markerPattern = tripWithPatternStops.pattern } marker.infoWindow = BusInfoWindow(map, update, markerPattern, true) { // set pattern to show if(it!=null) showPatternWithStop(it.code) } //marker.infoWindow as BusInfoWindow marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER) BusPositionUtils.updateBusPositionMarker(map,marker, update, tripMarkersAnimators,true) // the overlay is null when it's not attached yet? // cannot recreate it because it becomes null very soon // if(busPositionsOverlay == null) busPositionsOverlay = new FolderOverlay(); //save the marker if (busPositionsOverlay != null) { busPositionsOverlay.add(marker) busPositionMarkersByTrip.put(tripID, marker) } } } if (noPatternsTrips.size > 0) { Log.i(DEBUG_TAG, "These trips have no matching pattern: $noPatternsTrips") } } override fun onResume() { super.onResume() Log.d(DEBUG_TAG, "Resetting paused from onResume") pausedFragment = false val keySourcePositions = getString(R.string.pref_positions_source) useMQTTPositions = PreferenceManager.getDefaultSharedPreferences(requireContext()) .getString(keySourcePositions, "mqtt").contentEquals("mqtt") //separate paths if(useMQTTPositions) liveBusViewModel.requestMatoPosUpdates(GtfsUtils.getLineNameFromGtfsID(lineID)) else liveBusViewModel.requestGTFSUpdates() if(mapViewModel.currentLat.value!=MapViewModel.INVALID) { Log.d(DEBUG_TAG, "mapViewModel posi: ${mapViewModel.currentLat.value}, ${mapViewModel.currentLong.value}"+ " zoom ${mapViewModel.currentZoom.value}") val controller = map.controller viewLifecycleOwner.lifecycleScope.launch { delay(100) Log.d(DEBUG_TAG, "zooming back to point") controller.animateTo(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!), mapViewModel.currentZoom.value!!,null,null) //controller.setCenter(GeoPoint(mapViewModel.currentLat.value!!, mapViewModel.currentLong.value!!)) //controller.setZoom(mapViewModel.currentZoom.value!!) } //controller.setZoom() } //initialize GUI here fragmentListener.readyGUIfor(FragmentKind.LINES) } override fun onPause() { super.onPause() liveBusViewModel.stopMatoUpdates() pausedFragment = true //save map val center = map.mapCenter mapViewModel.currentLat.value = center.latitude mapViewModel.currentLong.value = center.longitude mapViewModel.currentZoom.value = map.zoomLevel.toDouble() } override fun getBaseViewForSnackBar(): View? { return null } companion object { private const val LINEID_KEY="lineID" fun newInstance() = LinesDetailFragment() const val DEBUG_TAG="LinesDetailFragment" fun makeArgs(lineID: String): Bundle{ val b = Bundle() b.putString(LINEID_KEY, lineID) return b } @JvmStatic private fun findOptimalPosition(stop: Stop, pointsList: MutableList): GeoPoint{ if(stop.latitude==null || stop.longitude ==null|| pointsList.isEmpty()) throw IllegalArgumentException() val sLat = stop.latitude!! val sLong = stop.longitude!! if(pointsList.size < 2) return pointsList[0] pointsList.sortBy { utils.measuredistanceBetween(sLat, sLong, it.latitude, it.longitude) } val p1 = pointsList[0] val p2 = pointsList[1] if (p1.longitude == p2.longitude){ //Log.e(DEBUG_TAG, "Same longitude") return GeoPoint(sLat, p1.longitude) } else if (p1.latitude == p2.latitude){ //Log.d(DEBUG_TAG, "Same latitude") return GeoPoint(p2.latitude,sLong) } val m = (p1.latitude - p2.latitude) / (p1.longitude - p2.longitude) val minv = (p1.longitude-p2.longitude)/(p1.latitude - p2.latitude) val cR = p1.latitude - p1.longitude * m val longNew = (minv * sLong + sLat -cR ) / (m+minv) val latNew = (m*longNew + cR) //Log.d(DEBUG_TAG,"Stop ${stop.ID} old pos: ($sLat, $sLong), new pos ($latNew,$longNew)") return GeoPoint(latNew,longNew) } private const val DEFAULT_CENTER_LAT = 45.12 private const val DEFAULT_CENTER_LON = 7.6858 } } \ No newline at end of file diff --git a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java index b80d22b..216464c 100644 --- a/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java +++ b/app/src/main/java/it/reyboz/bustorino/fragments/ResultListFragment.java @@ -1,297 +1,297 @@ /* BusTO - Fragments components Copyright (C) 2016 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.fragments; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.Parcelable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.*; import com.google.android.material.floatingactionbutton.FloatingActionButton; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.FiveTNormalizer; import it.reyboz.bustorino.backend.Palina; import it.reyboz.bustorino.backend.Route; import it.reyboz.bustorino.backend.Stop; import it.reyboz.bustorino.data.UserDB; /** * This is a generalized fragment that can be used both for * * */ public class ResultListFragment extends Fragment{ // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER static final String LIST_TYPE = "list-type"; protected static final String LIST_STATE = "list_state"; protected static final String MESSAGE_TEXT_VIEW = "message_text_view"; private FragmentKind adapterKind; protected FragmentListenerMain mListener; protected TextView messageTextView; protected ListView resultsListView; private ListAdapter mListAdapter = null; boolean listShown; private Parcelable mListInstanceState = null; public ResultListFragment() { // Required empty public constructor } public ListView getResultsListView() { return resultsListView; } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param listType whether the list is used for STOPS or LINES (Orari) * @return A new instance of fragment ResultListFragment. */ public static ResultListFragment newInstance(FragmentKind listType, String eventualStopTitle) { ResultListFragment fragment = new ResultListFragment(); Bundle args = new Bundle(); args.putSerializable(LIST_TYPE, listType); if (eventualStopTitle != null) { args.putString(ArrivalsFragment.STOP_TITLE, eventualStopTitle); } fragment.setArguments(args); return fragment; } public static ResultListFragment newInstance(FragmentKind listType) { return newInstance(listType, null); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { adapterKind = (FragmentKind) getArguments().getSerializable(LIST_TYPE); } } /** * Check if the last Bus Stop is in the favorites * @return true if it iss */ public boolean isStopInFavorites(String busStopId) { boolean found = false; // no stop no party if(busStopId != null) { SQLiteDatabase userDB = new UserDB(getContext()).getReadableDatabase(); found = UserDB.isStopInFavorites(userDB, busStopId); } return found; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View root = inflater.inflate(R.layout.fragment_list_view, container, false); messageTextView = (TextView) root.findViewById(R.id.messageTextView); if (adapterKind != null) { resultsListView = (ListView) root.findViewById(R.id.resultsListView); switch (adapterKind) { case STOPS: resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { /* * Casting because of Javamerda * @url http://stackoverflow.com/questions/30549485/androids-list-view-parameterized-type-in-adapterview-onitemclicklistener */ Stop busStop = (Stop) parent.getItemAtPosition(position); mListener.requestArrivalsForStopID(busStop.ID); } }); // set the textviewMessage setTextViewMessage(getString(R.string.results)); break; case ARRIVALS: resultsListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { String routeName; Route r = (Route) parent.getItemAtPosition(position); - routeName = FiveTNormalizer.routeInternalToDisplay(r.getNameForDisplay()); + routeName = FiveTNormalizer.routeInternalToDisplay(r.getName()); if (routeName == null) { - routeName = r.getNameForDisplay(); + routeName = r.getDisplayCode(); } if (r.destinazione == null || r.destinazione.length() == 0) { Toast.makeText(getContext(), getString(R.string.route_towards_unknown, routeName), Toast.LENGTH_SHORT).show(); } else { Toast.makeText(getContext(), getString(R.string.route_towards_destination, routeName, r.destinazione), Toast.LENGTH_SHORT).show(); } } }); String displayName = getArguments().getString(ArrivalsFragment.STOP_TITLE); setTextViewMessage(String.format( getString(R.string.passages), displayName)); break; default: throw new IllegalStateException("Argument passed was not of a supported type"); } String probablemessage = getArguments().getString(MESSAGE_TEXT_VIEW); if (probablemessage != null) { //Log.d("BusTO fragment " + this.getTag(), "We have a possible message here in the savedInstaceState: " + probablemessage); messageTextView.setText(probablemessage); messageTextView.setVisibility(View.VISIBLE); } } else Log.d(getString(R.string.list_fragment_debug), "No content root for fragment"); return root; } public boolean isFragmentForTheSameStop(Palina p) { if (!adapterKind.equals(FragmentKind.ARRIVALS)) return false; if (getTag() != null) return getTag().equals(getFragmentTag(p)); else return false; } public static String getFragmentTag(Palina p) { return "palina_"+p.ID; } @Override public void onResume() { super.onResume(); //Log.d(getString(R.string.list_fragment_debug),"Fragment restored, saved listAdapter is "+(mListAdapter)); if (mListAdapter != null) { ListAdapter adapter = mListAdapter; mListAdapter = null; resetListAdapter(adapter); } if (mListInstanceState != null) { Log.d("resultsListView", "trying to restore instance state"); resultsListView.onRestoreInstanceState(mListInstanceState); } switch (adapterKind) { case ARRIVALS: resultsListView.setOnScrollListener(new CommonScrollListener(mListener, true)); mListener.showFloatingActionButton(true); break; case STOPS: resultsListView.setOnScrollListener(new CommonScrollListener(mListener, false)); break; default: //NONE } mListener.readyGUIfor(adapterKind); } @Override public void onPause() { if (adapterKind.equals(FragmentKind.ARRIVALS)) { SwipeRefreshLayout reflay = getActivity().findViewById(R.id.listRefreshLayout); reflay.setEnabled(false); Log.d("BusTO Fragment " + this.getTag(), "RefreshLayout disabled"); } super.onPause(); } @Override public void onAttach(@NonNull Context context) { super.onAttach(context); if (context instanceof FragmentListenerMain) { mListener = (FragmentListenerMain) context; } else { throw new RuntimeException(context.toString() + " must implement ResultFragmentListener"); } } @Override public void onDetach() { mListener.showFloatingActionButton(false); mListener = null; super.onDetach(); } @Override public void onDestroyView() { resultsListView = null; //Log.d(getString(R.string.list_fragment_debug), "called onDestroyView"); getArguments().putString(MESSAGE_TEXT_VIEW, messageTextView.getText().toString()); super.onDestroyView(); } @Override public void onViewStateRestored(@Nullable Bundle savedInstanceState) { super.onViewStateRestored(savedInstanceState); Log.d("ResultListFragment", "onViewStateRestored"); if (savedInstanceState != null) { mListInstanceState = savedInstanceState.getParcelable(LIST_STATE); Log.d("ResultListFragment", "listInstanceStatePresent :" + mListInstanceState); } } protected void resetListAdapter(ListAdapter adapter) { boolean hadAdapter = mListAdapter != null; mListAdapter = adapter; if (resultsListView != null) { resultsListView.setAdapter(adapter); resultsListView.setVisibility(View.VISIBLE); } } /** * Set the message textView * @param message the whole message to write in the textView */ public void setTextViewMessage(String message) { messageTextView.setText(message); messageTextView.setVisibility(View.VISIBLE); } } \ No newline at end of file diff --git a/app/src/main/res/layout/entry_bus_line_passage.xml b/app/src/main/res/layout/entry_bus_line_passage.xml index 4906347..f064938 100644 --- a/app/src/main/res/layout/entry_bus_line_passage.xml +++ b/app/src/main/res/layout/entry_bus_line_passage.xml @@ -1,54 +1,79 @@ - - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 8c029ae..f257311 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,256 +1,256 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code Si No Prossimo Precedente Installare Barcode Scanner? Questa azione richiede un\'altra app per scansionare i codici QR. Vuoi installare Barcode Scanner? Numero fermata Nome fermata Inserisci il numero della fermata Inserisci il nome della fermata Verifica l\'accesso ad Internet! Sembra che nessuna fermata abbia questo nome Nessun passaggio trovato alla fermata Errore di lettura del sito 5T/GTT (dannato sito!) Fermata: %1$s Linea Linee Linee urbane Linee extraurbane Linee turistiche Direzione: Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code trovato, prova ad usare un\'altra app Preferiti Aiuto Informazioni Più informazioni Contribuisci https://gitpull.it/w/librebusto/it/ Codice sorgente Licenza Incontra l\'autore Fermata aggiunta ai preferiti Impossibile aggiungere ai preferiti (memoria piena o database corrotto?)! Preferiti Mappa Nessun preferito? Arghh!\nSchiaccia sulla stella di una fermata per aggiungerla a questa lista! Rimuovi Rinomina Rinomina fermata Reset Informazioni Tocca la stella per aggiungere la fermata ai preferiti\n\nCome leggere gli orari: \n   12:56* Orario in tempo reale\n   12:56   Orario programmato\n\nTrascina giù per aggiornare l\'orario. \nTocca a lungo su Fonte Orari per cambiare sorgente degli orari di arrivo. OK! Benvenuto!

Grazie per aver scelto BusTO, un\'app indipendente da GTT/5T, per spostarsi a Torino attraverso software libero:


Perché usare BusTO?

- Non sei monitorato
- Non ci sono pubblicità
- La tua privacy è al sicuro
- Inoltre l\'app è molto leggera!


Come funziona?

Quest\'app ottiene i passaggi dei bus, le fermate e altre informazioni utili unendo dati forniti dal sito www.gtt.to.it, www.5t.torino.it, muoversiatorino.it "per uso personale" e altre fonti Open Data (aperto.comune.torino.it).


Ingredienti:
- Fabio Mazza attuale rockstar developer anziano.
- Andrea Ugo attuale rockstar developer in formazione.
- Silviu Chiriac designer del logo 2021.
- Marco M formidabile tester e cacciatore di bug.
- Ludovico Pavesi ex rockstar developer anziano asd.
- Valerio Bozzolan attuale manutentore.
- Marco Gagino apprezzato ex collaboratore, ideatore icona e grafica.
- JSoup libreria per "web scaping".
- Google icone e libreria di supporto per il Material Design.
- Tutti i contributori e i beta tester!


Licenze

L\'app e il relativo codice sorgente sono distribuiti sotto la licenza GNU General Public License v3+. Ciò significa che puoi usare, studiare, migliorare e ricondividere quest\'app con qualunque mezzo e per qualsiasi scopo: a patto di mantenere sempre questi diritti a tua volta e di dare credito a Valerio Bozzolan.


Note

Quest\'applicazione è rilasciata nella speranza che sia utile a tutti ma senza NESSUNA garanzia sul suo funzionamento attuale e/o futuro.

Tutti i dati utilizzati dall\'app provengono direttamente da GTT o da simili agenzie pubbliche: se trovi che sono inesatti per qualche motivo, ti invitiamo a rivolgerti a loro.

Buon utilizzo! :)

]]> Nome troppo corto, digita più caratteri e riprova %1$s verso %2$s %s (destinazione sconosciuta) Errore interno inaspettato, impossibile estrarre dati dal sito GTT/5T Visualizza sulla mappa Non trovo un\'applicazione dove mostrarla Posizione della fermata non trovata Fermate vicine Ricerca della posizione Nessuna fermata nei dintorni Preferenze Aggiornamento del database… Aggiornamento del database Aggiornamento database forzato Tocca per aggiornare ora il database Numero minimo di fermate Il numero di fermate da ricercare non è valido Valore errato, inserisci un numero Impostazioni Distanza massima di ricerca (m) Funzionalità sperimentali Impostazioni Generali Fermate recenti Impostazioni generali Gestione del database Comincia aggiornamento manuale del database Consenti l\'accesso alla posizione per mostrarla sulla mappa Abilitare il GPS arriva alle alla fermata Mostra arrivi Mostra fermate Arrivi qui vicino Fermata rimossa dai preferiti Canale Telegram La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Fonti orari di arrivo Scegli le fonti di orari da usare Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Nessun passaggio per le linee: Canale unico delle notifiche Database Informazioni sul database (aggiornamento) Downloading trips from MaTO server Chiesto troppe volte per il permesso %1$s Non si può usare questa funzionalità senza il permesso di archivio di archivio Un bug ha fatto crashare l\'app! \nPremi \"OK\" per inviare il report agli sviluppatori via email, così potranno scovare e risolvere il tuo bug! \nIl report contiene piccole informazioni non sensibili sulla configurazione del tuo telefono e sullo stato dell\'app al momento del crash. L\'applicazione è crashata, e il crash report è stato messo negli allegati. Se vuoi, descrivi cosa stavi facendo prima che si interrompesse: \n Arrivi Mappa Preferiti Apri drawer Chiudi drawer Esperimenti Offrici un caffè Mappa Ricerca fermate Versione app Orari di arrivo Richiesto aggiornamento del database Download dati dal server MaTO Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola Mostra arrivi quando tocchi una fermata Abilita esperimenti Schermata da mostrare all\'avvio Tocca per cambiare Fonte posizioni in tempo reale di bus e tram MaTO (aggiornate più spesso, può non funzionare) GTFS RT (più stabile) Linea aggiunta ai preferiti Linea rimossa dai preferiti Preferite Tocca a lungo la fermata per le opzioni - Rimuovi tutti i trip GTFS + Rimuovi i dati dei trip (libera spazio) Tutti i trip GTFS sono rimossi dal database Mostra tutorial open source per il trasporto pubblico di Torino. Stai usando un\'app indipendente, senza pubblicità e senza nessun tracciamento. ]]> Se ti trovi a una fermata, puoi scansionare il codice QR presente sulla palina toccando l\'icona a sinistra della barra di ricerca.]]> preferiti toccando la stella a fianco del nome.]]> fermate più vicine a te direttamente nella schermata principale...]]> posizioni in tempo reale dei bus e tram (in blu)]]> Guarda nelle Impostazioni per personalizzare l\'app come preferisci, e su Informazioni per sapere di più sull\'app e il team di sviluppo.]]> Capito, chiudi il tutorial diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 74bbb32..87baff0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,58 +1,61 @@ #ff9800 #F57C00 #cc6600 #994d00 #2196F3 #2a65e8 #2060dd #8A4247 #2378e8 #0079f5 #2a968b #0067ff #2F59CC #CC5E43 #548017 #009688 #4DB6AC #80cbc4 #F5F5F5 #dddddd #f8f8f8 #757575 #444 #353535 #303030 #DE0908 #b30000 #dd441f #b30d0d #2060DD #FFFFFF #000000 #1c1c1c @color/blue_mid_2 @color/red_dark @color/blue_extra #FF039BE5 #FF01579B #FF40C4FF #FF00B0FF #66000000 #00000000 + @color/orange_500 + @color/blue_extraurbano + @color/metro_red \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fdee037..ef70276 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,290 +1,290 @@ BusTO Libre BusTO BusTO dev BusTO git You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code Yes No Next Previous Install Barcode Scanner? This application requires an app to scan the QR codes. Would you like to install Barcode Scanner now? Bus stop number Bus stop name Insert bus stop number Insert bus stop name %1$s towards %2$s %s (unknown destination) Verify your Internet connection! Seems that no bus stop have this name No arrivals found for this stop Error parsing the 5T/GTT website (damn site!) Name too short, type more characters and retry Arrivals at: %1$s Choose the bus stop… Line Lines Urban lines Extra urban lines Tourist lines Destination: Lines: %1$s Line: %1$s No timetable found No QR code found, try using another app to scan Unexpected internal error, cannot extract data from GTT/5T website Help About the app More about Contribute https://gitpull.it/w/librebusto/en/ Source code Licence11 Meet the author Bus stop is now in your favorites Bus stop removed from your favorites Added line to favorites Remove line from favorites Favorites Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About the app Tap the star to add the bus stop to the favourites\n\nHow to read timelines:\n   12:56* Real-time arrivals\n   12:56   Scheduled arrivals\n\nPull down to refresh the timetable \n Long press on Arrivals source to change the source of the arrival times GOT IT! Arrival times No arrivals found for lines: Welcome!

Thanks for using BusTO, a "politically" independent app useful to move around Torino using a Free/Libre software.


Why use this app?

- You\'ll never be tracked
- You\'ll never see boring ads
- We\'ll always respect your privacy
- Moreover, it\'s lightweight!


How does it work?

This app is able to do all the amazing things it does by pulling data from www.gtt.to.it, www.5t.torino.it or muoversiatorino.it "for personal use", along with open data from the AperTO (aperto.comune.torino.it) website.


The work of several people is behind this app, in particular:
- Fabio Mazza, current senior rockstar developer.
- Andrea Ugo, current junior rockstar developer.
- Silviu Chiriac, designer of the 2021 logo.
- Marco M, rockstar tester and bug hunter.
- Ludovico Pavesi, previous senior rockstar developer (asd).
- Valerio Bozzolan, maintainer and infrastructure (sponsor).
- Marco Gagino, contributor and first icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons and Volley framework.
- Android app components.
- All the contributors, and the beta testers, too!


Licenses

The app and the related source code are released by Valerio Bozzolan and the other authors under the terms of the GNU General Public License v3+). So everyone is allowed to use, to study, to improve and to share this app by any kind of means and for any purpose: under the conditions of maintaining this rights and of attributing the original work to Valerio Bozzolan.


Notes

This app has been developed with the hope to be useful to everyone, but comes without ANY warranty of any kind.

The data used by the app comes directly from GTT and other public agencies: if you find any errors, please take it up to them, not to us.

This translation is kindly provided by Riccardo Caniato, Marco Gagino and Fabio Mazza.

Now you can hack public transport, too! :)

]]>
Cannot add to favorites (storage full or corrupted database?)! View on a map Cannot find any application to show it in Cannot find the position of the stop ListFragment - BusTO it.reyboz.bustorino.preferences db_is_updating Nearby stops Nearby connections App version The number of stops to show in the recent stops is invalid Invalid value, put a valid number Finding location No stops nearby Minimum number of stops Preferences Settings Settings General Experimental features Maximum distance (meters) Recent stops General settings Database management Launch manual database update Allow access to position to show it on the map Please enable GPS Database update in progress… Updating the database Force database update Touch to update the app database now is arriving at at the stop %1$s - %2$s Show arrivals Show stops Join Telegram channel Center on my location Follow me Arrivals source: %1$s GTT App GTT Website 5T Torino website Muoversi a Torino app Undetermined Changing arrival times source… Long press to change the source of arrivals @string/source_mato @string/fivetapifetcher @string/gttjsonfetcher @string/fivetscraper Sources of arrival times Select which sources of arrival times to use Default Default channel for notifications Database Notifications on the update of the database Downloading trips from MaTO server Asked for %1$s permission too many times Cannot use the map with the storage permission! storage The application has crashed because you encountered a bug. \nIf you want, you can help the developers by sending the crash report via email. \nNote that no sensitive data is contained in the report, just small bits of info on your phone and app configuration/state. The application crashed and the crash report is in the attachments. Please describe what you were doing before the crash: \n Arrivals Map Favorites Open navigation drawer Close navigation drawer Experiments Buy us a coffee Map Search by stop Launching database update Downloading data from MaTO server Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST Section to show on startup Touch to change it Show arrivals touching on stop Enable experiments Long press the stop for options @string/nav_arrivals_text @string/nav_favorites_text @string/nav_map_text @string/lines Source of real time positions for buses and trams MaTO (updated more frequently, might be offline) GTFS RT (more stable, less frequently updated) - Remove all GTFS trips info + Remove trips data (free up space) All GTFS trips have been removed from the database Show tutorial open source app for Turin public transport. This is an independent app, with no ads and no tracking whatsoever.]]> favorites by touching the star next to its name]]> blue)]]> Settings to customize the app behaviour, and in the About the app section if you want to know more about the app and the developers.]]> OK, close the tutorial
diff --git a/build.gradle b/build.gradle index bc6a1da..ed12bc6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,49 +1,49 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { mavenCentral() maven { url 'https://maven.google.com' } google() maven { url 'https://jitpack.io' } } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20" + classpath 'com.android.tools.build:gradle:7.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22" } } ext { androidXTestVersion = "1.5.0" //multidex multidex_version = "2.0.1" //libraries versions fragment_version = "1.6.1" activity_version = "1.7.2" appcompat_version = "1.6.1" preference_version = "1.2.1" work_version = "2.8.1" acra_version = "5.7.0" lifecycle_version = "2.4.1" arch_version = "2.1.0" room_version = "2.5.2" //kotlin - kotlin_version = '1.8.22' + kotlin_version = '1.8.0' coroutines_version = "1.7.0" } allprojects { repositories { maven { url 'https://maven.google.com' } google() mavenCentral() maven { url "https://jitpack.io" } } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 115a516..973f4d9 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Apr 24 16:03:07 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME