/**
Copyright 2015 Tim Engler, Rareventure LLC
This file is part of Tiny Travel Tracker.
Tiny Travel Tracker 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.
Tiny Travel Tracker 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 Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>.
*/
package com.rareventure.gps2.reviewer.map;
import java.io.File;
import java.util.ArrayList;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.PointF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import com.mapzen.tangram.LngLat;
import com.mapzen.tangram.MapController;
import com.mapzen.tangram.MapView;
import com.mapzen.tangram.TouchInput;
import com.rareventure.gps2.R;
import com.rareventure.android.SuperThread;
import com.rareventure.android.Util;
import com.rareventure.android.AndroidPreferenceSet.AndroidPreferences;
import com.rareventure.gps2.GTG;
import com.rareventure.gps2.database.cache.AreaPanel;
import com.rareventure.gps2.database.cache.AreaPanelSpaceTimeBox;
public class OsmMapView extends MapView
{
private static final float ZOOM_STEP = 1.5f;
private static final int ZOOM_EASE_MS = 500;
private static final int PAN_EASE_MS = 500;
private static final int AUTOZOOM_PAN_EASE_MS = 1000;
private static final int AUTOZOOM_ZOOM_EASE_MS = 1000;
private ArrayList<GpsOverlay> overlays = new ArrayList<GpsOverlay>();
/**
* Coordinates of the screen in longitude and latitude. This is the most accurate representation
* of where the screen is (we get these values as is from mapzen).
* The y component of screenBottomRight is based on pointAreaHeight, *NOT* the height of the map.
*
* Writing and reading of these values are synchronized.
*/
private LngLat screenTopLeft = new LngLat(), screenBottomRight = new LngLat(), screenSize = new LngLat();
/**
* These are the screen coordinates in ap units (based on Mercator). See {@code AreaPanel} for more info.
* The apMaxY is based on pointAreaHeight, *NOT* the height of the map.
* <p>
* Writing and reading of these values are synchronized.
*/
private int apMinX, apMinY, apMaxX, apMaxY;
public static Preferences prefs = new Preferences();
private Paint tickPaint;
private MapScaleWidget scaleWidget;
private OsmMapGpsTrailerReviewerMapActivity activity;
private MapController mapController;
private Handler notifyScreenChangeHandler = new Handler();
/**
* Center of screen in pixels
*/
int centerX;
int centerY;
/**
* This is the height of the area in which we draw points. We don't want to draw points
* underneath the time scale widget at the bottom of the screen, so this excludes that
*/
int pointAreaHeight;
int windowWidth;
/**
* Since mapzen doesn't tell us when the screen moves, and stops moving (after a fling
* for example), we continuously pull the location. We only do so when an action occurs which
* would start the screen in motion, and when the screen has stopped, we turn off our
* polling.
*/
private Runnable notifyScreenChangeRunnable = new Runnable() {
LngLat lastP1 = new LngLat(), lastP2 = new LngLat();
PointF p = new PointF();
@Override
public void run() {
// p.x = 0;
// p.y = 0;
// LngLat p1 = mapController.screenPositionToLngLat(p);
// p.x = windowWidth;
// p.y = pointAreaHeight;
// LngLat p2 = mapController.screenPositionToLngLat(p);
//we normalize because mapcontroller lovingly returns values outside of -180/180 longitude
//if user wraps world while scrolling
LngLat p1 = Util.normalizeLngLat(mapController.screenPositionToLngLat(new PointF(0,0)));
LngLat p2 = Util.normalizeLngLat(mapController.screenPositionToLngLat(new PointF(windowWidth, pointAreaHeight)));
synchronized (this) {
//update our internal representation of the screen
double lngSize = p2.longitude - p1.longitude;
//if we are crossing the -180/+180 border
if(lngSize < 0)
lngSize = 360 + lngSize;
screenSize = new LngLat(lngSize, p1.latitude - p2.latitude);
screenTopLeft = p1;
screenBottomRight = p2;
apMinX = AreaPanel.convertLonToX(screenTopLeft.longitude);
apMinY = AreaPanel.convertLatToY(screenTopLeft.latitude);
apMaxX = AreaPanel.convertLonToX(screenBottomRight.longitude);
apMaxY = AreaPanel.convertLatToY(screenBottomRight.latitude);
}
updateScaleWidget();
notifyOverlayScreenChanged();
//if we haven't moved since our last run
//note that we place this check after we notify, so that if we are queued,
//we'll always redraw once no matter what. (used by redrawMap())
if(p1.equals(lastP1) && p2.equals(lastP2))
return;
lastP1 = p1;
lastP2 = p2;
//we keep queuing as long as there is a change
//we need to time them out, as to not waste resources, hence we use a handler and delay
//the next call
notifyScreenChangeHandler.postDelayed(
notifyScreenChangeHandlerRunnable
, 250);
}
};
//used to space out our checks for the map position
private Runnable notifyScreenChangeHandlerRunnable =
new Runnable() {
@Override
public void run() {
mapController.queueEvent(notifyScreenChangeRunnable);
}
};
private int windowHeight;
// private MultiTouchController<OsmMapView> multiTouchController = new MultiTouchController<OsmMapView>(this);
public OsmMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
/**
* Must be called after all addOverlay() calls
*/
public void init(final SuperThread fileCacheSuperThread, final OsmMapGpsTrailerReviewerMapActivity activity)
{
this.activity = activity;
// This starts a background process to set up the map.
getMapAsync(new MapView.OnMapReadyCallback(){
@Override
public void onMapReady(final MapController mapController) {
OsmMapView.this.mapController = mapController;
//delete the old cache if it exists
//TODO 3: eventually remove this
File oldCache = new File(GTG.getExternalStorageDirectory().toString()+"/tile_cache");
if(oldCache.exists())
Util.deleteRecursive(new File(GTG.getExternalStorageDirectory().toString()+"/tile_cache"));
File cacheDir = new File(GTG.getExternalStorageDirectory().toString()+"/tile_cache2");
cacheDir.mkdirs();
// Log.d(GTG.TAG, "cacheDir is "+cacheDir);
// GpsTrailerMapzenHttpHandler mapHandler =
// new GpsTrailerMapzenHttpHandler(cacheDir, fileCacheSuperThread);
//
// mapController.setHttpHandler(mapHandler);
mapController.setShoveResponder(new TouchInput.ShoveResponder() {
@Override
public boolean onShove(float distance) {
//this rotates the screen downwards for more 3d look. We don't allow it currently
//because it would mess up our calculations as to what points to
//display
//TODO 3 allow shoving
return true;
}
});
mapController.setRotateResponder(new TouchInput.RotateResponder() {
@Override
public boolean onRotate(float x, float y, float rotation) {
//this rotates the screen to change the northern direction. We don't allow it currently
//because it would mess up our calculations as to what points to
//display
//TODO 3 allow rotation
return true;
}
});
mapController.setPanResponder(new TouchInput.PanResponder() {
@Override
public boolean onPan(float startX, float startY, float endX, float endY) {
// if(duringLongPress)
// {
// sasRectangleManager.updateRectangleEndPoint(endX, endY);
// }
Log.d(GTG.TAG,String.format("panning sx %f sy %f ex %f ey %f",startX, startY,
endX, endY));
mapController.queueEvent(notifyScreenChangeRunnable);
return false;
}
@Override
public boolean onFling(float posX, float posY, float velocityX, float velocityY) {
Log.d(GTG.TAG,String.format("flinging px %f py %f vx %f vy %f",
posX, posY, velocityX, velocityY));
mapController.queueEvent(notifyScreenChangeRunnable);
return false;
}
});
mapController.setScaleResponder(new TouchInput.ScaleResponder() {
@Override
public boolean onScale(float x, float y, float scale, float velocity) {
Log.d(GTG.TAG,String.format("scaling x %f y %f sx %f sy %f",
x, y, scale, velocity));
mapController.queueEvent(notifyScreenChangeRunnable);
return false;
}
});
mapController.setTapResponder(new TouchInput.TapResponder() {
@Override
public boolean onSingleTapUp(float x, float y) {
return false;
}
@Override
public boolean onSingleTapConfirmed(float x, float y) {
for(GpsOverlay overlay : overlays)
{
overlay.onTap(x,y);
}
return false;
}
});
mapController.setLongPressResponder(new TouchInput.LongPressResponder() {
public float startX;
public float startY;
public void onLongPress(float x, float y) {
// Get instance of Vibrator from current Context
Vibrator v = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
// Vibrate for a short time
v.vibrate(50);
startX = x;
startY = y;
}
@Override
public void onLongPressUp(float x, float y) {
for(GpsOverlay overlay : overlays)
{
overlay.onLongPressEnd(startX, startY, x,y);
}
}
@Override
public boolean onLongPressPan(float movementStartX, float movementStartY, float endX, float endY) {
for(GpsOverlay overlay : overlays)
{
overlay.onLongPressMove(startX, startY, endX,endY);
}
return false;
}
});
for(GpsOverlay o : overlays)
o.startTask(mapController);
panAndZoom2(OsmMapGpsTrailerReviewerMapActivity.prefs.lastLon,
OsmMapGpsTrailerReviewerMapActivity.prefs.lastLat,
OsmMapGpsTrailerReviewerMapActivity.prefs.lastZoom);
}
},"map_style.yaml");
}
/**
* Returns the ratio of meters to pixels at the center of the screen
*/
public double metersToPixels() {
//we need the lat, because the distance changes depending on location from equator
double screenCenterLat = screenTopLeft.latitude - screenSize.latitude / 2;
double metersToLon = 1/(Util.LON_TO_METERS_AT_EQUATOR *
Math.cos(screenCenterLat/ 180 * Math.PI));
return screenSize.longitude / windowWidth * metersToLon;
}
public synchronized AreaPanelSpaceTimeBox getCoordinatesRectangleForScreen() {
AreaPanelSpaceTimeBox stBox = new AreaPanelSpaceTimeBox();
stBox.minX = apMinX;
stBox.maxX = apMaxX;
stBox.minY = apMinY;
stBox.maxY = apMaxY;
return stBox;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
GTG.cacheCreatorLock.registerReadingThread();
try {
super.onLayout(changed, left, top, right, bottom);
updateScaleWidget();
}
finally {
GTG.cacheCreatorLock.unregisterReadingThread();
}
}
public void addOverlay(GpsOverlay overlay) {
this.overlays.add(overlay);
}
private void updateScaleWidget() {
if(scaleWidget != null)
scaleWidget.change((float) (1./metersToPixels()));
}
public void zoomIn() {
float newZoom = mapController.getZoom() + ZOOM_STEP;
mapController.setZoomEased(newZoom,ZOOM_EASE_MS);
notifyScreenMoved();
}
private void notifyScreenMoved() {
//this makes our code that checks for a screen change
//we put a delay in there because we often do animated changes,
//and if we run our checker too soon, it will compare the last
//screen change to the current and determine that we've stopped,
//when actually we haven't started moving yet
notifyScreenChangeHandler.postDelayed(
notifyScreenChangeHandlerRunnable
, ZOOM_EASE_MS/2);
}
public void zoomOut() {
float newZoom = mapController.getZoom() - ZOOM_STEP;
mapController.setZoomEased(newZoom,ZOOM_EASE_MS);
notifyScreenMoved();
}
/**
* Redraws the map for a change of points displayed or screen
*/
public void redrawMap() {
if(mapController != null)
mapController.queueEvent(notifyScreenChangeRunnable);
}
public LngLat getScreenTopLeft() {
return screenTopLeft;
}
public LngLat getScreenBottomRight() {
return screenBottomRight;
}
public MapController getMapController() {
return mapController;
}
public static class Preferences implements AndroidPreferences
{
}
public void setScaleWidget(MapScaleWidget scaleWidget) {
this.scaleWidget = scaleWidget;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
GTG.cacheCreatorLock.registerReadingThread();
try {
tickPaint = new Paint();
tickPaint.setColor(0xFF000000);
}
finally {
GTG.cacheCreatorLock.unregisterReadingThread();
}
}
public void notifyNewBitmapInCache() {
if(getHandler() != null)
{
getHandler().post(new Runnable() {
@Override
public void run() {
invalidate();
}
});
}
}
/**
* Pans and zooms so the given points will show up as the top left and
* bottom right of the view. Note that the zooming/panning will be done so
* that the given bottom will be placed above the time view and the zoom buttons.
*/
public void panAndZoom(int minX, int minY, int maxX, int maxY) {
float currZoom = mapController.getZoom();
LngLat tl = Util.normalizeLngLat(mapController.screenPositionToLngLat(new PointF(0,0)));
LngLat br = Util.normalizeLngLat(mapController.screenPositionToLngLat(new PointF(windowWidth,windowHeight)));
int fromMinX= AreaPanel.convertLonToX(tl.longitude);
int fromMinY = AreaPanel.convertLatToY(tl.latitude);
int fromMaxX = AreaPanel.convertLonToX(br.longitude);
int fromMaxY = AreaPanel.convertLatToY(br.latitude);
//panAndZoom uses the center of the visible area, excluding the time view
//and buttons. However, mapzen, uses the entire window. So we need to adjust the
//y size to the whole window so mapzen will zoom and pan correctly
maxY = (int) (((float)maxY - minY) * windowHeight / pointAreaHeight) + minY;
float zoomMultiplier = Math.min(
((float)fromMaxX-fromMinX)/(maxX-minX),
((float)fromMaxY-fromMinY)/(maxY-minY)
);
//mapzen uses 2**(zoom) for zoom level, so we have to convert to it
float newZoom = (float) (currZoom + Math.log(zoomMultiplier)/Math.log(2));
LngLat newPos = new LngLat(
AreaPanel.convertXToLon((maxX-minX)/2+minX),
AreaPanel.convertYToLat((maxY-minY)/2+minY)
);
mapController.setPositionEased(newPos,AUTOZOOM_PAN_EASE_MS);
mapController.setZoomEased(newZoom,AUTOZOOM_ZOOM_EASE_MS);
notifyScreenMoved();
}
public void panAndZoom2(double lon, double lat, float zoom) {
if(mapController == null)
return;
LngLat newPos = new LngLat(lon, lat);
mapController.setPositionEased(newPos,AUTOZOOM_PAN_EASE_MS);
mapController.setZoomEased(zoom,AUTOZOOM_ZOOM_EASE_MS);
notifyScreenMoved();
}
public void panTo(LngLat loc) {
mapController.setPositionEased(loc,AUTOZOOM_PAN_EASE_MS);
}
private void notifyOverlayScreenChanged() {
AreaPanelSpaceTimeBox newStBox = getCoordinatesRectangleForScreen();
//we access the min and max time from the activity which is altered by the main ui thread
newStBox.minZ = OsmMapGpsTrailerReviewerMapActivity.prefs.currTimePosSec;
newStBox.maxZ = OsmMapGpsTrailerReviewerMapActivity.prefs.currTimePosSec +
OsmMapGpsTrailerReviewerMapActivity.prefs.currTimePeriodSec;
for(GpsOverlay o : overlays)
o.notifyScreenChanged(newStBox);
}
public void onPause() {
super.onPause();
for(GpsOverlay o : overlays)
o.onPause();
}
public void onResume() {
super.onResume();
for(GpsOverlay o : overlays)
o.onResume();
}
/**
* Set location of crosshairs and where zooms are centered. ie, this is the
* center of the screen in pixels.
*
* @param x
* @param y
*/
public void setZoomCenter(int x, int y) {
centerX = x;
centerY = y;
//TODO 2 FIXME
// activity.gpsTrailerOverlay.setZoomCenter(x,y);
}
// @Override
// public OsmMapView getDraggableObjectAtPoint(PointInfo touchPoint) {
// return this;
// }
//
// @Override
// public boolean pointInObjectGrabArea(PointInfo touchPoint, OsmMapView obj) {
// return false;
// }
//
// @Override
// public void getPositionAndScale(OsmMapView obj,
// PositionAndScale objPosAndScaleOut) {
// objPosAndScaleOut.set(-(float)x, -(float)y, true, zoom8bitPrec,
// false, 1,1,false,0);
//
// }
//
// @Override
// public boolean setPositionAndScale(OsmMapView obj,
// PositionAndScale newObjPosAndScale, PointInfo touchPoint) {
// long newZoom8BitPrec = (int) newObjPosAndScale.getScale();
//
// if(newZoom8BitPrec != zoom8bitPrec)
// {
// if(newZoom8BitPrec < OsmMapGpsTrailerReviewerMapActivity.prefs.maxZoom &&
// newZoom8BitPrec > OsmMapGpsTrailerReviewerMapActivity.prefs.minZoom)
// {
// x = (-newObjPosAndScale.getXOff() + centerX) *(newZoom8BitPrec)/(zoom8bitPrec) - centerX;
// y = (-newObjPosAndScale.getYOff() + centerY) *(newZoom8BitPrec)/(zoom8bitPrec) - centerY;
//
// zoom8bitPrec = newZoom8BitPrec;
//
//// Log.d("GTG", "xxxxxxx = "
//// + newZoom8BitPrec + " , "
//// + zoom8bitPrec + " : "
//// + newObjPosAndScale.getXOff() + " - " + newObjPosAndScale.getYOff());
//
// }
//
// activity.toolTip.setAction(UserAction.MAP_VIEW_PINCH_ZOOM);
// }
// else
// {
// x = -newObjPosAndScale.getXOff();
// y = -newObjPosAndScale.getYOff();
//
// activity.toolTip.setAction(UserAction.MAP_VIEW_MOVE);
// }
//
// invalidate();
//
// updateScaleWidget();
// activity.updatePlusMinusButtonsForNewZoom();
// return false;
// }
//
// @Override
// public void selectObject(OsmMapView obj, PointInfo touchPoint) {
//
// }
public void initAfterLayout() {
windowWidth = getWidth();
this.pointAreaHeight = activity.findViewById(R.id.main_window_area).getBottom();
windowHeight = getHeight();
// memoryCache.setWidthAndHeight(getWidth(), getHeight());
}
}