diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7cd7009..8cf2fbd 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,107 +1,124 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java b/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java index e5bc001..913941e 100644 --- a/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java +++ b/src/it/reyboz/bustorino/backend/FiveTStopsFetcher.java @@ -1,119 +1,119 @@ /* 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 org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; /** * Once was asynchronous BusStop[] fetcher from a query, code mostly taken from * AsyncWgetBusStopSuggestions (by Valerio Bozzolan) * * @see FiveTScraperFetcher */ public class FiveTStopsFetcher implements StopsFinderByName { @Override public List FindByName(String name, AtomicReference res) { // API apparently limited to 20 results ArrayList busStops = new ArrayList<>(20); String stopID; String stopName; String stopLocation; //Stop busStop; - if(name.length() < 3) { + if(name.length() < 2) { //some stops are shorter than 3 chars.. "PO" is an example res.set(Result.QUERY_TOO_SHORT); return busStops; } String responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas; URL u; try { u = new URL("http://www.5t.torino.it/5t/trasporto/stop-lookup.jsp?action=search&stopShortName=" + URLEncoder.encode(name, "utf-8")); } catch(Exception e) { res.set(Result.PARSER_ERROR); return busStops; } responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas = networkTools.getDOM(u, res); if (responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas == null) { // result already set in getDOM() return busStops; } Document doc = Jsoup.parse(responseInDOMFormatBecause5THaveAbsolutelyNoIdeaWhatJSONWas); // Find bus stops Elements lis = doc.getElementsByTag("li"); for(Element li : lis) { Elements spans = li.getElementsByTag("span"); // busStopID try { stopID = FiveTNormalizer.FiveTNormalizeRoute(spans.eq(0).text()); } catch(Exception e) { //Log.e("Suggestions", "Empty busStopID"); stopID = ""; } // busStopName try { stopName = spans.eq(1).text(); } catch(Exception e) { //Log.e("Suggestions", "Empty busStopName"); stopName = ""; } // busStopLocation try { stopLocation = (spans.eq(2).text()); } catch(Exception e) { //Log.e("Suggestions", "Empty busStopLocation"); stopLocation = null; } /* if(stopLocation == null || stopLocation.length() == 0) { stopLocation = db.getLocationFromID(stopID); }*/ busStops.add(new Stop(stopName, stopID, stopLocation, null, null)); } if(busStops.size() == 0) { res.set(Result.EMPTY_RESULT_SET); } else { res.set(Result.OK); } Collections.sort(busStops); // TODO: remove duplicates? (see GTTStopsFetcher) return busStops; } } diff --git a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java index 1b2df98..29dfc5f 100644 --- a/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java +++ b/src/it/reyboz/bustorino/backend/GTTStopsFetcher.java @@ -1,191 +1,191 @@ /* 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 androidx.annotation.NonNull; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicReference; public class GTTStopsFetcher implements StopsFinderByName { @Override @NonNull public List FindByName(String name, AtomicReference res) { URL url; // sorting an ArrayList should be faster than a LinkedList and the API is limited to 15 results List s = new ArrayList<>(15); List s2 = new ArrayList<>(15); String fullname; String content; String bacino; String localita; Route.Type type; JSONArray json; int howManyStops, i; JSONObject thisstop; - if(name.length() < 3) { + if(name.length() < 2) { res.set(Result.QUERY_TOO_SHORT); return s; } try { url = new URL("https://www.gtt.to.it/cms/index.php?option=com_gtt&view=palinejson&term=" + URLEncoder.encode(name, "utf-8")); } catch (Exception e) { res.set(Result.PARSER_ERROR); return s; } content = networkTools.queryURL(url, res); if(content == null) { return s; } try { json = new JSONArray(content); } catch(JSONException e) { if(content.contains("[]")) { // when no results are found, server returns a PHP Warning and an empty array. In case they fix the warning, we're looking for the array. res.set(Result.EMPTY_RESULT_SET); } else { res.set(Result.PARSER_ERROR); } return s; } howManyStops = json.length(); if(howManyStops == 0) { res.set(Result.EMPTY_RESULT_SET); return s; } try { for(i = 0; i < howManyStops; i++) { thisstop = json.getJSONObject(i); fullname = thisstop.getString("data"); String ID = thisstop.getString("value"); try { localita = thisstop.getString("localita"); if(localita.equals("[MISSING]")) { localita = null; } } catch(JSONException e) { localita = null; } /* if(localita == null || localita.length() == 0) { localita = db.getLocationFromID(ID); } //TODO: find località by ContentProvider */ try { bacino = thisstop.getString("bacino"); } catch (JSONException ignored) { bacino = "U"; } if(fullname.startsWith("Metro ")) { type = Route.Type.METRO; } else if(fullname.length() >= 6 && fullname.startsWith("S00")) { type = Route.Type.RAILWAY; } else if(fullname.startsWith("ST")) { type = Route.Type.RAILWAY; } else { type = FiveTNormalizer.decodeType("", bacino); } //TODO: refactor using content provider s.add(new Stop(fullname, ID, localita, type,null)); } } catch (JSONException e) { res.set(Result.PARSER_ERROR); return s; } if(s.size() < 1) { // shouldn't happen but prevents the next part from catching fire res.set(Result.EMPTY_RESULT_SET); return s; } Collections.sort(s); // the next loop won't work with less than 2 items if(s.size() < 2) { res.set(Result.OK); return s; } /* There are some duplicate stops returned by this API. * Long distance buses have stop IDs with 5 digits. Always. They are zero-padded if there * aren't enough. E.g. stop 631 becomes 00631. * * Unfortunately you can't use padded stops to query any API. * Fortunately, unpadded stops return both normal and long distance bus timetables. * FiveTNormalizer is already removing padding (there may be some padded stops for which the * API doesn't return an unpadded equivalent), here we'll remove duplicates by skipping * padded stops, which also never have a location. * * I had to draw a finite state machine on a piece of paper to understand how to implement * this loop. */ for(i = 1; i < howManyStops; ) { Stop current = s.get(i); Stop previous = s.get(i-1); // same stop: let's see which one to keep... if(current.ID.equals(previous.ID)) { if(previous.location == null) { // previous one is useless: discard it, increment i++; } else if(current.location == null) { // this one is useless: add previous and skip one s2.add(previous); i += 2; } else { // they aren't really identical: to err on the side of caution, keep them both. s2.add(previous); i++; } } else { // different: add previous, increment s2.add(previous); i++; } } // unless the last one was garbage (i would be howManyStops+1 in that case), add it if(i == howManyStops) { s2.add(s.get(i-1)); } res.set(Result.OK); return s2; } } diff --git a/src/it/reyboz/bustorino/backend/utils.java b/src/it/reyboz/bustorino/backend/utils.java index 2d615b9..06b17a0 100644 --- a/src/it/reyboz/bustorino/backend/utils.java +++ b/src/it/reyboz/bustorino/backend/utils.java @@ -1,183 +1,185 @@ 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 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; } public static String toTitleCase(String givenString, boolean lowercaseRest) { String[] arr = givenString.split(" "); StringBuilder sb = new StringBuilder(); //Log.d("BusTO chars", "String parsing: "+givenString+" in array: "+ Arrays.toString(arr)); for (int i = 0; i < arr.length; i++) { if (arr[i].length() > 1) { sb.append(Character.toUpperCase(arr[i].charAt(0))); if (lowercaseRest) sb.append(arr[i].substring(1).toLowerCase(Locale.ROOT)); else sb.append(arr[i].substring(1)); sb.append(" "); } else sb.append(arr[i]); } return sb.toString().trim(); } /** * 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