/* * Copyright 2008 Google Inc. * * 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 com.google.android.apps.mytracks.util; import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.maps.GeoPoint; import android.app.Activity; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; import android.location.Location; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.util.Log; import java.util.ArrayList; import java.util.List; import java.util.Stack; /** * Utility class for decimating tracks at a given level of precision. * * @author Leif Hendrik Wilden */ public class MyTracksUtils { private static final int RELEASE_SIGNATURE_HASHCODE = -1855564782; /** * Computes the distance on the two sphere between the point c0 and the line * segment c1 to c2. * * @param c0 the first coordinate * @param c1 the beginning of the line segment * @param c2 the end of the lone segment * @return the distance in m (assuming spherical earth) */ public static double distance( final Location c0, final Location c1, final Location c2) { if (c1.equals(c2)) { return c2.distanceTo(c0); } final double s0lat = c0.getLatitude() * UnitConversions.TO_RADIANS; final double s0lng = c0.getLongitude() * UnitConversions.TO_RADIANS; final double s1lat = c1.getLatitude() * UnitConversions.TO_RADIANS; final double s1lng = c1.getLongitude() * UnitConversions.TO_RADIANS; final double s2lat = c2.getLatitude() * UnitConversions.TO_RADIANS; final double s2lng = c2.getLongitude() * UnitConversions.TO_RADIANS; double s2s1lat = s2lat - s1lat; double s2s1lng = s2lng - s1lng; final double u = ((s0lat - s1lat) * s2s1lat + (s0lng - s1lng) * s2s1lng) / (s2s1lat * s2s1lat + s2s1lng * s2s1lng); if (u <= 0) { return c0.distanceTo(c1); } if (u >= 1) { return c0.distanceTo(c2); } Location sa = new Location(""); sa.setLatitude(c0.getLatitude() - c1.getLatitude()); sa.setLongitude(c0.getLongitude() - c1.getLongitude()); Location sb = new Location(""); sb.setLatitude(u * (c2.getLatitude() - c1.getLatitude())); sb.setLongitude(u * (c2.getLongitude() - c1.getLongitude())); return sa.distanceTo(sb); } /** * Decimates the given locations for a given zoom level. This uses a * Douglas-Peucker decimation algorithm. * * @param tolerance in meters * @param locations input * @param decimated output */ public static void decimate(double tolerance, ArrayList locations, ArrayList decimated) { final int n = locations.size(); if (n < 1) { return; } int idx; int maxIdx = 0; Stack stack = new Stack(); double[] dists = new double[n]; dists[0] = 1; dists[n - 1] = 1; double maxDist; double dist = 0.0; int[] current; if (n > 2) { int[] stackVal = new int[] {0, (n - 1)}; stack.push(stackVal); while (stack.size() > 0) { current = stack.pop(); maxDist = 0; for (idx = current[0] + 1; idx < current[1]; ++idx) { dist = MyTracksUtils.distance( locations.get(idx), locations.get(current[0]), locations.get(current[1])); if (dist > maxDist) { maxDist = dist; maxIdx = idx; } } if (maxDist > tolerance) { dists[maxIdx] = maxDist; int[] stackValCurMax = {current[0], maxIdx}; stack.push(stackValCurMax); int[] stackValMaxCur = {maxIdx, current[1]}; stack.push(stackValMaxCur); } } } int i = 0; idx = 0; decimated.clear(); for (Location l : locations) { if (dists[idx] != 0) { decimated.add(l); i++; } idx++; } Log.d(MyTracksConstants.TAG, "Decimating " + n + " points to " + i + " w/ tolerance = " + tolerance); } /** * Decimates the given track for the given precision. * * @param track a track * @param precision desired precision in meters */ public static void decimate(Track track, double precision) { ArrayList decimated = new ArrayList(); decimate(precision, track.getLocations(), decimated); track.setLocations(decimated); } /** * Limits number of points by dropping any points beyond the given number of * points. Note: That'll actually discard points. * * @param track a track * @param numberOfPoints maximum number of points */ public static void cut(Track track, int numberOfPoints) { ArrayList locations = track.getLocations(); while (locations.size() > numberOfPoints) { locations.remove(locations.size() - 1); } } /** * Splits a track in multiple tracks where each piece has less or equal than * maxPoints. * * @param track the track to split * @param maxPoints maximum number of points for each piece * @return a list of one or more track pieces */ public static ArrayList split(Track track, int maxPoints) { ArrayList result = new ArrayList(); final int nTotal = track.getLocations().size(); int n = 0; Track piece = null; do { piece = new Track(); TripStatistics pieceStats = piece.getStatistics(); piece.setId(track.getId()); piece.setName(track.getName()); piece.setDescription(track.getDescription()); piece.setCategory(track.getCategory()); List pieceLocations = piece.getLocations(); for (int i = n; i < nTotal && pieceLocations.size() < maxPoints; i++) { piece.addLocation(track.getLocations().get(i)); } int nPointsPiece = pieceLocations.size(); if (nPointsPiece >= 2) { pieceStats.setStartTime(pieceLocations.get(0).getTime()); pieceStats.setStopTime(pieceLocations.get(nPointsPiece - 1).getTime()); result.add(piece); } n += (pieceLocations.size() - 1); } while (n < nTotal && piece != null && piece.getLocations().size() > 1); return result; } /** * Test if a given GeoPoint is valid, i.e. within physical bounds. * * @param geoPoint the point to be tested * @return true, if it is a physical location on earth. */ public static boolean isValidGeoPoint(GeoPoint geoPoint) { return Math.abs(geoPoint.getLatitudeE6()) < 90E6 && Math.abs(geoPoint.getLongitudeE6()) <= 180E6; } /** * Checks if a given location is a valid (i.e. physically possible) location * on Earth. Note: The special separator locations (which have latitude = * 100) will not qualify as valid. Neither will locations with lat=0 and lng=0 * as these are most likely "bad" measurements which often cause trouble. * * @param location the location to test * @return true if the location is a valid location. */ public static boolean isValidLocation(Location location) { return location != null && Math.abs(location.getLatitude()) <= 90 && Math.abs(location.getLongitude()) <= 180; } /** * Gets a location from a GeoPoint. * * @param p a GeoPoint * @return the corresponding location */ public static Location getLocation(GeoPoint p) { Location result = new Location(""); result.setLatitude(p.getLatitudeE6() / 1.0E6); result.setLongitude(p.getLongitudeE6() / 1.0E6); return result; } public static GeoPoint getGeoPoint(Location location) { return new GeoPoint((int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6)); } /** * Returns whether or not this is a release build. */ public static boolean isRelease(Context context) { PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES); if (packageInfo == null) { return false; } for (Signature sig : packageInfo.signatures) { if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) { return true; } } return false; } /** * Get the My Tracks version from the manifest. * * @return the version, or an empty string in case of failure. */ public static String getMyTracksVersion(Context context) { PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_META_DATA); return packageInfo == null ? "" : packageInfo.versionName; } /** * Return the My Tracks version code from the manifest. * * @return the version code, or an empty string in case of failure. */ public static int getMyTracksVersionCode(Context context) { PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_META_DATA); return packageInfo == null ? -1 : packageInfo.versionCode; } private static PackageInfo getPackageInfo(Context context, int flags) { if (context == null) { Log.w(MyTracksConstants.TAG, "No context found when attempting to get PackageInfo"); return null; } try { return context.getPackageManager().getPackageInfo( "com.google.android.maps.mytracks", flags); } catch (NameNotFoundException e) { Log.w(MyTracksConstants.TAG, "Failed to get version info.", e); return null; } } /** * Tries to acquire a partial wake lock if not already acquired. Logs errors * and gives up trying in case the wake lock cannot be acquired. */ public static WakeLock acquireWakeLock(Activity activity, WakeLock wakeLock) { Log.i(MyTracksConstants.TAG, "MyTracksUtils: Acquiring wake lock."); try { PowerManager pm = (PowerManager) activity .getSystemService(Context.POWER_SERVICE); if (pm == null) { Log.e(MyTracksConstants.TAG, "MyTracksUtils: Power manager not found!"); return wakeLock; } if (wakeLock == null) { wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, MyTracksConstants.TAG); if (wakeLock == null) { Log.e(MyTracksConstants.TAG, "MyTracksUtils: Could not create wake lock (null)."); } return wakeLock; } if (!wakeLock.isHeld()) { wakeLock.acquire(); if (!wakeLock.isHeld()) { Log.e(MyTracksConstants.TAG, "MyTracksUtils: Could not acquire wake lock."); } } } catch (RuntimeException e) { Log.e(MyTracksConstants.TAG, "MyTracksUtils: Caught unexpected exception: " + e.getMessage(), e); } return wakeLock; } /** * This is a utility class w/ only static members. */ protected MyTracksUtils() { } }