"Trivial" area bounds for segments for super-quick culling during draw.

Reworked all the renderers to be much more object oriented.
Got the sample arrow and conveyor renderers working
Tracked down a few bugs - particularly one still there - TrkSegment with 0 points!
This in my opinion should never happen - it's coming in from "outside" to my code.
This commit is contained in:
Andrew Davie 2016-03-30 01:47:04 +11:00
parent c8a245d031
commit 63ff22b6ab
6 changed files with 267 additions and 313 deletions

View file

@ -41,6 +41,10 @@ public class QuadRect {
return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom;
}
public static boolean trivialOverlap(QuadRect a, QuadRect b) {
return !((a.right < b.left) || (a.left > b.right) || (a.top < b.bottom) || (a.bottom > b.top));
}
public double centerX() {
return (left + right) / 2;
}

View file

@ -570,4 +570,23 @@ public class Algorithms {
}
return hexString;
}
public static int getRainbowColor(double percent) {
// 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)));
switch (X) {
case 0: return 0xFFFF0000 + (Y<<8);
case 1: return 0xFF00FF00 + ((255-Y)<<16);
case 2: return 0xFF00FF00 + Y;
case 3: return 0xFF0000FF + ((255-Y)<<8);
case 4: return 0xFF0000FF + (Y << 16);
}
return 0xFFFF00FF;
}
}

View file

@ -207,54 +207,16 @@ 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_MARKERS: // a resample every N metres draw
rs = new Renderable.DistanceMarker(type, points, param1, param2);
break;
case CONVEYOR: // an animating segment draw
rs = new Renderable.Conveyor(type, points, param1, param2);
Renderable.startScreenRefresh(view, (long) param2);
break;
case ALTITUDE: // a colour-banded altitude draw
rs = new Renderable.AltitudeColours(type, points, param1, param2);
break;
case SPEED: // a colour-banded speed draw
rs = new Renderable.SpeedColours(type, points, param1, param2);
break;
default:
break;
public void recalculateRenderScales(double zoom) {
for (Renderable.RenderableSegment rs : renders) {
rs.recalculateRenderScale(zoom);
}
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)
for (Renderable.RenderableSegment rs : renders) {
rs.drawSingleSegment(p, c, tb);
}
}

View file

@ -3,6 +3,7 @@ package net.osmand.plus.views;
import android.os.AsyncTask;
import net.osmand.plus.GPXUtilities;
import net.osmand.util.Algorithms;
import net.osmand.util.MapUtils;
import java.util.ArrayList;
@ -20,29 +21,11 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
@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())
if (rs != null && result.equals("OK") && !isCancelled()) {
rs.setRDP(culled);
}
protected int getRainbowColour(double percent) {
// 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)));
switch (X) {
case 0: return 0xFFFF0000 + (Y<<8);
case 1: return 0xFF00FF00 + ((255-Y)<<16);
case 2: return 0xFF00FF00 + Y;
case 3: return 0xFF0000FF + ((255-Y)<<8);
case 4: return 0xFF0000FF + (Y << 16);
}
return 0xFFFF00FF;
}
// 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
@ -52,18 +35,19 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
protected List<GPXUtilities.WptPt> resampleTrack(List<GPXUtilities.WptPt> pts, double dist) {
ArrayList<GPXUtilities.WptPt> newPts = new ArrayList<GPXUtilities.WptPt>();
ArrayList<GPXUtilities.WptPt> newPts = new ArrayList<>();
int ptCt = pts.size();
if (pts != null && ptCt > 0) {
if (ptCt > 0) {
GPXUtilities.WptPt lastPt = pts.get(0);
double segSub = 0;
double cumDist = 0;
for (int i = 1; i < ptCt; i++) {
if (isCancelled())
if (isCancelled()) {
return null;
}
GPXUtilities.WptPt pt = pts.get(i);
double segLength = MapUtils.getDistance(pt.getLatitude(), pt.getLongitude(), lastPt.getLatitude(), lastPt.getLongitude());
@ -116,7 +100,7 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
culled = resampleTrack(rs.getPoints(), segmentSize);
if (!isCancelled()) {
int halfC = getRainbowColour(0.5); // default coloration if no elevations found
int halfC = Algorithms.getRainbowColor(0.5);
// Calculate the absolutes of the altitude variations
Double max = Double.NEGATIVE_INFINITY;
@ -124,13 +108,13 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
for (GPXUtilities.WptPt pt : culled) {
max = Math.max(max, pt.ele);
min = Math.min(min, pt.ele);
pt.colourARGB = halfC;
pt.colourARGB = halfC; // default, in case there are no 'ele' in GPX
}
Double elevationRange = max - min;
if (elevationRange > 0)
for (GPXUtilities.WptPt pt : culled)
pt.colourARGB = getRainbowColour((pt.ele - min) / elevationRange);
pt.colourARGB = Algorithms.getRainbowColor((pt.ele - min) / elevationRange);
}
return isCancelled() ? "" : "OK";
@ -163,28 +147,31 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
for (int i = 1; i < points.size(); i++) {
GPXUtilities.WptPt pt = points.get(i);
double delta = pt.time - lastPt.time;
if (delta > 0)
if (delta > 0) {
pt.speed = MapUtils.getDistance(pt.getLatitude(), pt.getLongitude(),
lastPt.getLatitude(), lastPt.getLongitude()) / delta;
else
} else {
pt.speed = 0; // GPX doesn't have time - this is OK, colour will be mid-range for whole track
}
lastPt = pt;
}
int halfC = Algorithms.getRainbowColor(0.5);
// Calculate the absolutes of the speed variations
Double max = Double.NEGATIVE_INFINITY;
Double min = Double.POSITIVE_INFINITY;
for (GPXUtilities.WptPt pt : points) {
max = Math.max(max, pt.speed);
min = Math.min(min, pt.speed);
pt.colourARGB = getRainbowColour(0.5);
pt.colourARGB = halfC; // default, in case there are no 'time' in GPX
}
Double range = max - min;
if (range > 0)
Double speedRange = max - min;
if (speedRange > 0) {
for (GPXUtilities.WptPt pt : points)
pt.colourARGB = getRainbowColour((pt.speed - min) / range);
pt.colourARGB = Algorithms.getRainbowColor((pt.speed - min) / speedRange);
}
}
return isCancelled() ? "" : "OK";
}
}
@ -221,9 +208,7 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
@Override protected String doInBackground(String... params) {
// 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.
// Reduce the point-count of the GPX track using Ramer-Douglas-Peucker algorithm.
points = rs.getPoints();
culled = new ArrayList<>();
@ -235,8 +220,9 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
if (!isCancelled()) {
survivor[0] = true;
for (int i = 0; i < survivor.length; i++)
if (survivor[i])
if (survivor[i]) {
culled.add(points.get(i));
}
}
}
return isCancelled() ? "" : "OK";
@ -248,8 +234,9 @@ public abstract class AsynchronousResampler extends AsyncTask<String,Integer,Str
int index = -1;
for (int i = start + 1; i < end; i++) {
if (isCancelled())
if (isCancelled()) {
return;
}
double d = MapUtils.getOrthogonalDistance(
points.get(i).getLatitude(), points.get(i).getLongitude(),

View file

@ -327,54 +327,24 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
List<TrkSegment> segments = g.getPointsToDisplay();
for (TrkSegment ts : segments) {
// TODO: Select/install renderables via UI or external process! (See comments below)
if (ts.renders.size()==0 // only do once (CODE HERE NEEDS TO BE UI INSTEAD)
&& !ts.points.isEmpty()) // hmmm. 0-point tracks happen, but.... how?
{
// 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.renders.add(new Renderable.StandardTrack(ts.points, 17)); // the base line (distance modifier)
ts.renders.add(new Renderable.Altitude(ts.points, 50, 128));
ts.renders.add(new Renderable.StandardTrack(ts.points, 17));
// 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.renders.add(new Renderable.AltitudeColours(ts.points, 50, 128));
ts.renders.add(new Renderable.Conveyor(ts.points, view, 20, 250));
ts.renders.add(new Renderable.DistanceMarker(ts.points, 1000, 1));
//ts.renders.add(new Renderable.SpeedColours(ts.points, 50, 128));
// [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.renders.add(new Renderable.Conveyor(ts.points, view, 20, 250));
ts.renders.add(new Renderable.DistanceMarker(ts.points, view, 1000));
//ts.renders.add(new Renderable.Speed(ts.points, 50, 128));
ts.renders.add(new Renderable.Arrows(ts.points,view,10,250));
}
ts.recalculateRenderScales(view); // rework all renderers as required
ts.recalculateRenderScales(view.getZoom());
updatePaints(ts.getColor(cachedColor), g.isRoutePoints(), g.isShowCurrentTrack(), settings, tileBox);
ts.drawRenderers(paint, canvas, tileBox); // any renderers now get to draw
ts.drawRenderers(paint, canvas, tileBox);
}
}
}
@ -402,45 +372,6 @@ public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContex
@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();
TIntArrayList ty = new TIntArrayList();
canvas.rotate(-tb.getRotate(), tb.getCenterPixelX(), tb.getCenterPixelY());
Path path = new Path();
for (int i = startIndex; i <= endIndex; i++) {
WptPt p = l.points.get(i);
int x = (int) tb.getPixXFromLatLon(p.lat, p.lon);
int y = (int) tb.getPixYFromLatLon(p.lat, p.lon);
// int x = tb.getPixXFromLonNoRot(p.lon);
// int y = tb.getPixYFromLatNoRot(p.lat);
tx.add(x);
ty.add(y);
}
calculatePath(tb, tx, ty, path);
if(isPaint_1) {
canvas.drawPath(path, paint_1);
}
if(isShadowPaint) {
canvas.drawPath(path, shadowPaint);
}
int clr = paint.getColor();
if(clr != l.getColor(clr) && l.getColor(clr) != 0) {
paint.setColor(l.getColor(clr));
}
canvas.drawPath(path, paint);
paint.setColor(clr);
if(isPaint2) {
canvas.drawPath(path, paint2);
}
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) ;

View file

@ -15,6 +15,7 @@ import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import gnu.trove.list.array.TIntArrayList;
@ -53,25 +54,47 @@ public class Renderable {
protected List<GPXUtilities.WptPt> points = null; // Original list of points
protected List<GPXUtilities.WptPt> culled = null; // Reduced/resampled list of points
double hash;
protected QuadRect trackBounds = new QuadRect(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY,
Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY);
double hashZoom;
double hashPoint;
boolean shadow = false; // TODO: fixup shadow support
AsynchronousResampler culler = null; // The currently active resampler
public RenderableSegment(List<GPXUtilities.WptPt> pt) {
points = pt;
hash = 0;
hashPoint = points.hashCode();
hashZoom = 0;
culled = null;
}
public void recalculateRenderScale(OsmandMapTileView view) {}
public void recalculateRenderScale(double zoom) {}
public void drawSingleSegment(Paint p, Canvas canvas, RotatedTileBox tileBox) {}
// When the asynchronous task has finished, it calls this function to set the 'culled' list
public void setRDP(List<GPXUtilities.WptPt> cull) {
culled = cull;
if (view != null)
// Find the segment's bounding box, to allow quick draw rejection later
trackBounds.left = trackBounds.bottom = Double.POSITIVE_INFINITY;
trackBounds.right = trackBounds.top = Double.NEGATIVE_INFINITY;
for (GPXUtilities.WptPt pt : culled) {
trackBounds.right = Math.max(trackBounds.right, pt.lon);
trackBounds.left = Math.min(trackBounds.left, pt.lon);
trackBounds.top = Math.max(trackBounds.top, pt.lat);
trackBounds.bottom = Math.min(trackBounds.bottom, pt.lat);
}
if (view != null) {
view.refreshMap(); // force a redraw
}
}
public List<GPXUtilities.WptPt> getPoints() {
@ -95,7 +118,7 @@ public class Renderable {
}
// When there is a zoom change OR the list of points changes, then we want to trigger a
// When there is a zoom change, 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).
@ -106,24 +129,34 @@ public class Renderable {
// 2. Individual derived classes (altitude, speed, etc) can override this routine to
// ensure that the cull only ever happens once.
@Override public void recalculateRenderScale(OsmandMapTileView view) {
@Override public void recalculateRenderScale(double zoom) {
// 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
// The asynchronous callback will set the variable, and that is preferentially used for rendering
double zoom = view.getZoom();
double hashCode = points.hashCode() ;
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,base-zoom);
culler = new AsynchronousResampler.RamerDouglasPeucer(this, cullDistance);
culled = null; // effectively use full-resolution until re-cull complete (see [Note 1] below)
culler.execute("");
if (hashPoint != hashCode) { // current track, changing?
if (culler != null) {
culler.cancel(true); // STOP culling a track with changing points
culled = null; // and force use of original track
}
} else if (culler == null || hashZoom != zoom) {
hashZoom = zoom;
if (culler != null) {
culler.cancel(true);
}
double cullDistance = Math.pow(2.0, base - zoom);
culler = new AsynchronousResampler.RamerDouglasPeucer(this, cullDistance);
culled = null; // use full-resolution until re-cull complete (see [Note 1] below)
culler.execute("");
// The trackBounds may be slightly inaccurate (unlikely, but...) so let's reset it
//trackBounds.left = trackBounds.bottom = Double.POSITIVE_INFINITY;
//trackBounds.right = trackBounds.bottom = Double.NEGATIVE_INFINITY;
}
}
@ -132,7 +165,10 @@ public class Renderable {
List<GPXUtilities.WptPt> pts = culled == null? points: culled; // [Note 1]: use culled points preferentially
final QuadRect latLonBounds = tileBox.getLatLonBounds();
QuadRect latLonBounds = tileBox.getLatLonBounds();
if (!QuadRect.trivialOverlap(latLonBounds, trackBounds)) {
return; // Not visible
}
int startIndex = -1;
int endIndex = -1;
@ -147,10 +183,7 @@ public class Renderable {
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 (endIndex != i - 1 || startIndex == -1) {
if (startIndex >= 0) {
drawSegment(pts, p, canvas, tileBox, startIndex, endIndex);
}
@ -178,10 +211,9 @@ public class Renderable {
ty.add((int)(tb.getPixYFromLatLon(p.lat, p.lon) + 0.5));
}
//TODO: colour
calculatePath(tb, tx, ty, path);
if (shadow) {
if (shadow) { // needs work, but let's leave it like this for now
float sw = paint.getStrokeWidth();
int col = paint.getColor();
paint.setColor(Color.BLACK);
@ -217,7 +249,7 @@ public class Renderable {
px, py, 0, w, h, 0);
if (intersection != -1) {
px = (int) (intersection >> 32);
py = (int) (intersection & 0xffffffff);
py = (int) (intersection & 0xffffffff); //TODO: Surely this is just intersection...!
draw = true;
}
}
@ -241,37 +273,34 @@ public class Renderable {
//----------------------------------------------------------------------------------------------
public static class AltitudeColours extends RenderableSegment {
public static class Altitude extends RenderableSegment {
private Paint alphaPaint = null;
private int alpha;
protected float colorBandWidth; // width of speed/altitude colour band
protected double segmentSize;
public AltitudeColours(List<GPXUtilities.WptPt> pt, double segmentSize, int alpha) {
public Altitude(List<GPXUtilities.WptPt> pt, double segmentSize, int alpha) {
super(pt);
this.segmentSize = segmentSize;
this.alpha = alpha;
alphaPaint = new Paint();
alphaPaint.setStrokeCap(Paint.Cap.BUTT);
colorBandWidth = 3.0f;
alphaPaint.setStrokeCap(Paint.Cap.ROUND);
colorBandWidth = 16f;
}
public double getSegmentSize() {
return segmentSize;
}
@Override public void recalculateRenderScale(OsmandMapTileView view) {
if (culler == null && culled == null) {
culler = new AsynchronousResampler.ResampleAltitude(this, segmentSize);
@Override public void recalculateRenderScale(double zoom) {
if (culled == null && culler == null) {
culler = new AsynchronousResampler.ResampleAltitude(this, segmentSize); // once only!
culler.execute("");
}
}
@Override public void drawSingleSegment(Paint p, Canvas canvas, RotatedTileBox tileBox) {
if (culled != null && culled.size() > 0) {
if (culled != null && !culled.isEmpty()
&& QuadRect.trivialOverlap(tileBox.getLatLonBounds(), trackBounds)) {
// Draws into a bitmap so that the lines can be drawn solid and the *ENTIRE* can be alpha-blended
@ -280,7 +309,7 @@ public class Renderable {
canvas2.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
alphaPaint.setAlpha(255);
alphaPaint.setStrokeWidth(p.getStrokeWidth()*colorBandWidth);
alphaPaint.setStrokeWidth(p.getStrokeWidth()*4.0f); // colorBandWidth
float lastx = Float.NEGATIVE_INFINITY;
@ -303,20 +332,19 @@ public class Renderable {
canvas.drawBitmap(newBitmap, 0, 0, alphaPaint);
}
}
}
//----------------------------------------------------------------------------------------------
public static class SpeedColours extends AltitudeColours {
public static class SpeedColours extends Altitude {
public SpeedColours(List<GPXUtilities.WptPt> pt, double segmentSize, int alpha) {
super(pt, segmentSize, alpha);
}
@Override public void recalculateRenderScale(OsmandMapTileView view) {
if (culler == null && culled == null) {
culler = new AsynchronousResampler.ResampleSpeed(this, segmentSize);
@Override public void recalculateRenderScale(double zoom) {
if (culled == null && culler == null) {
culler = new AsynchronousResampler.ResampleSpeed(this, segmentSize); // once only!
culler.execute("");
}
}
@ -335,14 +363,14 @@ public class Renderable {
Renderable.startScreenRefresh(view, refreshRate);
}
@Override public void recalculateRenderScale(OsmandMapTileView view) {
if (culled == null && culler == null) { // i.e., do NOT resample when scaling - only allow a one-off generation
culler = new AsynchronousResampler.GenericResampler(this, segmentSize);
@Override public void recalculateRenderScale(double zoom) {
if (culled == null && culler == null) {
culler = new AsynchronousResampler.GenericResampler(this, segmentSize); // once only!
culler.execute("");
}
}
public int getComplementaryColor(int colorToInvert) {
private 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;
@ -355,104 +383,110 @@ public class Renderable {
// 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;
if (culled != null && !culled.isEmpty()
/* && !QuadRect.trivialOverlap(tileBox.getLatLonBounds(), trackBounds)*/ ) {
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
int pCol = p.getColor();
float pSw = p.getStrokeWidth();
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
//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();
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!
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) {
if ((intp & 7) < 3) {
float x = tileBox.getPixXFromLatLon(pt.lat, pt.lon);
float y = tileBox.getPixYFromLatLon(pt.lat, pt.lon);
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
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;
} else
broken = true;
}
}
canvas.drawPath(path, p);
canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
p.setStrokeWidth(pSw);
p.setColor(pCol);
}
canvas.drawPath(path, p);
canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
p.setStrokeWidth(pSw);
p.setColor(pCol);
}
}
//----------------------------------------------------------------------------------------------
public static class DistanceMarker extends RenderableSegment {
// EXPERIMENTAL!
private float dotScale;
private double segmentSize;
private OsmandMapTileView view;
public DistanceMarker(List<GPXUtilities.WptPt> pt, double segmentSize, float dotScale) {
public DistanceMarker(List<GPXUtilities.WptPt> pt, OsmandMapTileView view, double segmentSize) {
super(pt);
this.dotScale = dotScale;
this.view = view;
this.dotScale = view.getScaleCoefficient();
this.segmentSize = segmentSize;
}
@Override public void recalculateRenderScale(OsmandMapTileView view) {
@Override public void recalculateRenderScale(double zoom) {
if (culled == null && culler == null) {
culler = new AsynchronousResampler.GenericResampler(this, segmentSize);
assert (culler != null);
if (culler != null)
culler.execute("");
culler.execute("");
}
}
private String getLabel(double value) {
String lab;
lab = String.format("%.2f km",value/1000.);
int dig2 = (int)(value / 10);
if ((dig2%10)==0)
lab = String.format("%d km",value/100);
else
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;
if (culled != null && !culled.isEmpty()
&& !QuadRect.trivialOverlap(tileBox.getLatLonBounds(), trackBounds)) {
Paint px = new Paint();
assert (px != null);
px.setStrokeCap(Paint.Cap.ROUND);
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
// rubbish... trying to understand screen scaling/density...
float viewScale = view.getScaleCoefficient()/4.0f; // now "1" for emulator sizing
float density = view.getDensity();
float ds = 160 / viewScale; // "10pt"
float sw = p.getStrokeWidth();
float ds = sw * dotScale;
int w = tileBox.getPixWidth();
int h = tileBox.getPixHeight();
@ -465,28 +499,21 @@ public class Renderable {
if (isIn(x, y, w, h)) {
px.setColor(0xFF000000);
px.setStrokeWidth(ds + 2);
px.setStrokeWidth(sw + 4);
canvas.drawPoint(x, y, px);
px.setStrokeWidth(ds);
px.setStrokeWidth(sw+2);
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) {
if (view.getZoom()>11) {
px.setColor(Color.BLACK);
px.setStrokeWidth(1);
px.setTextSize(sw*2f); //<<< TODO fix
px.setTextSize(ds);
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();
}
}
}
@ -494,8 +521,10 @@ public class Renderable {
//----------------------------------------------------------------------------------------------
public static class Arrows extends RenderableSegment {
// EXPERIMENTAL!
private double segmentSize;
private double zoom;
public Arrows(List<GPXUtilities.WptPt> pt, OsmandMapTileView view, double segmentSize, long refreshRate) {
super(pt);
@ -504,9 +533,10 @@ public class Renderable {
Renderable.startScreenRefresh(view, refreshRate);
}
@Override public void recalculateRenderScale(OsmandMapTileView view) {
if (culled == null && culler == null) { // i.e., do NOT resample when scaling - only allow a one-off generation
culler = new AsynchronousResampler.GenericResampler(this, segmentSize);
@Override public void recalculateRenderScale(double zoom) {
this.zoom = zoom;
if (culled == null && culler == null) {
culler = new AsynchronousResampler.GenericResampler(this, segmentSize); // once
culler.execute("");
}
}
@ -520,57 +550,78 @@ public class Renderable {
@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 && !culled.isEmpty()
/*&& !QuadRect.trivialOverlap(tileBox.getLatLonBounds(), trackBounds)*/) {
if (culled == null)
return;
// This is all very hacky and experimental code. Just showing how to do an animating segmented
// line to draw arrows in the direction of movement.
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
int pCol = p.getColor();
float pSw = p.getStrokeWidth();
float sizer = (float) Math.pow(2.0,zoom-18) * 512;
int pCol = p.getColor();
//p.setStrokeWidth(pSw * 2f); // use a thicker line
p.setColor(getComplementaryColor(p.getColor())); // and a complementary colour
p.setColor(getComplementaryColor(p.getColor())); // and a complementary colour
float lastx = Float.NEGATIVE_INFINITY;
float lasty = Float.NEGATIVE_INFINITY;
Path path = new Path();
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!
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) {
if ((intp & 15) < 8) {
float x = tileBox.getPixXFromLatLon(pt.lat, pt.lon);
float y = tileBox.getPixYFromLatLon(pt.lat, pt.lon);
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
if ((intp&15) == 0) {
// arrowhead - trial and error trig till it looked OK :)
double angle = Math.atan2(lasty-y,lastx-x);
float newx1 = x + (float)Math.sin(angle-0.4+Math.PI/2)*sizer;
float newy1 = y - (float)Math.cos(angle-0.4+Math.PI/2)*sizer;
float newx2 = x + (float)Math.sin(angle+0.4+Math.PI/2)*sizer;
float newy2 = y - (float)Math.cos(angle+0.4+Math.PI/2)*sizer;
if (broken) {
path.moveTo(x, y);
}
path.lineTo(x,y);
path.moveTo(newx1, newy1);
path.lineTo(x, y);
lastx = x;
lasty = y;
path.lineTo(newx2, newy2);
path.moveTo(x,y);
broken = false;
}
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;
} else
broken = true;
}
canvas.drawPath(path, p);
canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
p.setColor(pCol);
}
canvas.drawPath(path, p);
canvas.rotate(tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY());
p.setStrokeWidth(pSw);
p.setColor(pCol);
}
}