package net.osmand.plus.views;
import gnu.trove.list.array.TByteArrayList;
import gnu.trove.list.array.TIntArrayList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.TreeMap;
import net.osmand.Location;
import net.osmand.data.LatLon;
import net.osmand.data.QuadRect;
import net.osmand.data.RotatedTileBox;
import net.osmand.plus.R;
import net.osmand.plus.routing.RouteCalculationResult;
import net.osmand.plus.routing.RouteDirectionInfo;
import net.osmand.plus.routing.RoutingHelper;
import net.osmand.util.MapUtils;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.PorterDuff.Mode;
import android.graphics.PorterDuffColorFilter;
public class RouteLayer extends OsmandMapLayer {
private static final float EPSILON_IN_DPI = 2;
private OsmandMapTileView view;
private final RoutingHelper helper;
// keep array lists created
private List<Location> actionPoints = new ArrayList<Location>();
private Path path;
// cache
private Bitmap coloredArrowUp;
private Bitmap actionArrow;
private Paint paintIcon;
private Paint paintIconAction;
private Paint paintIconSelected;
private Bitmap selectedPoint;
private LatLon selectedPointLatLon;
private RenderingLineAttributes attrs;
public RouteLayer(RoutingHelper helper){
this.helper = helper;
}
public LatLon getSelectedPointLatLon() {
return selectedPointLatLon;
}
public void setSelectedPointLatLon(LatLon selectedPointLatLon) {
this.selectedPointLatLon = selectedPointLatLon;
}
private void initUI() {
actionArrow = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_action_arrow, null);
path = new Path();
paintIcon = new Paint();
paintIcon.setFilterBitmap(true);
paintIcon.setAntiAlias(true);
paintIcon.setColor(Color.BLACK);
paintIcon.setStrokeWidth(3);
paintIconAction = new Paint();
paintIconAction.setFilterBitmap(true);
paintIconAction.setAntiAlias(true);
attrs = new RenderingLineAttributes("route");
attrs.defaultWidth = (int) (12 * view.getDensity());
attrs.defaultWidth3 = (int) (7 * view.getDensity());
attrs.defaultColor = view.getResources().getColor(R.color.nav_track);
attrs.paint3.setStrokeCap(Cap.BUTT);
attrs.paint3.setColor(Color.WHITE);
attrs.paint2.setStrokeCap(Cap.BUTT);
attrs.paint2.setColor(Color.BLACK);
paintIconSelected = new Paint();
selectedPoint = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_default_location);
}
@Override
public void initLayer(OsmandMapTileView view) {
this.view = view;
initUI();
}
public void updateLayerStyle() {
attrs.cachedHash = -1;
}
@Override
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
if (helper.getFinalLocation() != null && helper.getRoute().isCalculated()) {
boolean updatePaints = attrs.updatePaints(view, settings, tileBox);
attrs.isPaint3 = false;
attrs.isPaint2 = false;
if(updatePaints) {
paintIconAction.setColorFilter(new PorterDuffColorFilter(attrs.paint3.getColor(), Mode.MULTIPLY));
paintIcon.setColorFilter(new PorterDuffColorFilter(attrs.paint2.getColor(), Mode.MULTIPLY));
}
if(coloredArrowUp == null) {
Bitmap originalArrowUp = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_route_direction_arrow, null);
coloredArrowUp = originalArrowUp;
// coloredArrowUp = Bitmap.createScaledBitmap(originalArrowUp, originalArrowUp.getWidth() * 3 / 4,
// originalArrowUp.getHeight() * 3 / 4, true);
}
int w = tileBox.getPixWidth();
int h = tileBox.getPixHeight();
Location lastProjection = helper.getLastProjection();
final RotatedTileBox cp ;
if(lastProjection != null &&
tileBox.containsLatLon(lastProjection.getLatitude(), lastProjection.getLongitude())){
cp = tileBox.copy();
cp.increasePixelDimensions(w /2, h);
} else {
cp = tileBox;
}
final QuadRect latlonRect = cp.getLatLonBounds();
double topLatitude = latlonRect.top;
double leftLongitude = latlonRect.left;
double bottomLatitude = latlonRect.bottom;
double rightLongitude = latlonRect.right;
// double lat = 0;
// double lon = 0;
// this is buggy lat/lon should be 0 but in that case
// it needs to be fixed in case there is no route points in the view bbox
double lat = topLatitude - bottomLatitude + 0.1;
double lon = rightLongitude - leftLongitude + 0.1;
drawLocations(tileBox, canvas, topLatitude + lat, leftLongitude - lon, bottomLatitude - lat, rightLongitude + lon);
if (selectedPointLatLon != null
&& selectedPointLatLon.getLatitude() >= latlonRect.bottom
&& selectedPointLatLon.getLatitude() <= latlonRect.top
&& selectedPointLatLon.getLongitude() >= latlonRect.left
&& selectedPointLatLon.getLongitude() <= latlonRect.right) {
float x = tileBox.getPixXFromLatLon(selectedPointLatLon.getLatitude(), selectedPointLatLon.getLongitude());
float y = tileBox.getPixYFromLatLon(selectedPointLatLon.getLatitude(), selectedPointLatLon.getLongitude());
canvas.drawBitmap(selectedPoint, x - selectedPoint.getWidth() / 2, y - selectedPoint.getHeight() / 2, paintIconSelected);
}
}
}
@Override
public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {}
private void drawAction(RotatedTileBox tb, Canvas canvas, List<Location> actionPoints) {
if (actionPoints.size() > 0) {
canvas.rotate(-tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
try {
Path pth = new Path();
Matrix matrix = new Matrix();
boolean first = true;
int x = 0, px = 0, py = 0, y = 0;
for (int i = 0; i < actionPoints.size(); i++) {
Location o = actionPoints.get(i);
if (o == null) {
first = true;
canvas.drawPath(pth, attrs.paint3);
double angleRad = Math.atan2(y - py, x - px);
double angle = (angleRad * 180 / Math.PI) + 90f;
double distSegment = Math.sqrt((y - py) * (y - py) + (x - px) * (x - px));
if (distSegment == 0) {
continue;
}
// int len = (int) (distSegment / pxStep);
float pdx = x - px;
float pdy = y - py;
matrix.reset();
matrix.postTranslate(0, -actionArrow.getHeight() / 2);
matrix.postRotate((float) angle, actionArrow.getWidth() / 2, 0);
matrix.postTranslate(px + pdx - actionArrow.getWidth() / 2, py + pdy);
canvas.drawBitmap(actionArrow, matrix, paintIconAction);
} else {
px = x;
py = y;
x = (int) tb.getPixXFromLatLon(o.getLatitude(), o.getLongitude());
y = (int) tb.getPixYFromLatLon(o.getLatitude(), o.getLongitude());
if (first) {
pth.reset();
pth.moveTo(x, y);
first = false;
} else {
pth.lineTo(x, y);
}
}
}
} finally {
canvas.rotate(tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
}
}
}
private void cullRamerDouglasPeucker(TByteArrayList survivor, List<Location> points,
int start, int end, double epsillon) {
double dmax = Double.NEGATIVE_INFINITY;
int index = -1;
Location startPt = points.get(start);
Location endPt = points.get(end);
for (int i = start + 1; i < end; i++) {
Location pt = points.get(i);
double d = MapUtils.getOrthogonalDistance(pt.getLatitude(), pt.getLongitude(),
startPt.getLatitude(), startPt.getLongitude(), endPt.getLatitude(), endPt.getLongitude());
if (d > dmax) {
dmax = d;
index = i;
}
}
if (dmax > epsillon) {
cullRamerDouglasPeucker(survivor, points, start, index, epsillon);
cullRamerDouglasPeucker(survivor, points, index, end, epsillon);
} else {
survivor.set(end, (byte) 1);
}
}
private void drawArrowsOverPath(Canvas canvas, RotatedTileBox tb, TIntArrayList tx, TIntArrayList ty,
List<Double> angles, List<Double> distances, Bitmap arrow, double distPixToFinish) {
int h = tb.getPixHeight();
int w = tb.getPixWidth();
int left = -w / 4;
int right = w + w / 4;
int top = - h/4;
int bottom = h + h/4;
double pxStep = arrow.getHeight() * 4f;
Matrix matrix = new Matrix();
double dist = 0;
if(distPixToFinish != 0) {
dist = distPixToFinish - pxStep * ((int) (distPixToFinish / pxStep)); // dist < 1
}
for (int i = tx.size() - 2; i >= 0; i --) {
int px = tx.get(i);
int py = ty.get(i);
int x = tx.get(i + 1);
int y = ty.get(i + 1);
double distSegment = distances.get(i + 1);
double angle = angles.get(i + 1);
// double angleRad = Math.atan2(y - py, x - px);
// angle = (angleRad * 180 / Math.PI) + 90f;
// distSegment = Math.sqrt((y - py) * (y - py) + (x - px) * (x - px));
if(distSegment == 0) {
continue;
}
if(dist >= pxStep) {
dist = 0; // unnecessary but double check to avoid errors
}
double percent = 1 - (pxStep - dist) / distSegment;
dist += distSegment;
while(dist >= pxStep) {
double pdx = (x - px) * percent;
double pdy = (y - py) * percent;
if (isIn((int)(px + pdx), (int) (py + pdy), left, top, right, bottom)) {
float icony = (float) (py + pdy);
float iconx = (float) (px + pdx - arrow.getWidth() / 2);
matrix.reset();
matrix.postTranslate(0, -arrow.getHeight() / 2);
matrix.postRotate((float) angle, arrow.getWidth() / 2, 0);
matrix.postTranslate(iconx, icony);
canvas.drawBitmap(arrow, matrix, paintIcon);
}
dist -= pxStep;
percent -= pxStep / distSegment;
}
}
}
private static class RouteGeometryZoom {
final TByteArrayList simplifyPoints;
List<Double> distances;
List<Double> angles;
public RouteGeometryZoom(List<Location> locations, RotatedTileBox tb) {
// this.locations = locations;
tb = new RotatedTileBox(tb);
tb.setZoomAndAnimation(tb.getZoom(), 0, tb.getZoomFloatPart());
simplifyPoints = new TByteArrayList(locations.size());
distances = new ArrayList<Double>(locations.size());
angles = new ArrayList<Double>(locations.size());
simplifyPoints.fill(0, locations.size(), (byte)0);
if(locations.size() > 0) {
simplifyPoints.set(0, (byte) 1);
}
double distInPix = (tb.getDistance(0, 0, tb.getPixWidth(), 0) / tb.getPixWidth());
double cullDistance = (distInPix * (EPSILON_IN_DPI * Math.max(1, tb.getDensity())));
cullRamerDouglasPeucker(simplifyPoints, locations, 0, locations.size() - 1, cullDistance);
int previousIndex = -1;
for(int i = 0; i < locations.size(); i++) {
double d = 0;
double angle = 0;
if(simplifyPoints.get(i) > 0) {
if(previousIndex > -1) {
Location loc = locations.get(i);
Location pr = locations.get(previousIndex);
float x = tb.getPixXFromLatLon(loc.getLatitude(), loc.getLongitude());
float y = tb.getPixYFromLatLon(loc.getLatitude(), loc.getLongitude());
float px = tb.getPixXFromLatLon(pr.getLatitude(), pr.getLongitude());
float py = tb.getPixYFromLatLon(pr.getLatitude(), pr.getLongitude());
d = Math.sqrt((y - py) * (y - py) + (x - px) * (x - px));
if(px != x || py != y) {
double angleRad = Math.atan2(y - py, x - px);
angle = (angleRad * 180 / Math.PI) + 90f;
}
}
previousIndex = i;
}
distances.add(d);
angles.add(angle);
}
}
public List<Double> getDistances() {
return distances;
}
private void cullRamerDouglasPeucker(TByteArrayList survivor, List<Location> points,
int start, int end, double epsillon) {
double dmax = Double.NEGATIVE_INFINITY;
int index = -1;
Location startPt = points.get(start);
Location endPt = points.get(end);
for (int i = start + 1; i < end; i++) {
Location pt = points.get(i);
double d = MapUtils.getOrthogonalDistance(pt.getLatitude(), pt.getLongitude(),
startPt.getLatitude(), startPt.getLongitude(), endPt.getLatitude(), endPt.getLongitude());
if (d > dmax) {
dmax = d;
index = i;
}
}
if (dmax > epsillon) {
cullRamerDouglasPeucker(survivor, points, start, index, epsillon);
cullRamerDouglasPeucker(survivor, points, index, end, epsillon);
} else {
survivor.set(end, (byte) 1);
}
}
public TByteArrayList getSimplifyPoints() {
return simplifyPoints;
}
}
private class RouteSimplificationGeometry {
RouteCalculationResult route;
double mapDensity;
TreeMap<Integer, RouteGeometryZoom> zooms = new TreeMap<>();
List<Location> locations = Collections.emptyList();
// cache arrays
TIntArrayList tx = new TIntArrayList();
TIntArrayList ty = new TIntArrayList();
List<Double> angles = new ArrayList<>();
List<Double> distances = new ArrayList<>();
public void updateRoute(RotatedTileBox tb, RouteCalculationResult route) {
if(tb.getMapDensity() != mapDensity || this.route != route) {
this.route = route;
if(route == null) {
locations = Collections.emptyList();
} else {
locations = route.getImmutableAllLocations();
}
this.mapDensity = tb.getMapDensity();
zooms.clear();
}
}
private RouteGeometryZoom getGeometryZoom(RotatedTileBox tb) {
RouteGeometryZoom zm = zooms.get(tb.getZoom());
if(zm == null) {
zm = new RouteGeometryZoom(locations, tb);
zooms.put(tb.getZoom(), zm);
}
return zm;
}
private void drawSegments(RotatedTileBox tb, Canvas canvas, double topLatitude, double leftLongitude,
double bottomLatitude, double rightLongitude, Location lastProjection, int currentRoute) {
RouteGeometryZoom geometryZoom = getGeometryZoom(tb);
TByteArrayList simplification = geometryZoom.getSimplifyPoints();
List<Double> odistances = geometryZoom.getDistances();
clearArrays();
boolean previousVisible = false;
if (lastProjection != null) {
if (leftLongitude <= lastProjection.getLongitude() && lastProjection.getLongitude() <= rightLongitude
&& bottomLatitude <= lastProjection.getLatitude() && lastProjection.getLatitude() <= topLatitude) {
addLocation(tb, lastProjection, tx, ty, angles, distances, 0);
previousVisible = true;
}
}
List<Location> routeNodes = locations;
int previous = -1;
for (int i = currentRoute; i < routeNodes.size(); i++) {
Location ls = routeNodes.get(i);
if(simplification.getQuick(i) == 0) {
continue;
}
if (leftLongitude <= ls.getLongitude() && ls.getLongitude() <= rightLongitude && bottomLatitude <= ls.getLatitude()
&& ls.getLatitude() <= topLatitude) {
double dist = 0;
if (!previousVisible) {
Location lt = null;
if (previous != -1) {
lt = routeNodes.get(previous);
dist = odistances.get(i);
} else if (lastProjection != null) {
lt = lastProjection;
}
addLocation(tb, lt, tx, ty, angles, distances, 0); // first point
}
addLocation(tb, ls, tx, ty, angles, distances, dist);
previousVisible = true;
} else if (previousVisible) {
addLocation(tb, ls, tx, ty, angles, distances, previous == -1 ? 0 : odistances.get(i));
double distToFinish = 0;
for(int ki = i + 1; ki < odistances.size(); ki++) {
distToFinish += odistances.get(ki);
}
drawRouteSegment(tb, canvas, tx, ty, angles, distances, distToFinish);
previousVisible = false;
clearArrays();
}
previous = i;
}
drawRouteSegment(tb, canvas, tx, ty, angles, distances, 0);
}
private void clearArrays() {
tx.clear();
ty.clear();
distances.clear();
angles.clear();
}
private void addLocation(RotatedTileBox tb, Location ls, TIntArrayList tx, TIntArrayList ty,
List<Double> angles, List<Double> distances, double dist) {
float x = tb.getPixXFromLatLon(ls.getLatitude(), ls.getLongitude());
float y = tb.getPixYFromLatLon(ls.getLatitude(), ls.getLongitude());
float px = x;
float py = y;
int previous = tx.size() - 1;
if (previous >= 0 && previous < tx.size()) {
px = tx.get(previous);
py = ty.get(previous);
}
double angle = 0;
if (px != x || py != y) {
double angleRad = Math.atan2(y - py, x - px);
angle = (angleRad * 180 / Math.PI) + 90f;
}
double distSegment = Math.sqrt((y - py) * (y - py) + (x - px) * (x - px));
if(dist != 0) {
distSegment = dist;
}
tx.add((int) x);
ty.add((int) y);
angles.add(angle);
distances.add(distSegment);
}
}
RouteSimplificationGeometry routeGeometry = new RouteSimplificationGeometry();
private void drawRouteSegment(RotatedTileBox tb, Canvas canvas, TIntArrayList tx, TIntArrayList ty,
List<Double> angles, List<Double> distances, double distToFinish) {
if(tx.size() < 2) {
return;
}
try {
path.reset();
canvas.rotate(-tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
calculatePath(tb, tx, ty, path);
attrs.drawPath(canvas, path);
if (tb.getZoomAnimation() == 0) {
drawArrowsOverPath(canvas, tb, tx, ty, angles, distances, coloredArrowUp, distToFinish);
}
} finally {
canvas.rotate(tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
}
}
public void drawLocations(RotatedTileBox tb, Canvas canvas, double topLatitude, double leftLongitude, double bottomLatitude, double rightLongitude) {
RouteCalculationResult route = helper.getRoute();
routeGeometry.updateRoute(tb, route);
routeGeometry.drawSegments(tb, canvas, topLatitude, leftLongitude, bottomLatitude, rightLongitude,
helper.getLastProjection(), route == null ? 0 : route.getCurrentRoute());
List<RouteDirectionInfo> rd = helper.getRouteDirections();
Iterator<RouteDirectionInfo> it = rd.iterator();
if (tb.getZoom() >= 14) {
List<Location> actionPoints = calculateActionPoints(topLatitude, leftLongitude, bottomLatitude, rightLongitude, helper.getLastProjection(),
helper.getRoute().getRouteLocations(), helper.getRoute().getCurrentRoute(), it, tb.getZoom());
drawAction(tb, canvas, actionPoints);
}
}
private List<Location> calculateActionPoints(double topLatitude, double leftLongitude, double bottomLatitude,
double rightLongitude, Location lastProjection, List<Location> routeNodes, int cd,
Iterator<RouteDirectionInfo> it, int zoom) {
RouteDirectionInfo nf = null;
double DISTANCE_ACTION = 35;
if(zoom >= 17) {
DISTANCE_ACTION = 15;
} else if (zoom == 15) {
DISTANCE_ACTION = 70;
} else if (zoom < 15) {
DISTANCE_ACTION = 110;
}
double actionDist = 0;
Location previousAction = null;
List<Location> actionPoints = this.actionPoints;
actionPoints.clear();
int prevFinishPoint = -1;
for (int routePoint = 0; routePoint < routeNodes.size(); routePoint++) {
Location loc = routeNodes.get(routePoint);
if(nf != null) {
int pnt = nf.routeEndPointOffset == 0 ? nf.routePointOffset : nf.routeEndPointOffset;
if(pnt < routePoint + cd ) {
nf = null;
}
}
while (nf == null && it.hasNext()) {
nf = it.next();
int pnt = nf.routeEndPointOffset == 0 ? nf.routePointOffset : nf.routeEndPointOffset;
if (pnt < routePoint + cd) {
nf = null;
}
}
boolean action = nf != null && (nf.routePointOffset == routePoint + cd ||
(nf.routePointOffset <= routePoint + cd && routePoint + cd <= nf.routeEndPointOffset));
if(!action && previousAction == null) {
// no need to check
continue;
}
boolean visible = leftLongitude <= loc.getLongitude() && loc.getLongitude() <= rightLongitude && bottomLatitude <= loc.getLatitude()
&& loc.getLatitude() <= topLatitude;
if(action && !visible && previousAction == null) {
continue;
}
if (!action) {
// previousAction != null
float dist = loc.distanceTo(previousAction);
actionDist += dist;
if (actionDist >= DISTANCE_ACTION) {
actionPoints.add(calculateProjection(1 - (actionDist - DISTANCE_ACTION) / dist, previousAction, loc));
actionPoints.add(null);
prevFinishPoint = routePoint;
previousAction = null;
actionDist = 0;
} else {
actionPoints.add(loc);
previousAction = loc;
}
} else {
// action point
if (previousAction == null) {
addPreviousToActionPoints(actionPoints, lastProjection, routeNodes, DISTANCE_ACTION,
prevFinishPoint, routePoint, loc);
}
actionPoints.add(loc);
previousAction = loc;
prevFinishPoint = -1;
actionDist = 0;
}
}
if(previousAction != null) {
actionPoints.add(null);
}
return actionPoints;
}
private void addPreviousToActionPoints(List<Location> actionPoints, Location lastProjection, List<Location> routeNodes, double DISTANCE_ACTION,
int prevFinishPoint, int routePoint, Location loc) {
// put some points in front
int ind = actionPoints.size();
Location lprevious = loc;
double dist = 0;
for (int k = routePoint - 1; k >= -1; k--) {
Location l = k == -1 ? lastProjection : routeNodes.get(k);
float locDist = lprevious.distanceTo(l);
dist += locDist;
if (dist >= DISTANCE_ACTION) {
if (locDist > 1) {
actionPoints.add(ind,
calculateProjection(1 - (dist - DISTANCE_ACTION) / locDist, lprevious, l));
}
break;
} else {
actionPoints.add(ind, l);
lprevious = l;
}
if (prevFinishPoint == k) {
if (ind >= 2) {
actionPoints.remove(ind - 2);
actionPoints.remove(ind - 2);
}
break;
}
}
}
private Location calculateProjection(double part, Location lp, Location l) {
Location p = new Location(l);
p.setLatitude(lp.getLatitude() + part * (l.getLatitude() - lp.getLatitude()));
p.setLongitude(lp.getLongitude() + part * (l.getLongitude() - lp.getLongitude()));
return p;
}
public RoutingHelper getHelper() {
return helper;
}
@Override
public void destroyLayer() {
}
@Override
public boolean drawInScreenPixels() {
return false;
}
@Override
public boolean onLongPressEvent(PointF point, RotatedTileBox tileBox) {
return false;
}
@Override
public boolean onSingleTap(PointF point, RotatedTileBox tileBox) {
return false;
}
}