package de.westnordost.streetcomplete.tangram;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.Nullable;
import com.mapzen.tangram.LabelPickResult;
import com.mapzen.tangram.LngLat;
import com.mapzen.tangram.MapController;
import com.mapzen.tangram.MapData;
import com.mapzen.tangram.TouchInput;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import de.westnordost.streetcomplete.data.Quest;
import de.westnordost.streetcomplete.data.QuestGroup;
import de.westnordost.streetcomplete.data.osm.ElementGeometry;
import de.westnordost.streetcomplete.util.SlippyMapMath;
import de.westnordost.osmapi.map.data.BoundingBox;
import de.westnordost.osmapi.map.data.LatLon;
import de.westnordost.streetcomplete.util.SphericalEarthMath;
public class QuestsMapFragment extends MapFragment implements TouchInput.TapResponder,
MapController.LabelPickListener
{
private static final String MARKER_QUEST_ID = "quest_id";
private static final String MARKER_QUEST_GROUP = "quest_group";
private static final String GEOMETRY_LAYER = "streetcomplete_geometry";
private static final String QUESTS_LAYER = "streetcomplete_quests";
private MapData questsLayer;
private MapData geometryLayer;
private Float previousZoom = null;
private LngLat lastPos;
private Rect lastDisplayedRect;
private Set<Point> retrievedTiles;
private static final int TILES_ZOOM = 14;
private Listener listener;
public interface Listener
{
void onClickedQuest(QuestGroup questGroup, Long questId);
void onClickedMapAt(@Nullable LatLon position);
/** Called once the given bbox comes into view first (listener should get quests there) */
void onFirstInView(BoundingBox bbox);
}
@Override public void onAttach(Activity activity)
{
super.onAttach(activity);
listener = (Listener) activity;
}
@Override public void onStart()
{
super.onStart();
/* while the map fragment is stopped, there could still be a download which retrieves new
* quests in progress. If the retrieved tiles memory would not be cleared, the map would not
* retrieve these new quests from DB when the user scrolls over the map because the map
* thinks it already retrieved the quests from DB.
* (If a download is active while the user views the map, the quests are added on the fly) */
retrievedTiles = new HashSet<>();
}
@Override public void onDestroy()
{
super.onDestroy();
questsLayer = geometryLayer = null;
}
protected void initMap()
{
super.initMap();
retrievedTiles = new HashSet<>();
geometryLayer = controller.addDataLayer(GEOMETRY_LAYER);
questsLayer = controller.addDataLayer(QUESTS_LAYER);
controller.setTapResponder(this);
controller.setLabelPickListener(this);
controller.setPickRadius(1);
}
private BitmapDrawable createBitmapDrawableFrom(int resId)
{
Drawable drawable = getResources().getDrawable(resId);
if(drawable instanceof BitmapDrawable) return (BitmapDrawable) drawable;
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);
}
@Override public boolean onShove(float distance)
{
// no tilting the map! (for now)
return true;
}
@Override public boolean onRotate(float x, float y, float rotation)
{
// no rotating the map! (for now)
return true;
}
@Override public boolean onSingleTapUp(float x, float y)
{
return false;
}
@Override public boolean onSingleTapConfirmed(float x, float y)
{
controller.pickLabel(x,y);
return true;
}
@Override
public void onLabelPick(LabelPickResult labelPickResult, float positionX, float positionY)
{
if(labelPickResult == null
|| labelPickResult.getType() != LabelPickResult.LabelType.ICON
|| labelPickResult.getProperties() == null
|| !labelPickResult.getProperties().containsKey(MARKER_QUEST_ID))
{
onClickedMap(positionX, positionY);
return;
}
previousZoom = Math.max(17.5f,controller.getZoom()); // never zoom back further than 17.5
if(controller.getZoom() < 19)
{
LatLon pos = TangramConst.toLatLon(labelPickResult.getCoordinates());
LngLat zoomTo = TangramConst.toLngLat(SphericalEarthMath.translate(pos,20,180));
// move center a little because we have the bottom sheet blocking part of the map (hopefully temporary solution)
controller.setPositionEased(zoomTo, 500);
controller.setZoomEased(19, 500);
}
else
{
controller.setPositionEased(labelPickResult.getCoordinates(), 500);
}
updateView();
Map<String,String> props = labelPickResult.getProperties();
listener.onClickedQuest(
QuestGroup.valueOf(props.get(MARKER_QUEST_GROUP)),
Long.valueOf(props.get(MARKER_QUEST_ID))
);
}
private void onClickedMap(float positionX, float positionY)
{
final LngLat pos = controller.screenPositionToLngLat(new PointF(positionX, positionY));
// necessary until/if not https://github.com/tangrams/tangram-es/issues/1365 is fixed
getActivity().runOnUiThread(new Runnable()
{
@Override public void run()
{
listener.onClickedMapAt(TangramConst.toLatLon(pos));
}
});
}
protected void updateView()
{
super.updateView();
if(controller.getZoom() < TILES_ZOOM) return;
// check if anything changed (needs to be extended when I reenable tilt and rotation)
LngLat positionNow = controller.getPosition();
if(lastPos != null && lastPos.equals(positionNow)) return;
lastPos = positionNow;
BoundingBox displayedArea = getDisplayedArea();
if(displayedArea == null) return;
Rect tilesRect = SlippyMapMath.enclosingTiles(displayedArea, TILES_ZOOM);
if(lastDisplayedRect != null && lastDisplayedRect.equals(tilesRect)) return;
lastDisplayedRect = tilesRect;
// area to big -> skip ( see https://github.com/tangrams/tangram-es/issues/1492 )
if(tilesRect.width() * tilesRect.height() > 4)
{
return;
}
List<Point> tiles = SlippyMapMath.asTileList(tilesRect);
tiles.removeAll(retrievedTiles);
Rect minRect = SlippyMapMath.minRect(tiles);
if(minRect == null) return;
BoundingBox bbox = SlippyMapMath.asBoundingBox(minRect, TILES_ZOOM);
listener.onFirstInView(bbox);
// debugging
/*List<LatLon> corners = new ArrayList<LatLon>(5);
corners.add(bbox.getMin());
corners.add(new OsmLatLon(bbox.getMinLatitude(), bbox.getMaxLongitude()));
corners.add(bbox.getMax());
corners.add(new OsmLatLon(bbox.getMaxLatitude(), bbox.getMinLongitude()));
corners.add(bbox.getMin());
ElementGeometry e = new ElementGeometry(null, Collections.singletonList(corners));
addQuestGeometry(e);*/
retrievedTiles.addAll(tiles);
}
public void addQuestGeometry(ElementGeometry g)
{
if(geometryLayer == null) return; // might still be null - async calls...
Map<String,String> props = new HashMap<>();
if(g.polygons != null)
{
props.put("type", "poly");
geometryLayer.addPolygon(TangramConst.toLngLat(g.polygons), props);
}
else if(g.polylines != null)
{
props.put("type", "line");
List<List<LngLat>> polylines = TangramConst.toLngLat(g.polylines);
for(List<LngLat> polyline : polylines)
{
geometryLayer.addPolyline(polyline, props);
}
}
else if(g.center != null)
{
props.put("type", "point");
geometryLayer.addPoint(TangramConst.toLngLat(g.center), props);
}
controller.applySceneUpdates();
}
public void removeQuestGeometry()
{
if(geometryLayer != null) geometryLayer.clear();
if(controller != null)
{
if (previousZoom != null) controller.setZoomEased(previousZoom, 500);
}
}
/*
public void addQuest(Quest quest, QuestGroup group)
{
// TODO: this method may also be called for quests that are already displayed on this map
if(questsLayer == null) return;
LngLat pos = TangramConst.toLngLat(quest.getMarkerLocation());
Map<String, String> props = new HashMap<>();
props.put("type", "point");
props.put("kind", quest.getType().getIconName());
props.put(MARKER_QUEST_GROUP, group.name());
props.put(MARKER_QUEST_ID, String.valueOf(quest.getId()));
questsLayer.addPoint(pos, props);
controller.applySceneUpdates();
}
*/
public void addQuests(Iterable quests, QuestGroup group)
{
if(questsLayer == null) return;
StringBuilder geoJson = new StringBuilder();
geoJson.append("{\"type\":\"FeatureCollection\",\"features\": [");
boolean first = true;
for(Object q : quests)
{
Quest quest = (Quest) q;
if(first) first = false;
else geoJson.append(",");
LatLon pos = quest.getMarkerLocation();
geoJson.append("{\"type\":\"Feature\",");
geoJson.append("\"geometry\":{\"type\":\"Point\",\"coordinates\": [");
geoJson.append(pos.getLongitude());
geoJson.append(",");
geoJson.append(pos.getLatitude());
geoJson.append("]},\"properties\": {\"type\":\"point\", \"kind\":\"");
geoJson.append(quest.getType().getIconName());
geoJson.append("\",\"");
geoJson.append(MARKER_QUEST_GROUP);
geoJson.append("\":\"");
geoJson.append(group.name());
geoJson.append("\",\"");
geoJson.append(MARKER_QUEST_ID);
geoJson.append("\":\"");
geoJson.append(quest.getId());
geoJson.append("\"}}");
}
geoJson.append("]}");
questsLayer.addGeoJson(geoJson.toString());
}
public void removeQuests(Collection<Long> questIds, QuestGroup group)
{
// TODO: this method may also be called for quests that are not displayed on this map (anymore)
if(questsLayer == null) return;
// TODO (currently not possible with tangram, but it has been announced that this will soon
// be added
// so for now...:
questsLayer.clear();
retrievedTiles.clear();
lastPos = null;
lastDisplayedRect = null;
updateView();
}
public BoundingBox getDisplayedArea()
{
if(controller == null) return null;
if(getView() == null) return null;
Point size = new Point(getView().getMeasuredWidth(), getView().getMeasuredHeight());
// the special cases here are: map tilt and map rotation:
// * map tilt makes the screen area -> world map area into a trapezoid
// * map rotation makes the screen area -> world map area into a rotated rectangle
// dealing with tilt: this method is just not defined if the tilt is above a certain limit
if(controller.getTilt() > Math.PI / 4f) return null; // 45°
LatLon[] positions = new LatLon[4];
positions[0] = getLatLonAtPos(new PointF(0,0));
positions[1] = getLatLonAtPos(new PointF(size.x, 0));
positions[2] = getLatLonAtPos(new PointF(0,size.y));
positions[3] = getLatLonAtPos(new PointF(size));
// dealing with rotation: find each the largest latlon and the smallest latlon, that'll
// be our bounding box
Double latMin = null, lonMin = null, latMax = null, lonMax = null;
for (LatLon position : positions)
{
double lat = position.getLatitude();
double lon = position.getLongitude();
if (latMin == null || latMin > lat) latMin = lat;
if (latMax == null || latMax < lat) latMax = lat;
if (lonMin == null || lonMin > lon) lonMin = lon;
if (lonMax == null || lonMax < lon) lonMax = lon;
}
return new BoundingBox(latMin, lonMin, latMax, lonMax);
}
private LatLon getLatLonAtPos(PointF pointF)
{
return TangramConst.toLatLon(controller.screenPositionToLngLat(pointF));
}
public LatLon getPosition()
{
if(controller == null) return null;
return TangramConst.toLatLon(controller.getPosition());
}
}