diff --git a/res/drawable-xxxhdpi/bus_stop_background.xml b/res/drawable/bus_stop_background.xml similarity index 100% copy from res/drawable-xxxhdpi/bus_stop_background.xml copy to res/drawable/bus_stop_background.xml diff --git a/res/drawable-xxxhdpi/bus_stop_background.xml b/res/drawable/bus_stop_background_pattern.xml similarity index 69% rename from res/drawable-xxxhdpi/bus_stop_background.xml rename to res/drawable/bus_stop_background_pattern.xml index 3d9ef7a..3f86294 100644 --- a/res/drawable-xxxhdpi/bus_stop_background.xml +++ b/res/drawable/bus_stop_background_pattern.xml @@ -1,8 +1,8 @@ - + \ No newline at end of file diff --git a/res/drawable/line.xml b/res/drawable/line.xml new file mode 100644 index 0000000..f2c2c6a --- /dev/null +++ b/res/drawable/line.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/res/drawable/line_drawable.xml b/res/drawable/line_drawable.xml new file mode 100644 index 0000000..86276c2 --- /dev/null +++ b/res/drawable/line_drawable.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/res/drawable/thumb.xml b/res/drawable/thumb.xml new file mode 100644 index 0000000..f7d476f --- /dev/null +++ b/res/drawable/thumb.xml @@ -0,0 +1,18 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/drawable/thumb_drawable.xml b/res/drawable/thumb_drawable.xml new file mode 100644 index 0000000..d8556ff --- /dev/null +++ b/res/drawable/thumb_drawable.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/res/layout/arrivals_nearby_card.xml b/res/layout/arrivals_nearby_card.xml index a728c51..8c10ca7 100644 --- a/res/layout/arrivals_nearby_card.xml +++ b/res/layout/arrivals_nearby_card.xml @@ -1,98 +1,100 @@ + card_view:cardCornerRadius="5dp" + android:layout_margin="5dp" android:padding="6dp"> + android:gravity="center_vertical" + android:textStyle="normal" android:layout_marginRight="10dp" android:layout_marginEnd="10dp" + android:layout_marginBottom="10dp" android:layout_marginTop="10dp"/> + android:layout_marginStart="10dp"/> \ No newline at end of file diff --git a/res/layout/entry_bus_stop.xml b/res/layout/entry_bus_stop.xml index f6cd113..6914fb3 100644 --- a/res/layout/entry_bus_stop.xml +++ b/res/layout/entry_bus_stop.xml @@ -1,74 +1,76 @@ + android:paddingBottom="8dip" + android:background="?android:attr/selectableItemBackground" +> \ No newline at end of file diff --git a/res/layout/fragment_lines.xml b/res/layout/fragment_lines.xml index f82eeb0..e0c393a 100644 --- a/res/layout/fragment_lines.xml +++ b/res/layout/fragment_lines.xml @@ -1,103 +1,110 @@ + app:layout_constraintHorizontal_bias="0.0" + app:fastScrollEnabled="true" + app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable" + app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable" + app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable" + app:fastScrollVerticalTrackDrawable="@drawable/line_drawable" + + /> \ No newline at end of file diff --git a/res/layout/stop_card.xml b/res/layout/stop_card.xml index b707222..c18c1c1 100644 --- a/res/layout/stop_card.xml +++ b/res/layout/stop_card.xml @@ -1,117 +1,117 @@ + card_view:cardCornerRadius="5dp" + android:layout_margin="5dp" android:padding="4dp"> \ No newline at end of file diff --git a/res/layout/bus_stop_line_elmt.xml b/res/layout/stop_line_element.xml similarity index 78% rename from res/layout/bus_stop_line_elmt.xml rename to res/layout/stop_line_element.xml index 75e65ec..d057fc7 100644 --- a/res/layout/bus_stop_line_elmt.xml +++ b/res/layout/stop_line_element.xml @@ -1,96 +1,99 @@ + android:layout_marginLeft="26dp" + android:layout_marginStart="26dp"/> + android:layout_marginLeft="26dp" + android:layout_marginStart="26dp"/> + android:textColor="@color/grey_600" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginBottom="9dp"/> \ No newline at end of file diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 8ebd3c7..c2f96f2 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -1,188 +1,192 @@ Stai utilizzando l\'ultimo ritrovato in materia di rispetto della tua privacy. Cerca QR Code 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 Linea: %1$s Linee: %1$s Scegli la fermata… Nessun passaggio Nessun QR code 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 in tempo reale filtrando i dati forniti pubblicamente sul sito www.gtt.to.it o www.5t.torino.it "per uso personale".

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!

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.

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 in corso… 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 La mia posizione Segui posizione Fonte orari: %1$s App GTT Sito GTT Sito 5T Torino App Muoversi a Torino Sconosciuta Cambiamento sorgente orari… Premi a lungo per cambiare la sorgente degli orari Canale unico delle notifiche Database Informazioni sul database (aggiornamento) 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 Mostra direzioni in maiuscolo Non cambiare Tutto in maiuscolo Solo la prima lettera maiuscola + Mostra arrivi quando tocchi una fermata + Abilita esperimenti + + - Tocca a lungo per le opzioni + Tocca a lungo la fermata per le opzioni
diff --git a/res/values/colors.xml b/res/values/colors.xml index 800aec7..8f32e5f 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -1,20 +1,26 @@ #ff9800 #F57C00 #2196F3 + #2a65e8 #2060dd + + #2378e8 + #0079f5 + #009688 #4DB6AC #80cbc4 #F5F5F5 #757575 #DE0908 #2060DD #FFFFFF #000000 + @color/blue_mid_2 \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 06b37fd..31ee1ec 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,207 +1,210 @@ BusTO Libre BusTO BusTO dev You\'re using the latest in technology when it comes to respecting your privacy. Search Scan QR Code 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 Lines: %1$s Line: %1$s No timetable found No QR code Unexpected internal error, cannot extract data from GTT/5T website Help About 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 Favorites Favorites Map No favorites? Arghh! Press on a bus stop star to populate this list! Delete Rename Rename the bus stop Reset About 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 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 will show you bus timetables gathering data from www.gtt.to.it or www.5t.torino.it "for personal use".

Who worked on BusTO:
- 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 icon creator.
- JSoup web scraper library.
- makovkastar floating buttons.
- Google Material Design icons.
- All the contributors!

Licenses

The app and the related source code are released by Valerio Bozzolan 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 hoping to be useful to everyone but without ANY warranty.

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

Get involved! :)

]]>
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 recents is invalid Invalid value, put a valid number Finding the position… 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 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 Default Default channel for notifications Database Notifications on the update of the database 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 Capitalize directions Do not change arrivals directions Capitalize everything Capitalize only first letter KEEP CAPITALIZE_ALL CAPITALIZE_FIRST + Show arrivals touching on stop + Enable experiments - Long press for options + Long press the stop for options +
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 5070733..25c4cd2 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,69 +1,79 @@ - - - + + + + + + + diff --git a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java index 5007ced..f3a5d8b 100644 --- a/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java +++ b/src/it/reyboz/bustorino/adapters/StopRecyclerAdapter.java @@ -1,232 +1,231 @@ /* BusTO - Adapter components Copyright (C) 2021 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.adapters; -import android.util.Log; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import java.util.List; import it.reyboz.bustorino.R; import it.reyboz.bustorino.backend.Stop; public class StopRecyclerAdapter extends RecyclerView.Adapter { private List stops; private static final int ITEM_LAYOUT_FAVORITES = R.layout.entry_bus_stop; - private static final int ITEM_LAYOUT_LINES = R.layout.bus_stop_line_elmt; + private static final int ITEM_LAYOUT_LINES = R.layout.stop_line_element; 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 static final int cityIcon = R.drawable.city; private NameCapitalize capitalizeLocation = NameCapitalize.DO_NOTHING; private final Use usedFor; private final StopAdapterListener listener; private int position; protected static class ViewHolder extends RecyclerView.ViewHolder implements View.OnCreateContextMenuListener{ TextView busStopIDTextView; TextView busStopNameTextView; //TextView busLineVehicleIcon; TextView busStopLinesTextView; TextView busStopLocaLityTextView; View topStub, bottomStub; Stop mStop; int menuResID=R.menu.menu_favourites_entry; public ViewHolder(@NonNull View itemView, StopAdapterListener listener, Use usedFor) { super(itemView); busStopIDTextView = itemView.findViewById(R.id.busStopID); busStopNameTextView = itemView.findViewById(R.id.busStopName); busStopLinesTextView = itemView.findViewById(R.id.routesThatStopHere); busStopLocaLityTextView = itemView.findViewById(R.id.busStopLocality); switch (usedFor){ case LINES: topStub = itemView.findViewById(R.id.topStub); bottomStub = itemView.findViewById(R.id.bottomStub); menuResID = R.menu.menu_line_item; break; case FAVORITES: default: topStub = null; bottomStub = null; } mStop = new Stop(""); itemView.setOnClickListener(view -> { listener.onTappedStop(mStop); }); } //many thanks to https://stackoverflow.com/questions/26466877/how-to-create-context-menu-for-recyclerview @Override public void onCreateContextMenu(ContextMenu contextMenu, View view, ContextMenu.ContextMenuInfo contextMenuInfo) { MenuInflater inflater = new MenuInflater(view.getContext()); inflater.inflate(menuResID, contextMenu); } } public StopRecyclerAdapter(List stops, StopAdapterListener listener, Use usedFor) { this.stops = stops; this.listener = listener; this.usedFor = usedFor; } public StopRecyclerAdapter(List stops, StopAdapterListener listener, Use usedFor, NameCapitalize locationCapit) { this.stops = stops; this.listener = listener; this.usedFor = usedFor; this.capitalizeLocation = locationCapit; } public NameCapitalize getCapitalizeLocation() { return capitalizeLocation; } public void setCapitalizeLocation(NameCapitalize capitalizeLocation) { this.capitalizeLocation = capitalizeLocation; notifyDataSetChanged(); } public void setStops(List stops){ this.stops = stops; notifyDataSetChanged(); } public List getStops() { return stops; } public int getPosition() { return position; } public void setPosition(int position) { this.position = position; } @NonNull @Override public StopRecyclerAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { final int layoutID; switch (usedFor){ case LINES: layoutID = ITEM_LAYOUT_LINES; break; case FAVORITES: default: layoutID = ITEM_LAYOUT_FAVORITES; } View view = LayoutInflater.from(parent.getContext()) .inflate(layoutID, parent, false); return new StopRecyclerAdapter.ViewHolder(view, listener, this.usedFor); } @Override public void onViewRecycled(@NonNull StopRecyclerAdapter.ViewHolder holder) { holder.itemView.setOnLongClickListener(null); super.onViewRecycled(holder); } @Override public void onBindViewHolder(@NonNull StopRecyclerAdapter.ViewHolder vh, int position) { //Log.d("StopRecyclerAdapter", "Called for position "+position); Stop stop = stops.get(position); vh.busStopIDTextView.setText(stop.ID); vh.mStop = stop; //Log.d("StopRecyclerAdapter", "Stop: "+stop.ID); // NOTE: intentionally ignoring stop username in search results: if it's in the favorites, why are you searching for it? vh.busStopNameTextView.setText(stop.getStopDisplayName()); String whatStopsHere = stop.routesThatStopHereToString(); if(whatStopsHere == null) { vh.busStopLinesTextView.setVisibility(View.GONE); } else { vh.busStopLinesTextView.setText(whatStopsHere); vh.busStopLinesTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } if(stop.type == null) { vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); } else { switch(stop.type) { case BUS: default: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(busIcon, 0, 0, 0); break; case METRO: case RAILWAY: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(trainIcon, 0, 0, 0); break; case TRAM: vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(tramIcon, 0, 0, 0); break; case LONG_DISTANCE_BUS: // è l'opposto della città ma va beh, dettagli. vh.busStopLinesTextView.setCompoundDrawablesWithIntrinsicBounds(cityIcon, 0, 0, 0); } } if (stop.location == null) { vh.busStopLocaLityTextView.setVisibility(View.GONE); } else { vh.busStopLocaLityTextView.setText(NameCapitalize.capitalizePass(stop.location, capitalizeLocation)); vh.busStopLocaLityTextView.setVisibility(View.VISIBLE); // might be GONE due to View Holder Pattern } //trick to set the position vh.itemView.setOnLongClickListener(view -> { setPosition(vh.getAdapterPosition()); return false; }); if(this.usedFor == Use.LINES){ //vh.menuResID; vh.bottomStub.setVisibility(View.VISIBLE); vh.topStub.setVisibility(View.VISIBLE); if(position == 0) { - vh.topStub.setVisibility(View.GONE); + vh.topStub.setVisibility(View.INVISIBLE); } else if (position == stops.size()-1) { vh.bottomStub.setVisibility(View.GONE); } } } @Override public int getItemCount() { return stops.size(); } public enum Use{ FAVORITES, LINES } } diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java index a638a89..02d13c4 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,226 +1,256 @@ +/* + BusTO (backend components) + Copyright (C) 2019 Fabio Mazza + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ package it.reyboz.bustorino.backend; import android.content.Context; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import android.view.View; import androidx.annotation.Nullable; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import it.reyboz.bustorino.backend.mato.MatoAPIFetcher; public abstract class utils { private static final double EarthRadius = 6371e3; public static Double measuredistanceBetween(double lat1,double long1,double lat2,double long2){ final double phi1 = Math.toRadians(lat1); final double phi2 = Math.toRadians(lat2); final double deltaPhi = Math.toRadians(lat2-lat1); final double deltaTheta = Math.toRadians(long2-long1); final double a = Math.sin(deltaPhi/2)*Math.sin(deltaPhi/2)+ Math.cos(phi1)*Math.cos(phi2)*Math.sin(deltaTheta/2)*Math.sin(deltaTheta/2); final double c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)); return Math.abs(EarthRadius*c); } public static Double angleRawDifferenceFromMeters(double distanceInMeters){ return Math.toDegrees(distanceInMeters/EarthRadius); } /* public static int convertDipToPixels(Context con,float dips) { return (int) (dips * con.getResources().getDisplayMetrics().density + 0.5f); } */ public static float convertDipToPixels(Context con, float dp){ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,con.getResources().getDisplayMetrics()); } /* public static int calculateNumColumnsFromSize(View containerView, int pixelsize){ int width = containerView.getWidth(); float ncols = ((float)width)/pixelsize; return (int) Math.floor(ncols); } */ /** * Check if there is an internet connection * @param con context object to get the system service * @return true if we are */ public static boolean isConnected(Context con) { ConnectivityManager connMgr = (ConnectivityManager) con.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); return networkInfo != null && networkInfo.isConnected(); } ///////////////////// INTENT HELPER //////////////////////////////////////////////////////////// /** * Try to extract the bus stop ID from a URi * * @param uri The URL * @return bus stop ID or null */ public static String getBusStopIDFromUri(Uri uri) { String busStopID; // everithing catches fire when passing null to a switch. String host = uri.getHost(); if (host == null) { Log.e("ActivityMain", "Not an URL: " + uri); return null; } switch (host) { case "m.gtt.to.it": // http://m.gtt.to.it/m/it/arrivi.jsp?n=1254 busStopID = uri.getQueryParameter("n"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?n from: " + uri); } break; case "www.gtt.to.it": case "gtt.to.it": // http://www.gtt.to.it/cms/percorari/arrivi?palina=1254 busStopID = uri.getQueryParameter("palina"); if (busStopID == null) { Log.e("ActivityMain", "Expected ?palina from: " + uri); } break; default: Log.e("ActivityMain", "Unexpected intent URL: " + uri); busStopID = null; } return busStopID; } final static Pattern ROMAN_PATTERN = Pattern.compile( "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"); private static boolean isRomanNumber(String str){ if(str.isEmpty()) return false; final Matcher matcher = ROMAN_PATTERN.matcher(str); return matcher.find(); } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.trim().split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (String s : arr) { if (s.length() > 0) { String[] allsubs = s.split("\\."); boolean addPoint = s.contains("."); /*if (s.contains(".lli")|| s.contains(".LLI")) //Fratelli { DOESN'T ALWAYS WORK addPoint = false; allsubs = new String[]{s}; }*/ boolean first = true; for (String subs : allsubs) { if(first) first=false; else { if (addPoint) sb.append("."); sb.append(" "); } if(isRomanNumber(subs)){ //add and skip the rest sb.append(subs); continue; } - sb.append(Character.toUpperCase(subs.charAt(0))); + //SPLIT ON ', check if contains "D'" + if(subs.toLowerCase(Locale.ROOT).startsWith("d'")){ + sb.append("D'"); + subs = subs.substring(2); + } + int index = 0; + char c = subs.charAt(index); + if(subs.length() > 1 && c=='('){ + sb.append(c); + index += 1; + c = subs.charAt(index); + } + sb.append(Character.toUpperCase(c)); if (lowercaseRest) - sb.append(subs.substring(1).toLowerCase(Locale.ROOT)); + sb.append(subs.substring(index+1).toLowerCase(Locale.ROOT)); else - sb.append(subs.substring(1)); + sb.append(subs.substring(index+1)); } + if(addPoint && allsubs.length == 1) sb.append('.'); sb.append(" "); /*sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); */ } else sb.append(s); } return sb.toString().trim(); } /** * Open an URL in the default browser. * * @param url URL */ public static void openIceweasel(String url, Context context) { Intent browserIntent1 = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); if (browserIntent1.resolveActivity(context.getPackageManager()) != null) { //check we have an activity ready to receive intents (otherwise, there will be a crash) context.startActivity(browserIntent1); } else{ Log.e("BusTO","openIceweasel can't find a browser"); } } public static ArrivalsFetcher[] getDefaultArrivalsFetchers(){ return new ArrivalsFetcher[]{ new MatoAPIFetcher(), new FiveTAPIFetcher(), new GTTJSONFetcher(), new FiveTScraperFetcher()}; } /** * Print the first i lines of the the trace of an exception * https://stackoverflow.com/questions/21706722/fetch-only-first-n-lines-of-a-stack-trace */ /* public static String traceCaller(Exception ex, int i) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); StringBuilder sb = new StringBuilder(); ex.printStackTrace(pw); String ss = sw.toString(); String[] splitted = ss.split("\n"); sb.append("\n"); if(splitted.length > 2 + i) { for(int x = 2; x < i+2; x++) { sb.append(splitted[x].trim()); sb.append("\n"); } return sb.toString(); } return "Trace too Short."; } */ public static String joinList(@Nullable List dat, String separator){ StringBuilder sb = new StringBuilder(); if(dat==null || dat.size()==0) return ""; else if(dat.size()==1) return dat.get(0); sb.append(dat.get(0)); for (int i=1; i. */ package it.reyboz.bustorino.fragments import android.content.Context import android.os.Bundle import android.util.Log import android.view.* import android.widget.* import android.widget.AdapterView.INVALID_POSITION import android.widget.AdapterView.OnItemSelectedListener import androidx.lifecycle.ViewModelProvider 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.Stop import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import it.reyboz.bustorino.data.gtfs.PatternStop import it.reyboz.bustorino.util.LinesNameSorter +import it.reyboz.bustorino.util.PatternWithStopsSorter class LinesFragment : ScreenBaseFragment() { companion object { fun newInstance(){ val fragment = LinesFragment() } const val DEBUG_TAG="BusTO-LinesFragment" const val FRAGMENT_TAG="LinesFragment" + + val patternStopsComparator = PatternWithStopsSorter() } private lateinit var viewModel: LinesViewModel private lateinit var linesSpinner: Spinner private lateinit var patternsSpinner: Spinner private lateinit var currentRoutes: List private lateinit var currentPatterns: List private lateinit var currentPatternStops: List private lateinit var routeDescriptionTextView: TextView private lateinit var stopsRecyclerView: RecyclerView private var linesAdapter: ArrayAdapter? = null private var patternsAdapter: ArrayAdapter? = null private var mListener: CommonFragmentListener? = null private val linesNameSorter = LinesNameSorter() private val linesComparator = Comparator { a,b -> return@Comparator linesNameSorter.compare(a.shortName, b.shortName) } + private var firstClick = true; private val adapterListener = object : StopAdapterListener { override fun onTappedStop(stop: Stop?) { //var r = "" //stop?.let { r= it.stopDisplayName.toString() } - Toast.makeText(context,R.string.long_press_for_options,Toast.LENGTH_SHORT).show() + if(viewModel.shouldShowMessage) { + Toast.makeText(context, R.string.long_press_stop_4_options, Toast.LENGTH_SHORT).show() + viewModel.shouldShowMessage=false + } + stop?.let { + mListener?.requestArrivalsForStopID(it.ID) + } + if(stop == null){ + Log.e(DEBUG_TAG,"Passed wrong stop") + } + if(mListener == null){ + Log.e(DEBUG_TAG, "Listener is null") + } } override fun onLongPressOnStop(stop: Stop?): Boolean { - Log.d("BusTO-LinesFrag", "LongPressOnStop") + Log.d(DEBUG_TAG, "LongPressOnStop") return true } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.fragment_lines, container, false) linesSpinner = rootView.findViewById(R.id.linesSpinner) patternsSpinner = rootView.findViewById(R.id.patternsSpinner) routeDescriptionTextView = rootView.findViewById(R.id.routeDescriptionTextView) stopsRecyclerView = rootView.findViewById(R.id.patternStopsRecyclerView) - /* - Stop busStop = (Stop) parent.getItemAtPosition(position); - - if(mListener!=null){ - mListener.requestArrivalsForStopID(busStop.ID); - } - - }); - - */ val llManager = LinearLayoutManager(context) llManager.orientation = LinearLayoutManager.VERTICAL stopsRecyclerView.layoutManager = llManager //allow the context menu to be opened registerForContextMenu(stopsRecyclerView) if(context!=null) { patternsAdapter = ArrayAdapter(context!!, android.R.layout.simple_spinner_dropdown_item, ArrayList()) patternsSpinner.adapter = patternsAdapter linesAdapter = ArrayAdapter(context!!, android.R.layout.simple_spinner_dropdown_item, ArrayList()) linesSpinner.adapter = linesAdapter linesSpinner.onItemSelectedListener = object: OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, pos: Int, p3: Long) { val selRoute = currentRoutes.get(pos) routeDescriptionTextView.text = selRoute.longName - + val oldRoute = viewModel.getRouteIDQueried() + val resetSpinner = (oldRoute != null) && (oldRoute.trim() != selRoute.gtfsId.trim()) + Log.d(DEBUG_TAG, "Selected route: ${selRoute.gtfsId}, reset spinner: $resetSpinner") + //launch query for this gtfsID viewModel.setRouteIDQuery(selRoute.gtfsId) + //reset spinner position + if(resetSpinner) patternsSpinner.setSelection(0) } override fun onNothingSelected(p0: AdapterView<*>?) { } } patternsSpinner.onItemSelectedListener = object : OnItemSelectedListener{ override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { val patternWithStops = currentPatterns.get(position) - + // setPatternAndReqStops(patternWithStops) } override fun onNothingSelected(p0: AdapterView<*>?) { } } } return rootView } override fun onAttach(context: Context) { super.onAttach(context) if(context is CommonFragmentListener) mListener = context else throw RuntimeException(context.toString() + " must implement CommonFragmentListener") } override fun onResume() { super.onResume() mListener?.readyGUIfor(FragmentKind.LINES) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel = ViewModelProvider(this).get(LinesViewModel::class.java) //val lines = viewModel.(); viewModel.routesGTTLiveData.observe(this) { setRoutes(it) } viewModel.patternsWithStopsByRouteLiveData.observe(this){ patterns -> run { - currentPatterns = patterns.sortedBy { p->"${p.pattern.directionId} - ${p.pattern.headsign}" } + currentPatterns = patterns.sortedBy { p-> p.pattern.code } + //patterns. //sortedBy {-1*it.stopsIndices.size}// "${p.pattern.directionId} - ${p.pattern.headsign}" } patternsAdapter?.let { it.clear() it.addAll(currentPatterns.map { p->"${p.pattern.directionId} - ${p.pattern.headsign}" }) it.notifyDataSetChanged() } val pos = patternsSpinner.selectedItemPosition - if(pos!= INVALID_POSITION){ + //might be possible that the selectedItem is different (larger than list size) + if(pos!= INVALID_POSITION && pos >= 0 && (pos < currentPatterns.size)){ setPatternAndReqStops(currentPatterns[pos]) } } } viewModel.stopsForPatternLiveData.observe(this){stops-> Log.d("BusTO-LinesFragment", "Setting stops from DB") setCurrentStops(stops) } } override fun getBaseViewForSnackBar(): View? { return null } private fun setRoutes(routes: List){ currentRoutes = routes.sortedWith(linesComparator) linesAdapter?.clear() linesAdapter?.addAll(currentRoutes.map { r -> r.shortName }) linesAdapter?.notifyDataSetChanged() } private fun setCurrentStops(stops: List){ val orderBy = currentPatternStops.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}") var setNewAdapter = true; if(stopsRecyclerView.adapter is StopRecyclerAdapter){ val adapter = stopsRecyclerView.adapter as StopRecyclerAdapter if(adapter.stops.size == stopsSorted.size && (adapter.stops.get(0).gtfsID == stopsSorted.get(0).gtfsID) && (adapter.stops.get(numStops-1).gtfsID == stopsSorted.get(numStops-1).gtfsID) ){ Log.d(DEBUG_TAG, "Found same stops on recyclerview") setNewAdapter = false } /*else { Log.d(DEBUG_TAG, "Found adapter on recyclerview, but not the same stops") adapter.stops = stopsSorted adapter.notifyDataSetChanged() }*/ } if(setNewAdapter){ stopsRecyclerView.adapter = StopRecyclerAdapter( stopsSorted, adapterListener, StopRecyclerAdapter.Use.LINES, NameCapitalize.FIRST ) } } private fun setPatternAndReqStops(patternWithStops: MatoPatternWithStops){ Log.d(DEBUG_TAG, "Requesting stops for pattern ${patternWithStops.pattern.code}") currentPatternStops = patternWithStops.stopsIndices.sortedBy { i-> i.order } viewModel.requestStopsForPatternWithStops(patternWithStops) } override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { super.onCreateContextMenu(menu, v, menuInfo) Log.d("BusTO-LinesFragment", "Creating context menu ") if (v.id == R.id.patternStopsRecyclerView) { // if we aren't attached to activity, return null if (activity == null) return val inflater = activity!!.menuInflater inflater.inflate(R.menu.menu_line_item, menu) } } override fun onContextItemSelected(item: MenuItem): Boolean { - val info = item.getMenuInfo(); if (stopsRecyclerView.getAdapter() !is StopRecyclerAdapter) return false val adapter =stopsRecyclerView.adapter as StopRecyclerAdapter val stop = adapter.stops.get(adapter.getPosition()) val acId = item.itemId if(acId == R.id.action_view_on_map){ // view on the map if ((stop.latitude == null) or (stop.longitude == null) or (mListener == null) ) { Toast.makeText(context, R.string.cannot_show_on_map_no_position, Toast.LENGTH_SHORT).show() return true } mListener!!.showMapCenteredOnStop(stop) return true } else if (acId == R.id.action_show_arrivals){ mListener?.requestArrivalsForStopID(stop.ID); return true } return false } } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt index 0b88b2d..2ec7a5e 100644 --- a/src/it/reyboz/bustorino/fragments/LinesViewModel.kt +++ b/src/it/reyboz/bustorino/fragments/LinesViewModel.kt @@ -1,77 +1,82 @@ package it.reyboz.bustorino.fragments import android.app.Application import android.util.Log import androidx.lifecycle.* import it.reyboz.bustorino.backend.Stop import it.reyboz.bustorino.data.GtfsRepository import it.reyboz.bustorino.data.NextGenDB import it.reyboz.bustorino.data.OldDataRepository import it.reyboz.bustorino.data.gtfs.GtfsDatabase import it.reyboz.bustorino.data.gtfs.GtfsRoute import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops import java.util.concurrent.Executors class LinesViewModel(application: Application) : AndroidViewModel(application) { private val gtfsRepo: GtfsRepository private val oldRepo: OldDataRepository //val patternsByRouteLiveData: LiveData> private val routeIDToSearch = MutableLiveData() val stopsForPatternLiveData = MutableLiveData>() val executor = Executors.newFixedThreadPool(2) init { val gtfsDao = GtfsDatabase.getGtfsDatabase(application).gtfsDao() gtfsRepo = GtfsRepository(gtfsDao) oldRepo = OldDataRepository(executor, NextGenDB.getInstance(application)) } val routesGTTLiveData: LiveData> by lazy{ gtfsRepo.getLinesLiveDataForFeed("gtt") } val patternsWithStopsByRouteLiveData = routeIDToSearch.switchMap { gtfsRepo.getPatternsWithStopsForRouteID(it) } val routesName: LiveData> = Transformations.map(routesGTTLiveData) { it.map { route -> route.longName } } fun setRouteIDQuery(routeID: String){ routeIDToSearch.value = routeID } + fun getRouteIDQueried(): String?{ + return routeIDToSearch.value + } + var shouldShowMessage = true; + fun requestStopsForGTFSIDs(gtfsIDs: List){ oldRepo.requestStopsWithGtfsIDs(gtfsIDs) { if (it.isSuccess) { stopsForPatternLiveData.postValue(it.result) } else { Log.e("BusTO-LinesVM", "Got error on callback with stops for gtfsID") it.exception?.printStackTrace() } } } fun requestStopsForPatternWithStops(patternStops: MatoPatternWithStops){ val gtfsIDs = ArrayList() for(pat in patternStops.stopsIndices){ gtfsIDs.add(pat.stopGtfsId) } requestStopsForGTFSIDs(gtfsIDs) } /*fun getLinesGTT(): MutableLiveData> { val routesData = MutableLiveData>() viewModelScope.launch { val routes=gtfsRepo.getLinesForFeed("gtt") routesData.postValue(routes) } return routesData }*/ } \ No newline at end of file diff --git a/src/it/reyboz/bustorino/util/LinesNameSorter.java b/src/it/reyboz/bustorino/util/LinesNameSorter.java index 929129f..7a3a900 100644 --- a/src/it/reyboz/bustorino/util/LinesNameSorter.java +++ b/src/it/reyboz/bustorino/util/LinesNameSorter.java @@ -1,115 +1,192 @@ /* BusTO (util) Copyright (C) 2019 Fabio Mazza This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package it.reyboz.bustorino.util; +import android.util.Log; +import androidx.annotation.NonNull; + import java.util.Comparator; public class LinesNameSorter implements Comparator { + final static private int cc = 100; + final static private int ERROR_PARSING = -10; + final static private int FIRST_1_LETTER = 120; + final static private int FIRST_2_LETTERS=220; @Override public int compare(String name1, String name2) { name1 = name1.trim(); name2 = name2.trim(); /* if(name1.length()>name2.length()) return 1; if(name1.length()==name2.length()) { try{ int num1 = Integer.parseInt(name1.trim()); int num2 = Integer.parseInt(name2.trim()); return num1-num2; } catch (NumberFormatException ex){ //Log.d("BUSTO Compare lines","Cannot compare lines "+name1+" and "+name2); //return name1.compareTo(name2); //One of them is not a line String trim1 = name1.substring(0, name1.length() - 1).trim(); String trim2 = name2.substring(0, name2.length()-1).trim(); if(isInteger(trim1)){ //cut away the last part //this means it's a line return compare(trim1, name2); } else if(isInteger(trim2)){ return compare(name1,trim2); } return name1.compareTo(name2); } - }**/ + }* //One of them is not int num1 = -1; if(isInteger(name1)) num1 = Integer.parseInt(name1); int num2 = -1; if (isInteger(name2)) num2 = Integer.parseInt(name2); if(num1 >= 0 && num2 >=0){ //we're very happy - return (num1-num2)*10; + return (num1-num2)*cc; } else if (num1>=0) { //name2 is not fully integer + if(name2.contains(" ")){ + final String[] allStr = name2.split(" "); + if(isInteger(allStr[0])) { + return (num1-Integer.parseInt(allStr[0]))*cc - incrementFromLastChar(allStr[1].trim().charAt(0)); + } + //sennò si fa come sotto + } final String name2sub = name2.substring(0, name2.length()-1).trim(); char lastchar = name2.charAt(name2.length()-1); if(isInteger(name2sub)){ num2 = Integer.parseInt(name2sub); - int diff = (num1-num2)*10; + int diff = (num1-num2)*cc; return diff - incrementFromLastChar(lastchar); } else{ //failed return name1.compareTo(name2); } } else if (num2>=0) { //name1 is not fully integer final String name1sub = name1.substring(0, name1.length()-1).trim(); char lastchar = name1.charAt(name1.length()-1); if (isInteger(name1sub)){ num1 = Integer.parseInt(name1sub); - int diff = (num1-num2)*10; + int diff = (num1-num2)*cc; return diff + incrementFromLastChar(lastchar); } else { return name1.compareTo(name2); } } //last case return name1.compareTo(name2); + **/ + //DO ALL CASES + final CompareHolder c1 = getValueOfComplexName(name1); + final CompareHolder c2 = getValueOfComplexName(name2); + if (c1.value != ERROR_PARSING && c2.value != ERROR_PARSING){ + + return (c1.value-c2.value)*100+c1.extra.compareTo(c2.extra); + } else { + if(c2.value== ERROR_PARSING && c1.value==ERROR_PARSING){ + return c1.extra.compareTo(c2.extra); + } + else if(c1.value == ERROR_PARSING){ + return 1; + } + else { + return -1; + } + //Log.e("BusTo-Parsing","Error with the string"); + //throw new IllegalArgumentException("Error with the string name parsing"); + } } + private static CompareHolder getValueOfComplexName(String name){ + String namec = name.trim(); + + if (isInteger(namec)) return new CompareHolder(Integer.parseInt(namec),""); + + //check for the first part + if(namec.contains(" ")){ + final String[] allStr = namec.split(" "); + if(isInteger(allStr[0])) { + int g = Integer.parseInt(allStr[0]); + return new CompareHolder(g, allStr[1]); + } + else return new CompareHolder(-7, namec); + }else { + final String name1sub = namec.substring(0, namec.length()-1).trim(); + String lastPart = namec.substring(namec.length()-1); + int g; + if (isInteger(name1sub)) { + return new CompareHolder(Integer.parseInt(name1sub), lastPart); + } else if(name1sub.equals("M1")){ + return new CompareHolder(-1, lastPart); + } else { + //check NightBuster (X+name) + if(isInteger(namec.substring(1))){ + g = Integer.parseInt((namec.substring(1))); + return new CompareHolder(FIRST_1_LETTER, namec); + } else if (isInteger(namec.substring(2))) { + return new CompareHolder(FIRST_2_LETTERS, namec); + } + return new CompareHolder(ERROR_PARSING, namec); + } + } + } + + private static class CompareHolder { + int value; + String extra; + + public CompareHolder(int value,@NonNull String extra) { + this.value = value; + this.extra = extra; + } + } public static boolean isInteger(String strNum) { if (strNum == null) { return false; } try { int d = Integer.parseInt(strNum); } catch (NumberFormatException nfe) { return false; } return true; } private static int incrementFromLastChar(char lastchar){ switch (lastchar){ case 'B': case 'b': case '/': return 1; case 'n': case 'N': return 3; default: - return 6; + return 1; } } } diff --git a/src/it/reyboz/bustorino/util/PatternWithStopsSorter.kt b/src/it/reyboz/bustorino/util/PatternWithStopsSorter.kt new file mode 100644 index 0000000..756b28d --- /dev/null +++ b/src/it/reyboz/bustorino/util/PatternWithStopsSorter.kt @@ -0,0 +1,21 @@ +package it.reyboz.bustorino.util + +import it.reyboz.bustorino.data.gtfs.MatoPatternWithStops + +/** + * Sorter for the patterns, which takes into account direction and length of pattern + */ +class PatternWithStopsSorter: Comparator { + override fun compare(p0: MatoPatternWithStops?, p1: MatoPatternWithStops?): Int { + if (p0 != null && p1!=null) { + if(p0.pattern.directionId != p1.pattern.directionId){ + return p0.pattern.directionId - p1.pattern.directionId + } + val g = -1*(p0.stopsIndices.size - p1.stopsIndices.size) + if(g!=0) + return g; + else return p0.pattern.code.compareTo(p1.pattern.code) + } + else return 0; + } +} \ No newline at end of file