package de.westnordost.streetcomplete.tangram;
import android.app.Activity;
import android.app.Fragment;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.SensorManager;
import android.location.Location;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v13.app.FragmentCompat;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.mapzen.android.lost.api.LocationListener;
import com.mapzen.android.lost.api.LocationRequest;
import com.mapzen.android.lost.api.LocationServices;
import com.mapzen.android.lost.api.LostApiClient;
import com.mapzen.tangram.HttpHandler;
import com.mapzen.tangram.LngLat;
import com.mapzen.tangram.MapController;
import com.mapzen.tangram.MapView;
import com.mapzen.tangram.Marker;
import com.mapzen.tangram.TouchInput;
import java.io.File;
import de.westnordost.osmapi.map.data.LatLon;
import de.westnordost.streetcomplete.Prefs;
import de.westnordost.streetcomplete.R;
import de.westnordost.streetcomplete.util.SphericalEarthMath;
import static android.content.Context.SENSOR_SERVICE;
public class MapFragment extends Fragment implements
FragmentCompat.OnRequestPermissionsResultCallback, LocationListener,
LostApiClient.ConnectionCallbacks, TouchInput.ScaleResponder,
TouchInput.ShoveResponder, TouchInput.RotateResponder,
TouchInput.PanResponder, TouchInput.DoubleTapResponder, CompassComponent.Listener
{
private CompassComponent compass = new CompassComponent();
private Marker locationMarker;
private Marker accuracyMarker;
private Marker directionMarker;
private String[] directionMarkerSize;
private MapView mapView;
private HttpHandler httpHandler;
/** controller to the asynchronously loaded map. Since it is loaded asynchronously, could be
* null still at any point! */
protected MapController controller;
private LostApiClient lostApiClient;
private boolean isFollowingPosition;
private Location lastLocation;
private boolean zoomedYet;
private Listener listener;
private String apiKey;
public interface Listener
{
void onMapReady();
void onUnglueViewFromPosition();
}
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.fragment_map, container, false);
mapView = (MapView) view.findViewById(R.id.map);
TextView mapzenLink = (TextView) view.findViewById(R.id.mapzenLink);
mapzenLink.setText(Html.fromHtml(
String.format(getResources().getString(R.string.map_attribution_mapzen),
"<a href=\"https://mapzen.com/\">Mapzen</a>"))
);
mapzenLink.setMovementMethod(LinkMovementMethod.getInstance());
return view;
}
/* --------------------------------- Map and Location --------------------------------------- */
public void getMapAsync(String apiKey)
{
getMapAsync(apiKey, "scene.yaml");
}
public void getMapAsync(String apiKey, @NonNull final String sceneFilePath)
{
this.apiKey = apiKey;
mapView.getMapAsync(new MapView.OnMapReadyCallback()
{
@Override public void onMapReady(MapController ctrl)
{
controller = ctrl;
initMap();
}
}, sceneFilePath);
}
protected void initMap()
{
updateMapTileCacheSize();
controller.setHttpHandler(httpHandler);
restoreCameraState();
controller.setRotateResponder(this);
controller.setShoveResponder(this);
controller.setScaleResponder(this);
controller.setPanResponder(this);
controller.setDoubleTapResponder(this);
locationMarker = controller.addMarker();
BitmapDrawable dot = createBitmapDrawableFrom(R.drawable.location_dot);
locationMarker.setStylingFromString("{ style: 'points', color: 'white', size: ["+TextUtils.join(",",sizeInDp(dot))+"], order: 2000, flat: true, collide: false }");
locationMarker.setDrawable(dot);
locationMarker.setDrawOrder(3);
directionMarker = controller.addMarker();
BitmapDrawable directionImg = createBitmapDrawableFrom(R.drawable.location_direction);
directionMarkerSize = sizeInDp(directionImg);
directionMarker.setDrawable(directionImg);
directionMarker.setDrawOrder(2);
accuracyMarker = controller.addMarker();
accuracyMarker.setDrawable(createBitmapDrawableFrom(R.drawable.accuracy_circle));
accuracyMarker.setDrawOrder(1);
compass.setListener(this);
showLocation();
followPosition();
updateView();
listener.onMapReady();
}
private String[] sizeInDp(Drawable drawable)
{
DisplayMetrics metrics = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(metrics);
float d = metrics.density;
return new String[]{
drawable.getIntrinsicWidth() / d + "px",
drawable.getIntrinsicHeight() / d + "px"};
}
private BitmapDrawable createBitmapDrawableFrom(int resId)
{
Drawable drawable = getResources().getDrawable(resId);
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return new BitmapDrawable(getResources(), bitmap);
}
private void updateMapTileCacheSize()
{
httpHandler = createHttpHandler();
}
private HttpHandler createHttpHandler()
{
int cacheSize = PreferenceManager.getDefaultSharedPreferences(getActivity()).getInt(Prefs.MAP_TILECACHE, 50);
File cacheDir = getActivity().getExternalCacheDir();
if (cacheDir != null && cacheDir.exists())
{
return new TileHttpHandler(apiKey, new File(cacheDir, "tile_cache"), cacheSize * 1024 * 1024);
}
return new TileHttpHandler(apiKey);
}
public void startPositionTracking()
{
if(!lostApiClient.isConnected()) lostApiClient.connect();
}
public void stopPositionTracking()
{
if(locationMarker != null)
{
locationMarker.setVisible(false);
accuracyMarker.setVisible(false);
directionMarker.setVisible(false);
}
zoomedYet = false;
try // TODO remove when https://github.com/mapzen/lost/issues/178 is solved
{
if(lostApiClient.isConnected())
{
LocationServices.FusedLocationApi.removeLocationUpdates(lostApiClient, this);
lostApiClient.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void setIsFollowingPosition(boolean value)
{
isFollowingPosition = value;
if(!isFollowingPosition) {
zoomedYet = false;
}
followPosition();
}
public boolean isFollowingPosition()
{
return isFollowingPosition;
}
private void followPosition()
{
if(isFollowingPosition && controller != null && lastLocation != null)
{
controller.setPositionEased(new LngLat(lastLocation.getLongitude(), lastLocation.getLatitude()),1000);
if(!zoomedYet)
{
zoomedYet = true;
controller.setZoomEased(19, 1000);
}
updateView();
}
}
/* -------------------------------- Touch responders --------------------------------------- */
@Override public boolean onDoubleTap(float x, float y)
{
unglueViewFromPosition();
LngLat zoomTo = controller.screenPositionToLngLat(new PointF(x, y));
controller.setPositionEased(zoomTo, 500);
controller.setZoomEased(controller.getZoom() + 1.5f, 500);
updateView();
return true;
}
@Override public boolean onScale(float x, float y, float scale, float velocity)
{
unglueViewFromPosition();
updateView();
return false;
}
@Override public boolean onPan(float startX, float startY, float endX, float endY)
{
unglueViewFromPosition();
updateView();
return false;
}
@Override public boolean onFling(float posX, float posY, float velocityX, float velocityY)
{
unglueViewFromPosition();
updateView();
return false;
}
@Override public boolean onShove(float distance)
{
updateView();
return false;
}
@Override public boolean onRotate(float x, float y, float rotation)
{
updateView();
return false;
}
protected void updateView()
{
updateAccuracy();
}
private void unglueViewFromPosition()
{
if(isFollowingPosition())
{
setIsFollowingPosition(false);
listener.onUnglueViewFromPosition();
}
}
/* ------------------------------------ LOST ------------------------------------------- */
@Override public void onLocationChanged(Location location)
{
lastLocation = location;
showLocation();
followPosition();
}
private void showLocation()
{
if(accuracyMarker != null && locationMarker != null && directionMarker != null && lastLocation != null)
{
LngLat pos = new LngLat(lastLocation.getLongitude(), lastLocation.getLatitude());
locationMarker.setVisible(true);
accuracyMarker.setVisible(true);
directionMarker.setVisible(true);
locationMarker.setPointEased(pos, 1000, MapController.EaseType.CUBIC);
accuracyMarker.setPointEased(pos, 1000, MapController.EaseType.CUBIC);
directionMarker.setPointEased(pos, 1000, MapController.EaseType.CUBIC);
updateAccuracy();
}
}
private void updateAccuracy()
{
if(accuracyMarker != null && lastLocation != null && accuracyMarker.isVisible())
{
LngLat pos = new LngLat(lastLocation.getLongitude(), lastLocation.getLatitude());
float size = meters2Pixels(pos, lastLocation.getAccuracy());
accuracyMarker.setStylingFromString("{ style: 'points', color: 'white', size: ["+size+"px, "+size+"px], order: 2000, flat: true, collide: false }");
}
}
@Override public void onRotationChanged(float rotation)
{
if(directionMarker != null && directionMarker.isVisible())
{
double r = rotation * 180 / Math.PI;
directionMarker.setStylingFromString(
"{ style: 'points', color: '#cc536dfe', size: [" +
TextUtils.join(",",directionMarkerSize) +
"], order: 2000, collide: false, flat: true, angle: " + r + " }");
}
}
private float meters2Pixels(LngLat at, float meters) {
LatLon pos0 = TangramConst.toLatLon(at);
LatLon pos1 = SphericalEarthMath.translate(pos0, meters, 0);
PointF screenPos0 = controller.lngLatToScreenPosition(at);
PointF screenPos1 = controller.lngLatToScreenPosition(TangramConst.toLngLat(pos1));
return Math.abs(screenPos1.y - screenPos0.y);
}
@Override public void onProviderEnabled(String provider)
{
}
@Override public void onProviderDisabled(String provider)
{
}
private static final String PREF_ROTATION = "map_rotation";
private static final String PREF_TILT = "map_tilt";
private static final String PREF_ZOOM = "map_zoom";
private static final String PREF_LAT = "map_lat";
private static final String PREF_LON = "map_lon";
private void restoreCameraState()
{
SharedPreferences prefs = getActivity().getPreferences(Activity.MODE_PRIVATE);
if(prefs.contains(PREF_ROTATION)) controller.setRotation(prefs.getFloat(PREF_ROTATION,0));
if(prefs.contains(PREF_TILT)) controller.setTilt(prefs.getFloat(PREF_TILT,0));
if(prefs.contains(PREF_ZOOM)) controller.setZoom(prefs.getFloat(PREF_ZOOM,0));
if(prefs.contains(PREF_LAT) && prefs.contains(PREF_LON))
{
LngLat pos = new LngLat(
Double.longBitsToDouble(prefs.getLong(PREF_LON,0)),
Double.longBitsToDouble(prefs.getLong(PREF_LAT,0))
);
controller.setPosition(pos);
}
}
private void saveCameraState()
{
if(controller == null) return;
SharedPreferences.Editor editor = getActivity().getPreferences(Activity.MODE_PRIVATE).edit();
editor.putFloat(PREF_ROTATION, controller.getRotation());
editor.putFloat(PREF_TILT, controller.getTilt());
editor.putFloat(PREF_ZOOM, controller.getZoom());
LngLat pos = controller.getPosition();
editor.putLong(PREF_LAT, Double.doubleToRawLongBits(pos.latitude));
editor.putLong(PREF_LON, Double.doubleToRawLongBits(pos.longitude));
editor.apply();
}
/* ------------------------------------ Lifecycle ------------------------------------------- */
@Override public void onCreate(@Nullable Bundle bundle)
{
super.onCreate(bundle);
compass.onCreate((SensorManager) getActivity().getSystemService(SENSOR_SERVICE));
if(mapView != null) mapView.onCreate(bundle);
}
@Override public void onAttach(Activity activity)
{
super.onAttach(activity);
listener = (Listener) activity;
lostApiClient = new LostApiClient.Builder(activity).addConnectionCallbacks(this).build();
}
@Override public void onStart()
{
super.onStart();
updateMapTileCacheSize();
}
@Override public void onResume()
{
super.onResume();
compass.onResume();
if(mapView != null) mapView.onResume();
}
@Override public void onPause()
{
super.onPause();
compass.onPause();
if(mapView != null) mapView.onPause();
saveCameraState();
}
@Override public void onStop()
{
super.onStop();
stopPositionTracking();
}
@Override public void onConnected() throws SecurityException
{
zoomedYet = false;
lastLocation = null;
LocationRequest request = LocationRequest.create()
.setInterval(2000)
.setSmallestDisplacement(5)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
LocationServices.FusedLocationApi.requestLocationUpdates(lostApiClient, request, this);
}
@Override public void onConnectionSuspended()
{
}
@Override public void onDestroy()
{
super.onDestroy();
compass.setListener(null);
if(mapView != null) mapView.onDestroy();
controller = null;
directionMarker = null;
accuracyMarker = null;
locationMarker = null;
}
@Override public void onLowMemory()
{
super.onLowMemory();
if(mapView != null) mapView.onLowMemory();
}
public void zoomIn()
{
if(controller == null) return;
controller.setZoomEased(controller.getZoom() + 1, 500);
updateView();
}
public void zoomOut()
{
if(controller == null) return;
controller.setZoomEased(controller.getZoom() - 1, 500);
updateView();
}
}