New GPX track rendering and animation/overlay package.
WHAT THIS PROVIDES: A generic rendering/draw of GPX tracks that allows multiple 'renders' to be included as a part of the draw of any TrkSegment. These 'renders' include the basic track drawing, but also other options such as dashed lines, rainbow-coloured altitude indication, speed indication, arrows to point direction, animating 'lines' showing direction of movement, and dots/text showing 1km (or any other) distances along tracks. It's very extendable and incredibly cheap in terms of procesisng speed. For a view of some of these operating on this version of the code, see https://youtu.be/aRGCNLmBAlk Additionally, all of the above have automatic asynchronous track resampling - either via line culling of Ramer-Douglas-Peucer (implemented for the base track draw at different zooms), or an actual resampler that takes a distance and steps off and creates a new track with points spaced exactly that distance apart along the original track. The asynchronous resampling/culling willl automatically enable the new (optimal) track display when the background task has completed. This is completely up to date with the master branch as of an hour or so ago! Two modified files - GPXUtilities Added some fields to WptPt to enable distance measurement on tracks and colouring for altitude/speed - GPXLayer Installed the new track rendering with examples (commented/out) Two new files - AsynchronousResampler.java Implements line resampling and culling asynchrnonously for all line drawing - Renderable.java Set of classes for drawing different kinds of gpx 'renders' - normal with automatic Ramer-Douglas-Peucer line culling - conveyor-belt type animation of segments on a path - altitude colouring of a path - speed colouring of a path - distance based waypoint/marker drawing
This commit is contained in:
parent
ce2f2eceee
commit
109ca692f1
4 changed files with 898 additions and 44 deletions
|
@ -2,12 +2,17 @@
|
|||
package net.osmand.plus;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
|
||||
import net.osmand.Location;
|
||||
import net.osmand.PlatformUtil;
|
||||
import net.osmand.data.LocationPoint;
|
||||
import net.osmand.data.PointDescription;
|
||||
import net.osmand.data.RotatedTileBox;
|
||||
import net.osmand.plus.views.OsmandMapTileView;
|
||||
import net.osmand.plus.views.Renderable;
|
||||
import net.osmand.util.Algorithms;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
|
@ -98,10 +103,20 @@ public class GPXUtilities {
|
|||
public double speed = 0;
|
||||
public double hdop = Double.NaN;
|
||||
public boolean deleted = false;
|
||||
public int colourARGB = 0; // point colour (used for altitude/speed colouring)
|
||||
public double distance = 0.0; // cumulative distance, if in a track
|
||||
|
||||
public WptPt() {
|
||||
}
|
||||
|
||||
public void setDistance(double dist) {
|
||||
distance = dist;
|
||||
}
|
||||
|
||||
public double getDistance() {
|
||||
return distance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColor() {
|
||||
return getColor(0);
|
||||
|
@ -166,6 +181,17 @@ public class GPXUtilities {
|
|||
|
||||
public static class TrkSegment extends GPXExtensions {
|
||||
public List<WptPt> points = new ArrayList<WptPt>();
|
||||
private OsmandMapTileView view;
|
||||
|
||||
// A list of renderables. A rendereable is 'a way of drawing' something related to a TrkSegment.
|
||||
// For example, we could have several renderables drawing on top of each other;
|
||||
// 1. rainbow coloured altitude indicator
|
||||
// 2. base rendered segments
|
||||
// 3. 'dot' 1km markers on top
|
||||
// These are dimply enabled by adding new Renderable objects to this list
|
||||
// Note; see addRenderers for a complete list of handled Renderables.
|
||||
|
||||
public List<Renderable.RenderableSegment> renders = new ArrayList<>();
|
||||
|
||||
public List<GPXTrackAnalysis> splitByDistance(double meters) {
|
||||
return split(getDistanceMetric(), getTimeSplit(), meters);
|
||||
|
@ -181,6 +207,57 @@ public class GPXUtilities {
|
|||
return convert(splitSegments);
|
||||
}
|
||||
|
||||
|
||||
// Track segments are drawn by 'Renderable' objects. A segment can have zero or more renderables,
|
||||
// each of which performs its own point-culling/reduction and display drawing. There are a selction
|
||||
// of renderable types, as defiend in Renderable.RenderType. To use, call this function to create
|
||||
// a new renderable type, and then attach it to your displayable object in a list or similar.
|
||||
|
||||
// The two parameters' maning varies based upon the type of renderable - see the parameters' usage
|
||||
// in each derived renderable class.
|
||||
|
||||
public Renderable.RenderableSegment addRenderable(OsmandMapTileView view, Renderable.RenderType type,
|
||||
double param1, double param2) {
|
||||
Renderable.RenderableSegment rs = null;
|
||||
switch (type) {
|
||||
case ORIGINAL: // a Ramer-Douglas-Peucer line reduction draw
|
||||
rs = new Renderable.RenderableSegment(type, points, param1, param2);
|
||||
break;
|
||||
case DISTANCE: // a resample every N metres draw
|
||||
rs = new Renderable.RenderableDot(type, points, param1, param2);
|
||||
break;
|
||||
case CONVEYOR: // an animating segment draw
|
||||
rs = new Renderable.RenderableConveyor(type, points, param1, param2);
|
||||
Renderable.startScreenRefresh(view, (long) param2);
|
||||
break;
|
||||
case ALTITUDE: // a colour-banded altitude draw
|
||||
rs = new Renderable.RenderableAltitude(type, points, param1, param2);
|
||||
break;
|
||||
case SPEED: // a colour-banded speed draw
|
||||
rs = new Renderable.RenderableSpeed(type, points, param1, param2);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (rs != null)
|
||||
renders.add(rs);
|
||||
return rs;
|
||||
}
|
||||
|
||||
|
||||
public void recalculateRenderScales(OsmandMapTileView view) {
|
||||
for (Renderable.RenderableSegment rs : renders)
|
||||
rs.recalculateRenderScale(view);
|
||||
}
|
||||
|
||||
public void drawRenderers(Paint p, Canvas c, RotatedTileBox tb) {
|
||||
for (Renderable.RenderableSegment rs : renders)
|
||||
rs.drawSingleSegment(p, c, tb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static class Track extends GPXExtensions {
|
||||
|
|
282
OsmAnd/src/net/osmand/plus/views/AsynchronousResampler.java
Normal file
282
OsmAnd/src/net/osmand/plus/views/AsynchronousResampler.java
Normal file
|
@ -0,0 +1,282 @@
|
|||
package net.osmand.plus.views;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import net.osmand.plus.GPXUtilities;
|
||||
import net.osmand.plus.views.OsmandMapTileView;
|
||||
import net.osmand.util.MapUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AsynchronousResampler extends AsyncTask<String,Integer,String> {
|
||||
|
||||
private OsmandMapTileView view;
|
||||
private Renderable.RenderableSegment rs;
|
||||
private Renderable.RenderType renderType;
|
||||
private double param1, param2;
|
||||
|
||||
private List<GPXUtilities.WptPt> culled = null;
|
||||
|
||||
|
||||
public AsynchronousResampler(Renderable.RenderType rt, OsmandMapTileView view,
|
||||
Renderable.RenderableSegment rs, double param1, double param2) {
|
||||
this.view = view;
|
||||
this.rs = rs;
|
||||
this.renderType = rt;
|
||||
|
||||
this.param1 = param1;
|
||||
this.param2 = param2;
|
||||
}
|
||||
|
||||
public int getColor2(double percent) {
|
||||
|
||||
// ugly code - given an input percentage (0.0-1.0) this will produce a colour from a "wide rainbow"
|
||||
// from purple (low) to red(high). This is useful for producing value-based colourations (e.g., altitude)
|
||||
|
||||
double a = (1. - percent) * 5.;
|
||||
int X = (int)Math.floor(a);
|
||||
int Y = (int)(Math.floor(255 * (a - X)));
|
||||
int r = 0,g = 0,b = 0;
|
||||
switch (X) {
|
||||
case 0:
|
||||
r = 255;
|
||||
g = Y;
|
||||
b = 0;
|
||||
break;
|
||||
case 1:
|
||||
r = 255 - Y;
|
||||
g = 255;
|
||||
b = 0;
|
||||
break;
|
||||
case 2:
|
||||
r = 0;
|
||||
g = 255;
|
||||
b = Y;
|
||||
break;
|
||||
case 3:
|
||||
r = 0;
|
||||
g = 255 - Y;
|
||||
b = 255;
|
||||
break;
|
||||
case 4:
|
||||
r = Y;
|
||||
g = 0;
|
||||
b = 255;
|
||||
break;
|
||||
case 5:
|
||||
r = 255;
|
||||
g = 0;
|
||||
b = 255;
|
||||
break;
|
||||
}
|
||||
return 0xFF000000 + (r<<16) + (g<<8) + b;
|
||||
}
|
||||
|
||||
|
||||
private List<GPXUtilities.WptPt> resampleAltitude(List<GPXUtilities.WptPt> pts, double dist) {
|
||||
|
||||
List<GPXUtilities.WptPt> track = resampleTrack(pts, dist);
|
||||
|
||||
int halfC = getColor2(0.5);
|
||||
|
||||
// Calculate the absolutes of the altitude variations
|
||||
Double max = Double.NEGATIVE_INFINITY;
|
||||
Double min = Double.POSITIVE_INFINITY;
|
||||
for (GPXUtilities.WptPt pt : track) {
|
||||
max = Math.max(max, pt.ele);
|
||||
min = Math.min(min, pt.ele);
|
||||
pt.colourARGB = halfC;
|
||||
}
|
||||
Double elevationRange = max-min;
|
||||
if (elevationRange > 0)
|
||||
for (GPXUtilities.WptPt pt : track)
|
||||
pt.colourARGB = getColor2((pt.ele - min)/elevationRange);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
|
||||
private List<GPXUtilities.WptPt> resampleSpeed(List<GPXUtilities.WptPt> pts, double dist) {
|
||||
|
||||
List<GPXUtilities.WptPt> track = resampleTrack(pts, dist);
|
||||
|
||||
GPXUtilities.WptPt lastPt = track.get(0);
|
||||
lastPt.speed = 0;
|
||||
|
||||
// calculate speeds
|
||||
for (int i=1; i<track.size(); i++) {
|
||||
GPXUtilities.WptPt pt = track.get(i);
|
||||
double delta = pt.time - lastPt.time;
|
||||
if (delta>0)
|
||||
pt.speed = MapUtils.getDistance(pt.getLatitude(),pt.getLongitude(),
|
||||
lastPt.getLatitude(),lastPt.getLongitude())/delta;
|
||||
else
|
||||
pt.speed = 0; // GPX doesn't have time - this is OK, colour will be mid-range for whole track
|
||||
lastPt = pt;
|
||||
}
|
||||
|
||||
// Calculate the absolutes of the altitude variations
|
||||
Double max = Double.NEGATIVE_INFINITY;
|
||||
Double min = Double.POSITIVE_INFINITY;
|
||||
for (GPXUtilities.WptPt pt : track) {
|
||||
max = Math.max(max, pt.speed);
|
||||
min = Math.min(min, pt.speed);
|
||||
pt.colourARGB = getColor2(0.5);
|
||||
}
|
||||
Double range = max-min;
|
||||
if (range > 0)
|
||||
for (GPXUtilities.WptPt pt : track)
|
||||
pt.colourARGB = getColor2((pt.speed - min) / range);
|
||||
|
||||
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Resample a list of points into a new list of points.
|
||||
// The new list is evenly-spaced (dist) and contains the first and last point from the original list.
|
||||
// The purpose is to allow tracks to be displayed with colours/shades/animation with even spacing
|
||||
// This routine essentially 'walks' along the path, dropping sample points along the trail where necessary. It is
|
||||
// Typically, pass a point list to this, and set dist (in metres) to something that's relative to screen zoom
|
||||
// The new point list has resampled times, elevations, speed and hdop too!
|
||||
|
||||
private List<GPXUtilities.WptPt> resampleTrack(List<GPXUtilities.WptPt> pts, double dist) {
|
||||
|
||||
ArrayList<GPXUtilities.WptPt> newPts = new ArrayList<GPXUtilities.WptPt>();
|
||||
|
||||
int ptCt = pts.size();
|
||||
if (pts != null && ptCt > 0) {
|
||||
|
||||
GPXUtilities.WptPt lastPt = pts.get(0);
|
||||
double segSub = 0;
|
||||
double cumDist = 0;
|
||||
for (int i = 1; i < ptCt; i++) {
|
||||
GPXUtilities.WptPt pt = pts.get(i);
|
||||
double segLength = MapUtils.getDistance(pt.getLatitude(), pt.getLongitude(), lastPt.getLatitude(), lastPt.getLongitude());
|
||||
|
||||
// March along the segment, calculating the interpolated point values as we go
|
||||
while (segSub < segLength) {
|
||||
double partial = segSub / segLength;
|
||||
GPXUtilities.WptPt newPoint = new GPXUtilities.WptPt(
|
||||
lastPt.getLatitude() + partial * (pt.getLatitude() - lastPt.getLatitude()),
|
||||
lastPt.getLongitude() + partial * (pt.getLongitude() - lastPt.getLongitude()),
|
||||
(long) (lastPt.time + partial * (pt.time - lastPt.time)),
|
||||
lastPt.ele + partial * (pt.ele - lastPt.ele),
|
||||
lastPt.speed + partial * (pt.speed - lastPt.speed),
|
||||
lastPt.hdop + partial * (pt.hdop - lastPt.hdop)
|
||||
);
|
||||
newPoint.setDistance(cumDist + segLength * partial);
|
||||
newPts.add(newPts.size(), newPoint);
|
||||
segSub += dist;
|
||||
}
|
||||
segSub -= segLength; // leftover
|
||||
cumDist += segLength;
|
||||
lastPt = pt;
|
||||
}
|
||||
|
||||
// Add in the last point as a terminator (with total distance recorded)
|
||||
GPXUtilities.WptPt newPoint = new GPXUtilities.WptPt( lastPt.getLatitude(), lastPt.getLongitude(), lastPt.time, lastPt.ele, lastPt.speed, lastPt. hdop);
|
||||
newPoint.setDistance(cumDist);
|
||||
newPts.add(newPts.size(), newPoint);
|
||||
}
|
||||
return newPts;
|
||||
}
|
||||
|
||||
// Reduce the point-count of the GPX track. The concept is that at arbitrary scales, some points are superfluous.
|
||||
// This is handled using the well-known 'Ramer-Douglas-Peucker' algorithm. This code is modified from the similar code elsewhere
|
||||
// but optimised for this specific usage.
|
||||
|
||||
private boolean[] cullRamerDouglasPeucer(List<GPXUtilities.WptPt> points, double epsilon) {
|
||||
|
||||
int nsize = points.size();
|
||||
boolean[] survivor = new boolean[nsize];
|
||||
if (nsize > 0) {
|
||||
cullRamerDouglasPeucer(points, epsilon, survivor, 0, nsize - 1);
|
||||
survivor[0] = true;
|
||||
}
|
||||
return survivor;
|
||||
}
|
||||
|
||||
private void cullRamerDouglasPeucer(List<GPXUtilities.WptPt> pt, double epsilon, boolean[] survivor, int start, int end) {
|
||||
|
||||
double dmax = -1;
|
||||
int index = -1;
|
||||
for (int i = start + 1; i < end; i++) {
|
||||
|
||||
if (isCancelled())
|
||||
return;
|
||||
|
||||
double d = MapUtils.getOrthogonalDistance(
|
||||
pt.get(i).getLatitude(), pt.get(i).getLongitude(),
|
||||
pt.get(start).getLatitude(), pt.get(start).getLongitude(),
|
||||
pt.get(end).getLatitude(), pt.get(end).getLongitude());
|
||||
if (d > dmax) {
|
||||
dmax = d;
|
||||
index = i;
|
||||
}
|
||||
}
|
||||
if (dmax >= epsilon) {
|
||||
cullRamerDouglasPeucer(pt, epsilon, survivor, start, index);
|
||||
cullRamerDouglasPeucer(pt, epsilon, survivor, index, end);
|
||||
} else {
|
||||
survivor[end] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doInBackground(String... params) {
|
||||
|
||||
String resp = "OK";
|
||||
try {
|
||||
List<GPXUtilities.WptPt> points = rs.getPoints();
|
||||
|
||||
switch (renderType) {
|
||||
|
||||
case ALTITUDE:
|
||||
culled = resampleAltitude(points, param1);
|
||||
break;
|
||||
|
||||
case SPEED:
|
||||
culled = resampleSpeed(points, param1);
|
||||
break;
|
||||
|
||||
case CONVEYOR:
|
||||
case DISTANCE:
|
||||
culled = resampleTrack(points, param1);
|
||||
break;
|
||||
|
||||
case ORIGINAL:
|
||||
boolean[] survivor = cullRamerDouglasPeucer(points, param1);
|
||||
culled = new ArrayList<>();
|
||||
for (int i = 0; i < survivor.length; i++)
|
||||
if (survivor[i])
|
||||
culled.add(points.get(i));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
resp = e.getMessage();
|
||||
}
|
||||
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(String result) {
|
||||
// executes on the UI thread so it's OK to change its variables
|
||||
if (rs != null && result.equals("OK") && !isCancelled()) {
|
||||
rs.setRDP(culled);
|
||||
view.refreshMap(); // FORCE redraw to guarantee new culled track is shown
|
||||
}
|
||||
}
|
||||
}
|
|
@ -207,7 +207,8 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
|
|||
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
|
||||
if(points != null) {
|
||||
updatePaints(0, false, false, settings, tileBox);
|
||||
drawSegments(canvas, tileBox, points);
|
||||
for (TrkSegment ts : points)
|
||||
ts.drawRenderers(paint, canvas,tileBox);
|
||||
} else {
|
||||
List<SelectedGpxFile> selectedGPXFiles = selectedGpxHelper.getSelectedGPXFiles();
|
||||
cache.clear();
|
||||
|
@ -326,10 +327,57 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
|
|||
private void drawSelectedFilesSegments(Canvas canvas, RotatedTileBox tileBox,
|
||||
List<SelectedGpxFile> selectedGPXFiles, DrawSettings settings) {
|
||||
for (SelectedGpxFile g : selectedGPXFiles) {
|
||||
List<TrkSegment> points = g.getPointsToDisplay();
|
||||
boolean routePoints = g.isRoutePoints();
|
||||
updatePaints(g.getColor(), routePoints, g.isShowCurrentTrack(), settings, tileBox);
|
||||
drawSegments(canvas, tileBox, points);
|
||||
List<TrkSegment> segments = g.getPointsToDisplay();
|
||||
for (TrkSegment ts : segments) {
|
||||
|
||||
// TODO: Select/install renderables via UI or external process! (See comments below)
|
||||
|
||||
// Each TrkSegment has one or more 'Renderables' - these handle drawing different things such
|
||||
// as the original track, rainbow altitude colouring, 1km markers, etc. They also handle the
|
||||
// asynchronous re-sampling of tracks for more efficient display. For example, the default
|
||||
// operation Renderable.RenderType.ORIGINAL does Ramer-Douglas-Peucer line optimisation for
|
||||
// zoom-level changes in track resolution. Change the '18' to '20' to see it more clearly.
|
||||
|
||||
// Renderables are processed in order that ALL have asynchronous resampling/culling capabililty.
|
||||
// In an ideal world, the renderers to be used are selected via UI or externally, and the
|
||||
// segment will already have them attached. For this first version, we add them the very
|
||||
// first time the TrkSegment gets drawn.
|
||||
|
||||
// NOTE: At the moment the 'ORIGINAL' renderer below is TOTALLY FUNCTIONALLY EQUIVALENT <<<IMPORTANT!!!
|
||||
// with the master branch/version before I added this capability, except that the track now resamples
|
||||
// based on zoom level and displays the resampled version when its available.
|
||||
|
||||
if (ts.renders.size()==0) { // only do once
|
||||
|
||||
// TODO: To see more clearly the line reduction in action, change the 18 to a higher number (say, 18.5 or 19)...
|
||||
|
||||
ts.addRenderable(view, Renderable.RenderType.ORIGINAL, 18, 0); // the base line (distance modifier)
|
||||
|
||||
// TODO : enable these to see how the experimental conveyor, altitude, speed, waypoint renders work
|
||||
|
||||
// Note: the conveyor is EXAMPLE ONLY just to show an example of multiple renderables being used
|
||||
// - it is intended to show how support for arrows in route-based rendering can be supported by this
|
||||
// type of system. Please leave the code alone, and I will implement the arrows after this code is approved!
|
||||
|
||||
// You can, of course, comment out the following...
|
||||
|
||||
ts.addRenderable(view, Renderable.RenderType.CONVEYOR, 10, 250); // conveyor belt animation (m,refresh(ms))
|
||||
//ts.addRenderable(view, Renderable.RenderType.ALTITUDE, 25, 128); // an altitude display (m,alpha) << IMPORTANT: See [note 1] below
|
||||
//ts.addRenderable(view, Renderable.RenderType.DISTANCE, 1000, 1); // 1km markings (m,size)
|
||||
//ts.addRenderable(view, Renderable.RenderType.SPEED, 20, 255); // a speed display (m,alpha) << IMPORTANT: See [Note 1]
|
||||
|
||||
// [Note 1]: The altitude and speed renders (only if enabled, above) have a bug that can crash OsmAnd
|
||||
// - crashes on the emulator only!
|
||||
// - works fine on real hardware
|
||||
// - CAN YOU HELP ME FIND/UNDERSTAND IT???
|
||||
// [Note 2]: we only see valid altitude data if the GPX file has 'ele' tags
|
||||
// [Note 3]: we only see valid speed data if the GPX has 'time' tags
|
||||
}
|
||||
|
||||
ts.recalculateRenderScales(view); // rework all renderers as required
|
||||
updatePaints(ts.getColor(cachedColor), g.isRoutePoints(), g.isShowCurrentTrack(), settings, tileBox);
|
||||
ts.drawRenderers(paint, canvas, tileBox); // any renderers now get to draw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -352,47 +400,14 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
|
|||
return pts;
|
||||
}
|
||||
|
||||
private void drawSegments(Canvas canvas, RotatedTileBox tileBox, List<TrkSegment> points) {
|
||||
final QuadRect latLonBounds = tileBox.getLatLonBounds();
|
||||
for (TrkSegment l : points) {
|
||||
int startIndex = -1;
|
||||
int endIndex = -1;
|
||||
int prevCross = 0;
|
||||
double shift = 0;
|
||||
for (int i = 0; i < l.points.size(); i++) {
|
||||
WptPt ls = l.points.get(i);
|
||||
int cross = 0;
|
||||
cross |= (ls.lon < latLonBounds.left - shift ? 1 : 0);
|
||||
cross |= (ls.lon > latLonBounds.right + shift ? 2 : 0);
|
||||
cross |= (ls.lat > latLonBounds.top + shift ? 4 : 0);
|
||||
cross |= (ls.lat < latLonBounds.bottom - shift ? 8 : 0);
|
||||
if (i > 0) {
|
||||
if ((prevCross & cross) == 0) {
|
||||
if (endIndex == i - 1 && startIndex != -1) {
|
||||
// continue previous line
|
||||
} else {
|
||||
// start new segment
|
||||
if (startIndex >= 0) {
|
||||
drawSegment(canvas, tileBox, l, startIndex, endIndex);
|
||||
}
|
||||
startIndex = i - 1;
|
||||
}
|
||||
endIndex = i;
|
||||
}
|
||||
}
|
||||
prevCross = cross;
|
||||
}
|
||||
if (startIndex != -1) {
|
||||
drawSegment(canvas, tileBox, l, startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
ORIGINAL drawSegment below
|
||||
This is now moved to become one of the 'Renderable' types in Renderable.java
|
||||
|
||||
private void drawSegment(Canvas canvas, RotatedTileBox tb, TrkSegment l, int startIndex, int endIndex) {
|
||||
TIntArrayList tx = new TIntArrayList();
|
||||
|
@ -427,7 +442,7 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
|
|||
canvas.rotate(tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
|
||||
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
private boolean calculateBelongs(int ex, int ey, int objx, int objy, int radius) {
|
||||
return (Math.abs(objx - ex) <= radius * 2 && Math.abs(objy - ey) <= radius * 2) ;
|
||||
|
|
480
OsmAnd/src/net/osmand/plus/views/Renderable.java
Normal file
480
OsmAnd/src/net/osmand/plus/views/Renderable.java
Normal file
|
@ -0,0 +1,480 @@
|
|||
package net.osmand.plus.views;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
|
||||
import net.osmand.data.QuadRect;
|
||||
import net.osmand.data.RotatedTileBox;
|
||||
import net.osmand.plus.GPXUtilities;
|
||||
import net.osmand.plus.views.OsmandMapTileView;
|
||||
import net.osmand.util.MapAlgorithms;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import gnu.trove.list.array.TIntArrayList;
|
||||
|
||||
|
||||
public class Renderable {
|
||||
|
||||
// This class handles the actual drawing of segment 'layers'. A segment is a piece of track
|
||||
// (i.e., a list of WptPt) which has renders attached to it. There can be any number of renders
|
||||
// layered upon each other to give multiple effects.
|
||||
|
||||
public enum RenderType {
|
||||
ORIGINAL, // Auto-resizing using Ramer-Douglas-Peucer algorithm
|
||||
DISTANCE, // markers at given distance
|
||||
CONVEYOR, // arrows/direction movers
|
||||
ALTITUDE, // colour-rainbow altitude band
|
||||
SPEED, // colour-rainbow speed band
|
||||
}
|
||||
|
||||
|
||||
static private Timer t = null; // fires a repaint for animating segments
|
||||
static private int conveyor = 0; // single cycler for 'conveyor' style renders
|
||||
static private OsmandMapTileView view = null; // for paint refresh
|
||||
|
||||
|
||||
// If any render wants to have animation, something needs to make a one-off call to 'startScreenRefresh'
|
||||
// to setup a timer to periodically force a screen refresh/redraw
|
||||
|
||||
public static void startScreenRefresh(OsmandMapTileView v, double period) {
|
||||
view = v;
|
||||
if (t==null && v != null) {
|
||||
t = new Timer();
|
||||
t.scheduleAtFixedRate(new TimerTask() {
|
||||
public void run() {
|
||||
conveyor = (conveyor+1)&31; // mask/wrap to avoid boundary issues
|
||||
view.refreshMap();
|
||||
}
|
||||
}, 0, (long) period);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//----------------------------------------------------------------------------------------------
|
||||
|
||||
public static class RenderableSegment {
|
||||
|
||||
|
||||
protected RenderType renderType;
|
||||
protected List<GPXUtilities.WptPt> points = null; // Original list of points
|
||||
protected List<GPXUtilities.WptPt> culled = null; // Reduced/resampled list of points
|
||||
|
||||
double hash;
|
||||
double param1,param2;
|
||||
boolean shadow = false; // TODO: fixup shadow support
|
||||
|
||||
AsynchronousResampler culler = null; // The currently active resampler
|
||||
|
||||
public List<GPXUtilities.WptPt> getPoints() { return points; }
|
||||
|
||||
|
||||
public RenderableSegment(RenderType type, List<GPXUtilities.WptPt> pt, double param1, double param2) {
|
||||
|
||||
hash = 0;
|
||||
culled = null;
|
||||
renderType = type;
|
||||
this.param1 = param1;
|
||||
this.param2 = param2;
|
||||
points = pt;
|
||||
}
|
||||
|
||||
|
||||
// When the asynchronous task has finished, it calls this function to set the 'culled' list
|
||||
public void setRDP(List<GPXUtilities.WptPt> cull) {
|
||||
culled = cull;
|
||||
}
|
||||
|
||||
|
||||
// When there is a zoom change OR the list of points changes, then we want to trigger a
|
||||
// cull of the original point list (for example, Ramer-Douglas-Peucer algorithm or a
|
||||
// simple distance-based resampler. The cull operates asynchronously and results will be
|
||||
// returned into 'culled' via 'setRDP' algorithm (above).
|
||||
|
||||
// Notes:
|
||||
// 1. If a new cull is triggered, then the existing one is immediately discarded
|
||||
// so that we don't 'zoom in' to low-resolution tracks and see poor visuals.
|
||||
// 2. Individual derived classes (altitude, speed, etc) can override this routine to
|
||||
// ensure that the cull only ever happens once.
|
||||
|
||||
public void recalculateRenderScale(OsmandMapTileView view) {
|
||||
|
||||
// Here we create the 'shadow' resampled/culled points list, based on the asynchronous call.
|
||||
// The asynchronous callback will set the variable, and that is used for rendering
|
||||
|
||||
double zoom = view.getZoom();
|
||||
|
||||
if (points != null) {
|
||||
double hashCode = points.hashCode() + zoom;
|
||||
if (culled == null || hash != hashCode) {
|
||||
if (culler != null)
|
||||
culler.cancel(true); // stop any still-running cull
|
||||
hash = hashCode;
|
||||
double cullDistance = Math.pow(2.0,param1-zoom);
|
||||
culler = new AsynchronousResampler(renderType, view, this, cullDistance, param2);
|
||||
culled = null; // effectively use full-resolution until re-cull complete (see [Note 1] below)
|
||||
culler.execute("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void drawSingleSegment(Paint p, Canvas canvas, RotatedTileBox tileBox) {
|
||||
|
||||
List<GPXUtilities.WptPt> pts = culled == null? points: culled; // [Note 1]: use culled points preferentially
|
||||
|
||||
final QuadRect latLonBounds = tileBox.getLatLonBounds();
|
||||
|
||||
int startIndex = -1;
|
||||
int endIndex = -1;
|
||||
int prevCross = 0;
|
||||
double shift = 0;
|
||||
for (int i = 0; i < pts.size(); i++) {
|
||||
GPXUtilities.WptPt ls = pts.get(i);
|
||||
int cross = 0;
|
||||
cross |= (ls.lon < latLonBounds.left - shift ? 1 : 0);
|
||||
cross |= (ls.lon > latLonBounds.right + shift ? 2 : 0);
|
||||
cross |= (ls.lat > latLonBounds.top + shift ? 4 : 0);
|
||||
cross |= (ls.lat < latLonBounds.bottom - shift ? 8 : 0);
|
||||
if (i > 0) {
|
||||
if ((prevCross & cross) == 0) {
|
||||
if (endIndex == i - 1 && startIndex != -1) {
|
||||
// continue previous line
|
||||
} else {
|
||||
// start new segment
|
||||
if (startIndex >= 0) {
|
||||
drawSegment(pts, p, canvas, tileBox, startIndex, endIndex);
|
||||
}
|
||||
startIndex = i - 1;
|
||||
}
|
||||
endIndex = i;
|
||||
}
|
||||
}
|
||||
prevCross = cross;
|
||||
}
|
||||
if (startIndex != -1) {
|
||||
drawSegment(pts, p, canvas, tileBox, startIndex, endIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void drawSegment(List<GPXUtilities.WptPt> pts, Paint paint, Canvas canvas, RotatedTileBox tb, int startIndex, int endIndex) {
|
||||
TIntArrayList tx = new TIntArrayList();
|
||||
TIntArrayList ty = new TIntArrayList();
|
||||
canvas.rotate(-tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
|
||||
Path path = new Path();
|
||||
for (int i = startIndex; i <= endIndex; i++) {
|
||||
GPXUtilities.WptPt p = pts.get(i);
|
||||
tx.add((int)(tb.getPixXFromLatLon(p.lat, p.lon) + 0.5));
|
||||
ty.add((int)(tb.getPixYFromLatLon(p.lat, p.lon) + 0.5));
|
||||
}
|
||||
|
||||
//TODO: colour
|
||||
calculatePath(tb, tx, ty, path);
|
||||
|
||||
if (shadow) {
|
||||
float sw = paint.getStrokeWidth();
|
||||
int col = paint.getColor();
|
||||
paint.setColor(Color.BLACK);
|
||||
paint.setStrokeWidth(sw + 4);
|
||||
canvas.drawPath(path, paint);
|
||||
paint.setStrokeWidth(sw);
|
||||
paint.setColor(col);
|
||||
canvas.drawPath(path, paint);
|
||||
} else
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
|
||||
canvas.rotate(tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
|
||||
}
|
||||
|
||||
public int calculatePath(RotatedTileBox tb, TIntArrayList xs, TIntArrayList ys, Path path) {
|
||||
boolean start = false;
|
||||
int px = xs.get(0);
|
||||
int py = ys.get(0);
|
||||
int h = tb.getPixHeight();
|
||||
int w = tb.getPixWidth();
|
||||
int cnt = 0;
|
||||
boolean pin = isIn(px, py, w, h);
|
||||
for(int i = 1; i < xs.size(); i++) {
|
||||
int x = xs.get(i);
|
||||
int y = ys.get(i);
|
||||
boolean in = isIn(x, y, w, h);
|
||||
boolean draw = false;
|
||||
if(pin && in) {
|
||||
draw = true;
|
||||
} else {
|
||||
long intersection = MapAlgorithms.calculateIntersection(x, y,
|
||||
px, py, 0, w, h, 0);
|
||||
if (intersection != -1) {
|
||||
px = (int) (intersection >> 32);
|
||||
py = (int) (intersection & 0xffffffff);
|
||||
draw = true;
|
||||
}
|
||||
}
|
||||
if (draw) {
|
||||
if (!start) {
|
||||
cnt++;
|
||||
path.moveTo(px, py);
|
||||
}
|
||||
path.lineTo(x, y);
|
||||
start = true;
|
||||
} else{
|
||||
start = false;
|
||||
}
|
||||
pin = in;
|
||||
px = x;
|
||||
py = y;
|
||||
}
|
||||
return cnt;
|
||||
}
|
||||
|
||||
|
||||
protected boolean isIn(float x, float y, float rx, float by) {
|
||||
return x >= 0f && x <= rx && y >= 0f && y <= by;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------
|
||||
|
||||
public static class RenderableAltitude extends RenderableSegment {
|
||||
|
||||
private Paint alphaPaint = null;
|
||||
private int alpha;
|
||||
protected float colorBandWidth; // width of speed/altitude colour band
|
||||
|
||||
public RenderableAltitude(RenderType type, List<GPXUtilities.WptPt> pt, double param1, double param2) {
|
||||
super(type, pt, param1, param2);
|
||||
|
||||
alpha = (int)param2;
|
||||
alphaPaint = new Paint();
|
||||
alphaPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
colorBandWidth = 3.0f;
|
||||
}
|
||||
|
||||
@Override public void recalculateRenderScale(OsmandMapTileView view) {
|
||||
if (culler == null && culled == null) {
|
||||
culler = new AsynchronousResampler(renderType, view, this, param1, param2);
|
||||
culler.execute("");
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void drawSingleSegment(Paint p, Canvas canvas, RotatedTileBox tileBox) {
|
||||
|
||||
if (culled != null && culled.size() > 0) {
|
||||
|
||||
// Draws into a bitmap so that the lines can be drawn solid and the *ENTIRE* can be alpha-blended
|
||||
|
||||
Bitmap newBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas2 = new Canvas(newBitmap);
|
||||
canvas2.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
|
||||
|
||||
alphaPaint.setAlpha(255);
|
||||
alphaPaint.setStrokeWidth(p.getStrokeWidth()*colorBandWidth);
|
||||
|
||||
|
||||
float lastx = Float.NEGATIVE_INFINITY;
|
||||
float lasty = 0;
|
||||
|
||||
for (GPXUtilities.WptPt pt : culled) {
|
||||
float x = tileBox.getPixXFromLatLon(pt.lat, pt.lon);
|
||||
float y = tileBox.getPixYFromLatLon(pt.lat, pt.lon);
|
||||
|
||||
if (lasty != Float.NEGATIVE_INFINITY) {
|
||||
alphaPaint.setColor(pt.colourARGB);
|
||||
canvas2.drawLine(lastx, lasty, x, y, alphaPaint);
|
||||
}
|
||||
|
||||
lastx = x;
|
||||
lasty = y;
|
||||
}
|
||||
canvas2.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
|
||||
alphaPaint.setAlpha(alpha);
|
||||
canvas.drawBitmap(newBitmap, 0, 0, alphaPaint);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------
|
||||
|
||||
public static class RenderableSpeed extends RenderableAltitude {
|
||||
public RenderableSpeed(RenderType type, List<GPXUtilities.WptPt> pt, double param1, double param2) {
|
||||
super(type, pt, param1, param2);
|
||||
colorBandWidth = 3f;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------
|
||||
|
||||
public static class RenderableConveyor extends RenderableSegment {
|
||||
|
||||
private double zoom = 0;
|
||||
|
||||
public RenderableConveyor(RenderType type, List<GPXUtilities.WptPt> pt, double param1, double param2) {
|
||||
super(type, pt, param1, param2);
|
||||
}
|
||||
|
||||
@Override public void recalculateRenderScale(OsmandMapTileView view) {
|
||||
this.zoom = zoom;
|
||||
if (culled == null && culler == null) { // i.e., do NOT resample when scaling - only allow a one-off generation
|
||||
culler = new AsynchronousResampler(renderType, view, this, param1, param2);
|
||||
culler.execute("");
|
||||
}
|
||||
}
|
||||
|
||||
public int getComplementaryColor(int colorToInvert) {
|
||||
float[] hsv = new float[3];
|
||||
Color.RGBToHSV(Color.red(colorToInvert), Color.green(colorToInvert), Color.blue(colorToInvert), hsv);
|
||||
hsv[0] = (hsv[0] + 180) % 360;
|
||||
return Color.HSVToColor(hsv);
|
||||
}
|
||||
|
||||
@Override public void drawSingleSegment(Paint p, Canvas canvas, RotatedTileBox tileBox) {
|
||||
|
||||
// This is a simple/experimental track subsegment 'conveyor' animator just to show how
|
||||
// effects of constant segment-size can be used for animation effects. I've put an arrowhead
|
||||
// in just to show what can be done. Very hacky, it's just a "hey look at this".
|
||||
|
||||
if (culled == null)
|
||||
return;
|
||||
|
||||
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
|
||||
|
||||
int pCol = p.getColor();
|
||||
float pSw = p.getStrokeWidth();
|
||||
|
||||
//p.setStrokeWidth(pSw * 2f); // use a thicker line
|
||||
p.setColor(getComplementaryColor(p.getColor())); // and a complementary colour
|
||||
|
||||
float lastx = Float.NEGATIVE_INFINITY;
|
||||
float lasty = Float.NEGATIVE_INFINITY;
|
||||
Path path = new Path();
|
||||
|
||||
int h = tileBox.getPixHeight();
|
||||
int w = tileBox.getPixWidth();
|
||||
boolean broken = true;
|
||||
int intp = conveyor; // the segment cycler
|
||||
for (GPXUtilities.WptPt pt : culled) {
|
||||
intp--; // increment to go the other way!
|
||||
|
||||
if ((intp & 7) < 3) {
|
||||
|
||||
float x = tileBox.getPixXFromLatLon(pt.lat, pt.lon);
|
||||
float y = tileBox.getPixYFromLatLon(pt.lat, pt.lon);
|
||||
|
||||
if ((isIn(x, y, w, h) || isIn(lastx, lasty, w, h))) {
|
||||
if (broken) {
|
||||
path.moveTo(x, y);
|
||||
broken = false;
|
||||
} else
|
||||
path.lineTo(x, y);
|
||||
lastx = x;
|
||||
lasty = y;
|
||||
} else
|
||||
broken = true;
|
||||
} else
|
||||
broken = true;
|
||||
|
||||
}
|
||||
|
||||
canvas.drawPath(path, p);
|
||||
canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
|
||||
|
||||
p.setStrokeWidth(pSw);
|
||||
p.setColor(pCol);
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------------------------
|
||||
|
||||
public static class RenderableDot extends RenderableSegment {
|
||||
|
||||
float dotScale;
|
||||
|
||||
public RenderableDot(RenderType type, List<GPXUtilities.WptPt> pt, double param1, double param2) {
|
||||
super(type, pt, param1, param2);
|
||||
|
||||
dotScale = (float) param2;
|
||||
}
|
||||
|
||||
@Override public void recalculateRenderScale(OsmandMapTileView view) {
|
||||
if (culled == null && culler == null) {
|
||||
culler = new AsynchronousResampler(renderType, view, this, param1, param2);
|
||||
assert (culler != null);
|
||||
if (culler != null)
|
||||
culler.execute("");
|
||||
}
|
||||
}
|
||||
|
||||
private String getLabel(double value) {
|
||||
String lab;
|
||||
lab = String.format("%.2f km",value/1000.);
|
||||
return lab;
|
||||
}
|
||||
|
||||
@Override public void drawSingleSegment(Paint p, Canvas canvas, RotatedTileBox tileBox) {
|
||||
|
||||
assert p != null;
|
||||
assert canvas != null;
|
||||
assert tileBox != null;
|
||||
|
||||
try {
|
||||
|
||||
if (culled == null)
|
||||
return;
|
||||
|
||||
Paint px = new Paint();
|
||||
assert (px != null);
|
||||
|
||||
px.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
|
||||
|
||||
float sw = p.getStrokeWidth();
|
||||
float ds = sw * dotScale;
|
||||
|
||||
int w = tileBox.getPixWidth();
|
||||
int h = tileBox.getPixHeight();
|
||||
|
||||
for (GPXUtilities.WptPt pt : culled) {
|
||||
|
||||
float x = tileBox.getPixXFromLatLon(pt.lat, pt.lon);
|
||||
float y = tileBox.getPixYFromLatLon(pt.lat, pt.lon);
|
||||
|
||||
if (isIn(x, y, w, h)) {
|
||||
|
||||
px.setColor(0xFF000000);
|
||||
px.setStrokeWidth(ds + 2);
|
||||
canvas.drawPoint(x, y, px);
|
||||
px.setStrokeWidth(ds);
|
||||
px.setColor(0xFFFFFFFF);
|
||||
canvas.drawPoint(x, y, px);
|
||||
|
||||
//TODO: I do not know how to correctly handle screen density!
|
||||
//TODO: modify the text size based on density calculations!!
|
||||
|
||||
if (sw > 6) {
|
||||
px.setColor(Color.BLACK);
|
||||
px.setStrokeWidth(1);
|
||||
px.setTextSize(sw*2f); //<<< TODO fix
|
||||
canvas.drawText(getLabel(pt.getDistance()), x + ds / 2, y + ds / 2, px);
|
||||
}
|
||||
}
|
||||
canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String exception = e.getMessage();
|
||||
Throwable cause = e.getCause();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue