/*
* Copyright (c) 2013, Will Szumski
* Copyright (c) 2013, Doug Szumski
*
* This file is part of Cyclismo.
*
* Cyclismo 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.
*
* Cyclismo 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 Cyclismo. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* 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 org.cowboycoders.cyclismo;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.graphics.drawable.Drawable;
import android.location.Location;
import android.util.Log;
import org.cowboycoders.cyclismo.maps.AugmentedPolyline;
import org.mapsforge.core.model.BoundingBox;
import org.mapsforge.core.model.Dimension;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.core.model.MapPosition;
import org.mapsforge.core.util.LatLongUtils;
import org.mapsforge.map.layer.Layer;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.android.view.MapView;
import org.cowboycoders.cyclismo.content.Waypoint;
import org.cowboycoders.cyclismo.maps.TrackPath;
import org.cowboycoders.cyclismo.maps.TrackPathFactory;
import org.cowboycoders.cyclismo.util.LocationUtils;
import org.cowboycoders.cyclismo.util.PreferencesUtils;
import org.cowboycoders.cyclismo.util.UnitConversions;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* A map overlay that displays my location arrow, error circle, and track info.
*
* @author Leif Hendrik Wilden
*/
public class MapOverlay implements MarkerSource {
public static final String TAG = MapOverlay.class.getSimpleName();
public static final float WAYPOINT_X_ANCHOR = 13f / 48f;
private static final float WAYPOINT_Y_ANCHOR = 43f / 48f;
private static final float MARKER_X_ANCHOR = 50f / 96f;
private static final float MARKER_Y_ANCHOR = 90f / 96f;
private static final int INITIAL_LOCATIONS_SIZE = 1024;
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.track_color_mode_key))) {
trackColorMode = PreferencesUtils.getString(
context, R.string.track_color_mode_key, PreferencesUtils.TRACK_COLOR_MODE_DEFAULT);
trackPath = TrackPathFactory.getTrackPath(context, trackColorMode);
}
}
};
private final Context context;
private final List<CachedLocation> locations;
private final BlockingQueue<CachedLocation> pendingLocations;
private final List<Waypoint> waypoints;
private final MapMarkerUpdater mapMarkerUpdater = new MapMarkerUpdater(this);
@Override
public float getWaypointXAnchor() {
return WAYPOINT_X_ANCHOR;
}
@Override
public float getWaypointYAnchor() {
return WAYPOINT_Y_ANCHOR;
}
@Override
public float getMarkerXAnchor() {
return MARKER_X_ANCHOR;
}
@Override
public float getMarkerYAnchor() {
return MARKER_Y_ANCHOR;
}
@Override
public Drawable getWaypoint(Waypoint waypoint) {
int drawableId = waypoint.getType() == Waypoint.TYPE_STATISTICS ? R.drawable.yellow_pushpin
: R.drawable.blue_pushpin;
return context.getResources().getDrawable(drawableId);
}
@Override
public Drawable getStopMarker() {
return getContext().getResources().getDrawable(R.drawable.red_dot);
}
@Override
public Drawable getStartMarker() {
return getContext().getResources().getDrawable(R.drawable.green_dot);
}
/**
* @return the pendingLocations
*/
protected BlockingQueue<CachedLocation> getPendingLocations() {
return pendingLocations;
}
/**
* @return the locations
*/
public List<CachedLocation> getLocations() {
return locations;
}
/**
* @return the waypoints
*/
public List<Waypoint> getWaypoints() {
return waypoints;
}
private String trackColorMode = PreferencesUtils.TRACK_COLOR_MODE_DEFAULT;
private boolean showEndMarker = true;
private TrackPath trackPath;
private StaticOverlay underlay;
/**
* @return the context
*/
public Context getContext() {
return context;
}
/**
* @return the trackPath
*/
public TrackPath getTrackPath() {
return trackPath;
}
/**
* A pre-processed {@link Location} to speed up drawing.
*
* @author Jimmy Shih
*/
public static class CachedLocation {
private final boolean valid;
private final LatLong latLng;
private final int speed;
/**
* Constructor for an invalid cached location.
*/
public CachedLocation() {
this.valid = false;
this.latLng = null;
this.speed = -1;
}
/**
* Constructor for a potentially valid cached location.
*/
public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location);
this.latLng = valid ? new LatLong(location.getLatitude(), location.getLongitude()) : null;
this.speed = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH);
}
/**
* Returns true if the location is valid.
*/
public boolean isValid() {
return valid;
}
/**
* Gets the speed in kilometers per hour.
*/
public int getSpeed() {
return speed;
}
/**
* Gets the LatLong.
*/
public LatLong getLatLong() {
return latLng;
}
};
public MapOverlay(Context context) {
this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.locations = new ArrayList<CachedLocation>(INITIAL_LOCATIONS_SIZE);
this.pendingLocations = new ArrayBlockingQueue<CachedLocation>(
Constants.MAX_DISPLAYED_TRACK_POINTS, true);
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
/**
* Add a location.
*
* @param location the location
*/
public void addLocation(Location location) {
// Queue up in the pendingLocations until it's merged with locations
if (!pendingLocations.offer(new CachedLocation(location))) {
Log.e(TAG, "Unable to add to pendingLocations.");
}
}
/**
* @return the showEndMarker
*/
@Override
public boolean isShowEndMarker() {
return showEndMarker;
}
/**
* Adds a segment split.
*/
public void addSegmentSplit() {
// Queue up in the pendingLocations until it's merged with locations
if (!pendingLocations.offer(new CachedLocation())) {
Log.e(TAG, "Unable to add to pendingLocations.");
}
}
/**
* Clears the locations.
*/
public void clearPoints() {
synchronized (locations) {
locations.clear();
pendingLocations.clear();
}
}
/**
* Adds a waypoint.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
/**
* Clears the waypoints.
*/
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
/**
* Sets whether to show the end marker.
*
* @param show true to show the end marker
*/
public void setShowEndMarker(boolean show) {
showEndMarker = show;
}
/**
* Updates the track, start and end markers, and waypoints.
*
* @param googleMap the google map
* @param paths the paths
* @param reload true to reload all points
*/
public void update(MapView googleMap, ArrayList<AugmentedPolyline> paths, boolean reload) {
synchronized (locations) {
// Merge pendingLocations with locations
@SuppressWarnings("hiding")
TrackPath trackPath = getTrackPath();
int newLocations = pendingLocations.drainTo(locations);
boolean needReload = reload || trackPath.updateState();
if (needReload) {
Layers layers = googleMap.getLayerManager().getLayers();
//leave map and current location marker: index 0, 1
for (int i= 2; i< layers.size() ; i++){
Layer layer = layers.remove(i);
layer.onDestroy();
}
paths.clear();
if (underlay != null) {
updateUnderlay(googleMap);
}
trackPath.updatePath(googleMap, paths, 0, locations);
// we will already have start/stop markers from underlay
if (underlay == null) {
updateStartAndEndMarkers(googleMap);
}
updateWaypoints(googleMap);
} else {
if (newLocations != 0) {
int numLocations = locations.size();
trackPath.updatePath(googleMap, paths, numLocations - newLocations, locations);
}
}
Layer startMarker = mapMarkerUpdater.getStartMarker();
Layer endMarker = mapMarkerUpdater.getEndMarker();
if (startMarker != null && endMarker != null) {
double minLat;
double maxLat;
double minLong;
double maxLong;
if (startMarker.getPosition().latitude < endMarker.getPosition().latitude) {
minLat = startMarker.getPosition().latitude;
maxLat = endMarker.getPosition().latitude;
} else {
minLat = endMarker.getPosition().latitude;
maxLat = startMarker.getPosition().latitude;
}
if (startMarker.getPosition().longitude < endMarker.getPosition().longitude) {
minLong = startMarker.getPosition().longitude;
maxLong = endMarker.getPosition().longitude;
} else {
minLong = endMarker.getPosition().longitude;
maxLong = startMarker.getPosition().longitude;
}
BoundingBox bb = new BoundingBox(minLat,minLong, maxLat, maxLong);
Dimension dimension = googleMap.getModel().mapViewDimension.getDimension();
googleMap.getModel().mapViewPosition.setMapPosition(new MapPosition(
bb.getCenterPoint(),
LatLongUtils.zoomForBounds(
dimension,
bb,
googleMap.getModel().displayModel.getTileSize())));
}
}
}
private void updateUnderlay(MapView googleMap) {
Log.d(TAG,"updating underlay");
try {
underlay.update(googleMap);
} catch (IllegalStateException e) {
Log.d(TAG,"Illegal state exception whilst updating underlay polyline");
}
}
/**
* Updates the start and end markers.
*
* @param googleMap the google map
*/
protected void updateStartAndEndMarkers(MapView googleMap) {
// Add the end marker
// Add the start marker
mapMarkerUpdater.updateStartAndEndMarkers(googleMap);
}
/**
* Updates the waypoints.
*
* @param googleMap the google map.
*/
protected void updateWaypoints(MapView googleMap) {
mapMarkerUpdater.updateWaypoints(googleMap);
}
public void addUnderlay(StaticOverlay underlay) {
if (underlay !=null) {
Log.d(TAG, "adding underlay");
}
this.underlay = underlay;
}
public void destroy() {
Log.d(TAG, "map overlay destroyed");
if (underlay != null) underlay.destroy();
mapMarkerUpdater.destroy();
}
}