/* * Copyright (C) 2014 University of South Florida (sjbarbeau@gmail.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onebusaway.android.util; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.LocationServices; import org.onebusaway.android.R; import org.onebusaway.android.app.Application; import org.onebusaway.android.directions.util.CustomAddress; import org.onebusaway.android.io.elements.ObaRegion; import android.content.Context; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.os.Build; import android.os.Bundle; import android.os.SystemClock; import android.provider.Settings; import android.text.TextUtils; import android.util.Log; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.concurrent.TimeUnit; /** * Utilities to help obtain and process location data * * @author barbeau */ public class LocationUtils { public static final String TAG = "LocationUtil"; public static final int DEFAULT_SEARCH_RADIUS = 40000; private static final float FUZZY_EQUALS_THRESHOLD = 15.0f; public static final float ACC_THRESHOLD = 50f; // 50 meters public static final long TIME_THRESHOLD = TimeUnit.MINUTES.toMillis(10); // 10 minutes private static final int GEOCODER_MAX_RESULTS = 5; //in meters private static final int GEOCODING_MAX_ERROR = 100; public static Location getDefaultSearchCenter() { ObaRegion region = Application.get().getCurrentRegion(); if (region != null) { double results[] = new double[4]; RegionUtils.getRegionSpan(region, results); return LocationUtils.makeLocation(results[2], results[3]); } else { return null; } } /** * Compares Location A to Location B - prefers a non-null location that is more recent. Does * NOT take estimated accuracy into account. * @param a first location to compare * @param b second location to compare * @return true if Location a is "better" than b, or false if b is "better" than a */ public static boolean compareLocationsByTime(Location a, Location b) { return (a != null && (b == null || a.getTime() > b.getTime())); } /** * Compares Location A to Location B, considering timestamps and accuracy of locations. * Typically * this is used to compare a new location delivered by a LocationListener (Location A) to * a previously saved location (Location B). * * @param a location to compare * @param b location to compare against * @return true if Location a is "better" than b, or false if b is "better" than a */ public static boolean compareLocations(Location a, Location b) { if (a == null) { // New location isn't valid, return false return false; } // If the new location is the first location, save it if (b == null) { return true; } // If the last location is older than TIME_THRESHOLD minutes, and the new location is more recent, // save the new location, even if the accuracy for new location is worse if (System.currentTimeMillis() - b.getTime() > TIME_THRESHOLD && compareLocationsByTime(a, b)) { return true; } // If the new location has an accuracy better than ACC_THRESHOLD and is newer than the last location, save it if (a.getAccuracy() < ACC_THRESHOLD && compareLocationsByTime(a, b)) { return true; } // If we get this far, A isn't better than B return false; } /** * Converts a latitude/longitude to a Location. * * @param lat The latitude. * @param lon The longitude. * @return A Location representing this latitude/longitude. */ public static Location makeLocation(double lat, double lon) { Location l = new Location(""); l.setLatitude(lat); l.setLongitude(lon); return l; } /** * Returns true if the locations are approximately equal (i.e., within a certain distance * threshold) * * @param a first location * @param b second location * @return true if the locations are approximately equal, false if they are not */ public static boolean fuzzyEquals(Location a, Location b) { return a.distanceTo(b) <= FUZZY_EQUALS_THRESHOLD; } /** * Returns true if the user has enabled location services on their device, false if they have * not * * from http://stackoverflow.com/a/22980843/937715 * * @return true if the user has enabled location services on their device, false if they have * not */ public static boolean isLocationEnabled(Context context) { int locationMode = Settings.Secure.LOCATION_MODE_OFF; String locationProviders; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { locationMode = Settings.Secure .getInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE); } catch (Settings.SettingNotFoundException e) { e.printStackTrace(); return false; } return locationMode != Settings.Secure.LOCATION_MODE_OFF; } else { locationProviders = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.LOCATION_PROVIDERS_ALLOWED); return !TextUtils.isEmpty(locationProviders); } } /** * Returns the human-readable details of a Location (provider, lat/long, accuracy, timestamp) * * @return the details of a Location (provider, lat/long, accuracy, timestamp) in a string */ public static String printLocationDetails(Location loc) { if (loc == null) { return ""; } long timeDiff; double timeDiffSec; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { timeDiff = SystemClock.elapsedRealtimeNanos() - loc.getElapsedRealtimeNanos(); // Convert to seconds timeDiffSec = timeDiff / 1E9; } else { timeDiff = System.currentTimeMillis() - loc.getTime(); timeDiffSec = timeDiff / 1E3; } StringBuilder sb = new StringBuilder(); sb.append(loc.getProvider()); sb.append(' '); sb.append(loc.getLatitude()); sb.append(','); sb.append(loc.getLongitude()); if (loc.hasAccuracy()) { sb.append(' '); sb.append(loc.getAccuracy()); } sb.append(", "); sb.append(String.format("%.0f", timeDiffSec) + " second(s) ago"); return sb.toString(); } /** * Returns a new GoogleApiClient which includes LocationServicesCallbacks */ public static GoogleApiClient getGoogleApiClientWithCallbacks(Context context) { LocationServicesCallback locCallback = new LocationServicesCallback(); return new GoogleApiClient.Builder(context) .addApi(LocationServices.API) .addConnectionCallbacks(locCallback) .addOnConnectionFailedListener(locCallback) .build(); } /** * Class to handle Google Play Location Services callbacks */ public static class LocationServicesCallback implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private static final String TAG = "LocationServicesCallbck"; @Override public void onConnected(Bundle bundle) { Log.d(TAG, "GoogleApiClient.onConnected"); } @Override public void onConnectionSuspended(int i) { Log.d(TAG, "GoogleApiClient.onConnectionSuspended"); } @Override public void onConnectionFailed(ConnectionResult connectionResult) { Log.d(TAG, "GoogleApiClient.onConnectionFailed"); } } public static List<CustomAddress> processGeocoding(Context context, ObaRegion region, String... reqs) { return processGeocoding(context, region, false, reqs); } public static List<CustomAddress> processGeocoding(Context context, ObaRegion region, boolean geocodingForMarker, String... reqs) { ArrayList<CustomAddress> addressesReturn = new ArrayList<CustomAddress>(); String address = reqs[0]; if (address == null || address.equalsIgnoreCase("")) { return null; } double latitude = 0, longitude = 0; boolean latLngSet = false; try { if (reqs.length >= 3) { latitude = Double.parseDouble(reqs[1]); longitude = Double.parseDouble(reqs[2]); latLngSet = true; } } catch (Exception e) { Log.e(TAG, "Geocoding without reference latitude/longitude"); } if (address.equalsIgnoreCase(context.getString(R.string.tripplanner_current_location))) { if (latLngSet) { CustomAddress addressReturn = new CustomAddress(context.getResources().getConfiguration().locale); addressReturn.setLatitude(latitude); addressReturn.setLongitude(longitude); addressReturn.setAddressLine(addressReturn.getMaxAddressLineIndex() + 1, context.getString(R.string.tripplanner_current_location)); addressesReturn.add(addressReturn); return addressesReturn; } return null; } List<CustomAddress> addresses = new ArrayList<>(); // Originally checks app preferences. Could add this as a preference. Geocoder gc = new Geocoder(context); try { List<Address> androidTypeAddresses; if (region != null) { double[] regionSpan = new double[4]; RegionUtils.getRegionSpan(region, regionSpan); double minLat = regionSpan[2] - (regionSpan[0] / 2); double minLon = regionSpan[3] - (regionSpan[1] / 2); double maxLat = regionSpan[2] + (regionSpan[0] / 2); double maxLon = regionSpan[3] + (regionSpan[1] / 2); androidTypeAddresses = gc.getFromLocationName(address, GEOCODER_MAX_RESULTS, minLat, minLon, maxLat, maxLon); } else { androidTypeAddresses = gc.getFromLocationName(address, GEOCODER_MAX_RESULTS); } for (Address androidTypeAddress : androidTypeAddresses) { addresses.add(new CustomAddress(androidTypeAddress)); } } catch (IOException e) { e.printStackTrace(); } addresses = filterAddressesBBox(region, addresses); boolean resultsCloseEnough = true; if (geocodingForMarker && latLngSet) { float results[] = new float[1]; resultsCloseEnough = false; for (CustomAddress addressToCheck : addresses) { Location.distanceBetween(latitude, longitude, addressToCheck.getLatitude(), addressToCheck.getLongitude(), results); if (results[0] < GEOCODING_MAX_ERROR) { resultsCloseEnough = true; break; } } } if ((addresses == null) || addresses.isEmpty() || !resultsCloseEnough) { if (addresses == null) { addresses = new ArrayList<CustomAddress>(); } Log.e(TAG, "Geocoder did not find enough addresses: " + addresses); } addresses = filterAddressesBBox(region, addresses); if (geocodingForMarker && latLngSet && addresses != null && !addresses.isEmpty()) { float results[] = new float[1]; float minDistanceToOriginalLatLon = Float.MAX_VALUE; CustomAddress closestAddress = addresses.get(0); for (CustomAddress addressToCheck : addresses) { Location.distanceBetween(latitude, longitude, addressToCheck.getLatitude(), addressToCheck.getLongitude(), results); if (results[0] < minDistanceToOriginalLatLon) { closestAddress = addressToCheck; minDistanceToOriginalLatLon = results[0]; } } addressesReturn.add(closestAddress); } else { addressesReturn.addAll(addresses); } return addressesReturn; } /** * Filters the addresses obtained in geocoding process, removing the * results outside server limits. * * @param addresses list of addresses to filter * @return a new list filtered */ private static List<CustomAddress> filterAddressesBBox(ObaRegion region, List<CustomAddress> addresses) { if ((!(addresses == null || addresses.isEmpty())) && region != null) { for (Iterator<CustomAddress> it = addresses.iterator(); it.hasNext(); ) { CustomAddress address = it.next(); Location loc = new Location(""); loc.setLatitude(address.getLatitude()); loc.setLongitude(address.getLongitude()); if (!RegionUtils.isLocationWithinRegion(loc, region)) { it.remove(); } } } return addresses; } }