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:
Andrew Davie 2016-03-28 23:40:30 +11:00
parent ce2f2eceee
commit 109ca692f1
4 changed files with 898 additions and 44 deletions

View file

@ -2,12 +2,17 @@
package net.osmand.plus; package net.osmand.plus;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint;
import net.osmand.Location; import net.osmand.Location;
import net.osmand.PlatformUtil; import net.osmand.PlatformUtil;
import net.osmand.data.LocationPoint; import net.osmand.data.LocationPoint;
import net.osmand.data.PointDescription; 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 net.osmand.util.Algorithms;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
@ -98,10 +103,20 @@ public class GPXUtilities {
public double speed = 0; public double speed = 0;
public double hdop = Double.NaN; public double hdop = Double.NaN;
public boolean deleted = false; 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 WptPt() {
} }
public void setDistance(double dist) {
distance = dist;
}
public double getDistance() {
return distance;
}
@Override @Override
public int getColor() { public int getColor() {
return getColor(0); return getColor(0);
@ -166,6 +181,17 @@ public class GPXUtilities {
public static class TrkSegment extends GPXExtensions { public static class TrkSegment extends GPXExtensions {
public List<WptPt> points = new ArrayList<WptPt>(); 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) { public List<GPXTrackAnalysis> splitByDistance(double meters) {
return split(getDistanceMetric(), getTimeSplit(), meters); return split(getDistanceMetric(), getTimeSplit(), meters);
@ -181,6 +207,57 @@ public class GPXUtilities {
return convert(splitSegments); 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 { public static class Track extends GPXExtensions {

View 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
}
}
}

View file

@ -207,7 +207,8 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) {
if(points != null) { if(points != null) {
updatePaints(0, false, false, settings, tileBox); updatePaints(0, false, false, settings, tileBox);
drawSegments(canvas, tileBox, points); for (TrkSegment ts : points)
ts.drawRenderers(paint, canvas,tileBox);
} else { } else {
List<SelectedGpxFile> selectedGPXFiles = selectedGpxHelper.getSelectedGPXFiles(); List<SelectedGpxFile> selectedGPXFiles = selectedGpxHelper.getSelectedGPXFiles();
cache.clear(); cache.clear();
@ -326,10 +327,57 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
private void drawSelectedFilesSegments(Canvas canvas, RotatedTileBox tileBox, private void drawSelectedFilesSegments(Canvas canvas, RotatedTileBox tileBox,
List<SelectedGpxFile> selectedGPXFiles, DrawSettings settings) { List<SelectedGpxFile> selectedGPXFiles, DrawSettings settings) {
for (SelectedGpxFile g : selectedGPXFiles) { for (SelectedGpxFile g : selectedGPXFiles) {
List<TrkSegment> points = g.getPointsToDisplay(); List<TrkSegment> segments = g.getPointsToDisplay();
boolean routePoints = g.isRoutePoints(); for (TrkSegment ts : segments) {
updatePaints(g.getColor(), routePoints, g.isShowCurrentTrack(), settings, tileBox);
drawSegments(canvas, tileBox, points); // 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; 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 @Override
public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { 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) { private void drawSegment(Canvas canvas, RotatedTileBox tb, TrkSegment l, int startIndex, int endIndex) {
TIntArrayList tx = new TIntArrayList(); TIntArrayList tx = new TIntArrayList();
@ -427,7 +442,7 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
canvas.rotate(tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY()); canvas.rotate(tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
} }
*/
private boolean calculateBelongs(int ex, int ey, int objx, int objy, int radius) { 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) ; return (Math.abs(objx - ex) <= radius * 2 && Math.abs(objy - ey) <= radius * 2) ;

View 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();
}
}
}
}