source: josm/trunk/src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java@ 11451

Last change on this file since 11451 was 11451, checked in by Don-vip, 7 years ago

fix #13124 - Heat Map Extension (patch by kidelo)

  • Property svn:eol-style set to native
File size: 48.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer.gpx;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.AlphaComposite;
8import java.awt.BasicStroke;
9import java.awt.Color;
10import java.awt.Composite;
11import java.awt.Graphics2D;
12import java.awt.LinearGradientPaint;
13import java.awt.MultipleGradientPaint;
14import java.awt.Paint;
15import java.awt.Point;
16import java.awt.Rectangle;
17import java.awt.RenderingHints;
18import java.awt.Stroke;
19import java.awt.image.BufferedImage;
20import java.awt.image.DataBufferInt;
21import java.awt.image.Raster;
22import java.io.BufferedReader;
23import java.io.IOException;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.Collection;
27import java.util.Collections;
28import java.util.Date;
29import java.util.List;
30
31import org.openstreetmap.josm.Main;
32import org.openstreetmap.josm.data.SystemOfMeasurement;
33import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener;
34import org.openstreetmap.josm.data.coor.LatLon;
35import org.openstreetmap.josm.data.gpx.GpxConstants;
36import org.openstreetmap.josm.data.gpx.GpxData;
37import org.openstreetmap.josm.data.gpx.WayPoint;
38import org.openstreetmap.josm.data.preferences.AbstractProperty;
39import org.openstreetmap.josm.data.preferences.ColorProperty;
40import org.openstreetmap.josm.gui.MapView;
41import org.openstreetmap.josm.io.CachedFile;
42import org.openstreetmap.josm.tools.ColorScale;
43import org.openstreetmap.josm.tools.JosmRuntimeException;
44import org.openstreetmap.josm.tools.Utils;
45
46/**
47 * Class that helps to draw large set of GPS tracks with different colors and options
48 * @since 7319
49 */
50public class GpxDrawHelper implements SoMChangeListener {
51
52 /**
53 * The color that is used for drawing GPX points.
54 * @since 10824
55 */
56 public static final ColorProperty DEFAULT_COLOR = new ColorProperty(marktr("gps point"), Color.magenta);
57
58 private final GpxData data;
59
60 // draw lines between points belonging to different segments
61 private boolean forceLines;
62 // use alpha blending for line draw
63 private boolean alphaLines;
64 // draw direction arrows on the lines
65 private boolean direction;
66 /** width of line for paint **/
67 private int lineWidth;
68 /** don't draw lines if longer than x meters **/
69 private int maxLineLength;
70 // draw lines
71 private boolean lines;
72 /** paint large dots for points **/
73 private boolean large;
74 private int largesize;
75 private boolean hdopCircle;
76 /** paint direction arrow with alternate math. may be faster **/
77 private boolean alternateDirection;
78 /** don't draw arrows nearer to each other than this **/
79 private int delta;
80 private double minTrackDurationForTimeColoring;
81
82 /** maximum value of displayed HDOP, minimum is 0 */
83 private int hdoprange;
84
85 private static final double PHI = Math.toRadians(15);
86
87 //// Variables used only to check cache validity
88 private boolean computeCacheInSync;
89 private int computeCacheMaxLineLengthUsed;
90 private Color computeCacheColorUsed;
91 private boolean computeCacheColorDynamic;
92 private ColorMode computeCacheColored;
93 private int computeCacheColorTracksTune;
94 private int computeCacheHeatMapDrawColorTableIdx;
95
96 //// Color-related fields
97 /** Mode of the line coloring **/
98 private ColorMode colored;
99 /** max speed for coloring - allows to tweak line coloring for different speed levels. **/
100 private int colorTracksTune;
101 private boolean colorModeDynamic;
102 private Color neutralColor;
103 private int largePointAlpha;
104
105 // default access is used to allow changing from plugins
106 private ColorScale velocityScale;
107 /** Colors (without custom alpha channel, if given) for HDOP painting. **/
108 private ColorScale hdopScale;
109 private ColorScale dateScale;
110 private ColorScale directionScale;
111
112 /** Opacity for hdop points **/
113 private int hdopAlpha;
114
115 // lookup array to draw arrows without doing any math
116 private static final int ll0 = 9;
117 private static final int sl4 = 5;
118 private static final int sl9 = 3;
119 private static final int[][] dir = {
120 {+sl4, +ll0, +ll0, +sl4}, {-sl9, +ll0, +sl9, +ll0},
121 {-ll0, +sl4, -sl4, +ll0}, {-ll0, -sl9, -ll0, +sl9},
122 {-sl4, -ll0, -ll0, -sl4}, {+sl9, -ll0, -sl9, -ll0},
123 {+ll0, -sl4, +sl4, -ll0}, {+ll0, +sl9, +ll0, -sl9}
124 };
125
126 /** heat map parameters **/
127
128 // enabled or not (override by settings)
129 private boolean heatMapEnabled;
130 // draw small extra line
131 private boolean heatMapDrawExtraLine;
132 // used index for color table (parameter)
133 private int heatMapDrawColorTableIdx;
134
135 // normal buffered image and draw object (cached)
136 private BufferedImage heatMapImgGray;
137 private Graphics2D heatMapGraph2d;
138
139 // some cached values
140 Rectangle heatMapCacheScreenBounds = new Rectangle();
141 int heatMapCacheVisibleSegments;
142 double heatMapCacheZoomScale;
143 int heatMapCacheLineWith;
144
145 // copied value for line drawing
146 private final List<Integer> heatMapPolyX = new ArrayList<>();
147 private final List<Integer> heatMapPolyY = new ArrayList<>();
148
149 // setup color maps used by heat map
150 private static Color[] heatMapLutColorJosmInferno = createColorFromResource("inferno");
151 private static Color[] heatMapLutColorJosmViridis = createColorFromResource("viridis");
152 private static Color[] heatMapLutColorJosmBrown2Green = createColorFromResource("brown2green");
153 private static Color[] heatMapLutColorJosmRed2Blue = createColorFromResource("red2blue");
154
155 // user defined heatmap color
156 private Color[] heatMapLutUserColor = createColorLut(Color.BLACK, Color.WHITE);
157
158 // heat map color in use
159 private Color[] heatMapLutColor;
160
161 private void setupColors() {
162 hdopAlpha = Main.pref.getInteger("hdop.color.alpha", -1);
163 velocityScale = ColorScale.createHSBScale(256);
164 /** Colors (without custom alpha channel, if given) for HDOP painting. **/
165 hdopScale = ColorScale.createHSBScale(256).makeReversed().addTitle(tr("HDOP"));
166 dateScale = ColorScale.createHSBScale(256).addTitle(tr("Time"));
167 directionScale = ColorScale.createCyclicScale(256).setIntervalCount(4).addTitle(tr("Direction"));
168 heatMapLutColor = heatMapLutUserColor;
169
170 systemOfMeasurementChanged(null, null);
171 }
172
173 @Override
174 public void systemOfMeasurementChanged(String oldSoM, String newSoM) {
175 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
176 velocityScale.addTitle(tr("Velocity, {0}", som.speedName));
177 if (Main.isDisplayingMapView() && oldSoM != null && newSoM != null) {
178 Main.map.mapView.repaint();
179 }
180 }
181
182 /**
183 * Different color modes
184 */
185 public enum ColorMode {
186 NONE, VELOCITY, HDOP, DIRECTION, TIME, HEATMAP;
187
188 static ColorMode fromIndex(final int index) {
189 return values()[index];
190 }
191
192 int toIndex() {
193 return Arrays.asList(values()).indexOf(this);
194 }
195 }
196
197 /**
198 * Constructs a new {@code GpxDrawHelper}.
199 * @param gpxData GPX data
200 * @param abstractProperty The color to draw with
201 * @since 10824
202 */
203 public GpxDrawHelper(GpxData gpxData, AbstractProperty<Color> abstractProperty) {
204 data = gpxData;
205 setupColors();
206 }
207
208 private static String specName(String layerName) {
209 return "layer " + layerName;
210 }
211
212 /**
213 * Get the default color for gps tracks for specified layer
214 * @param layerName name of the GpxLayer
215 * @param ignoreCustom do not use preferences
216 * @return the color or null if the color is not constant
217 */
218 public Color getColor(String layerName, boolean ignoreCustom) {
219 if (ignoreCustom || getColorMode(layerName) == ColorMode.NONE) {
220 return DEFAULT_COLOR.getChildColor(specName(layerName)).get();
221 } else {
222 return null;
223 }
224 }
225
226 /**
227 * Read coloring mode for specified layer from preferences
228 * @param layerName name of the GpxLayer
229 * @return coloring mode
230 */
231 public ColorMode getColorMode(String layerName) {
232 try {
233 int i = Main.pref.getInteger("draw.rawgps.colors", specName(layerName), 0);
234 return ColorMode.fromIndex(i);
235 } catch (IndexOutOfBoundsException e) {
236 Main.warn(e);
237 }
238 return ColorMode.NONE;
239 }
240
241 /** Reads generic color from preferences (usually gray)
242 * @return the color
243 **/
244 public static Color getGenericColor() {
245 return DEFAULT_COLOR.get();
246 }
247
248 /**
249 * Read all drawing-related settings from preferences
250 * @param layerName layer name used to access its specific preferences
251 **/
252 public void readPreferences(String layerName) {
253 String spec = specName(layerName);
254 forceLines = Main.pref.getBoolean("draw.rawgps.lines.force", spec, false);
255 direction = Main.pref.getBoolean("draw.rawgps.direction", spec, false);
256 lineWidth = Main.pref.getInteger("draw.rawgps.linewidth", spec, 0);
257 alphaLines = Main.pref.getBoolean("draw.rawgps.lines.alpha-blend", spec, false);
258
259 if (!data.fromServer) {
260 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length.local", spec, -1);
261 lines = Main.pref.getBoolean("draw.rawgps.lines.local", spec, true);
262 } else {
263 maxLineLength = Main.pref.getInteger("draw.rawgps.max-line-length", spec, 200);
264 lines = Main.pref.getBoolean("draw.rawgps.lines", spec, true);
265 }
266 large = Main.pref.getBoolean("draw.rawgps.large", spec, false);
267 largesize = Main.pref.getInteger("draw.rawgps.large.size", spec, 3);
268 hdopCircle = Main.pref.getBoolean("draw.rawgps.hdopcircle", spec, false);
269 colored = getColorMode(layerName);
270 alternateDirection = Main.pref.getBoolean("draw.rawgps.alternatedirection", spec, false);
271 delta = Main.pref.getInteger("draw.rawgps.min-arrow-distance", spec, 40);
272 colorTracksTune = Main.pref.getInteger("draw.rawgps.colorTracksTune", spec, 45);
273 colorModeDynamic = Main.pref.getBoolean("draw.rawgps.colors.dynamic", spec, false);
274 /* good HDOP's are between 1 and 3, very bad HDOP's go into 3 digit values */
275 hdoprange = Main.pref.getInteger("hdop.range", 7);
276 minTrackDurationForTimeColoring = Main.pref.getInteger("draw.rawgps.date-coloring-min-dt", 60);
277 largePointAlpha = Main.pref.getInteger("draw.rawgps.large.alpha", -1) & 0xFF;
278
279 // get heatmap parameters
280 heatMapEnabled = Main.pref.getBoolean("draw.rawgps.heatmap.enabled", spec, false);
281 heatMapDrawExtraLine = Main.pref.getBoolean("draw.rawgps.heatmap.line-extra", spec, false);
282 heatMapDrawColorTableIdx = Main.pref.getInteger("draw.rawgps.heatmap.colormap", specName(layerName), 0);
283
284 neutralColor = getColor(layerName, true);
285 velocityScale.setNoDataColor(neutralColor);
286 dateScale.setNoDataColor(neutralColor);
287 hdopScale.setNoDataColor(neutralColor);
288 directionScale.setNoDataColor(neutralColor);
289
290 largesize += lineWidth;
291 }
292
293 /**
294 * Draw all enabled GPX elements of layer.
295 * @param g the common draw object to use
296 * @param mv the meta data to current displayed area
297 * @param visibleSegments segments visible in the current scope of mv
298 */
299 public void drawAll(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
300
301 final long timeStart = System.currentTimeMillis();
302
303 checkCache();
304
305 // STEP 2b - RE-COMPUTE CACHE DATA *********************
306 if (!computeCacheInSync) { // don't compute if the cache is good
307 calculateColors();
308 }
309
310 fixColors(visibleSegments);
311
312 // backup the environment
313 Composite oldComposite = g.getComposite();
314 Stroke oldStroke = g.getStroke();
315 Paint oldPaint = g.getPaint();
316
317 // set hints for the render
318 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
319 Main.pref.getBoolean("mappaint.gpx.use-antialiasing", false) ?
320 RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
321
322 if (lineWidth != 0) {
323 g.setStroke(new BasicStroke(lineWidth, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
324 }
325
326 // global enabled or select via color
327 boolean useHeatMap = heatMapEnabled || ColorMode.HEATMAP == colored;
328
329 // default global alpha level
330 float layerAlpha = 1.00f;
331
332 // extract current alpha blending value
333 if (oldComposite instanceof AlphaComposite) {
334 layerAlpha = ((AlphaComposite) oldComposite).getAlpha();
335 }
336
337 // use heatmap background layer
338 if (useHeatMap) {
339 drawHeatMap(g, mv, visibleSegments);
340 } else {
341 // use normal line style or alpha-blending lines
342 if (!alphaLines) {
343 drawLines(g, mv, visibleSegments);
344 } else {
345 drawLinesAlpha(g, mv, visibleSegments, layerAlpha);
346 }
347 }
348
349 // override global alpha settings (smooth overlay)
350 if (alphaLines || useHeatMap) {
351 g.setComposite(AlphaComposite.SrcOver.derive(0.25f * layerAlpha));
352 }
353
354 // normal overlays
355 drawArrows(g, mv, visibleSegments);
356 drawPoints(g, mv, visibleSegments);
357
358 // restore environment
359 g.setPaint(oldPaint);
360 g.setStroke(oldStroke);
361 g.setComposite(oldComposite);
362
363 // show some debug info
364 if (Main.isDebugEnabled() && !visibleSegments.isEmpty()) {
365 final long timeDiff = System.currentTimeMillis() - timeStart;
366
367 Main.debug("gpxdraw::draw takes " +
368 Utils.getDurationString(timeDiff) +
369 "(" +
370 "segments= " + visibleSegments.size() +
371 ", per 10000 = " + Utils.getDurationString(10_000 * timeDiff / visibleSegments.size()) +
372 ")"
373 );
374 }
375 }
376
377 /**
378 * Calculate colors of way segments based on latest configuration settings
379 */
380 public void calculateColors() {
381 double minval = +1e10;
382 double maxval = -1e10;
383 WayPoint oldWp = null;
384
385 if (colorModeDynamic) {
386 if (colored == ColorMode.VELOCITY) {
387 final List<Double> velocities = new ArrayList<>();
388 for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
389 if (!forceLines) {
390 oldWp = null;
391 }
392 for (WayPoint trkPnt : segment) {
393 LatLon c = trkPnt.getCoor();
394 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
395 continue;
396 }
397 if (oldWp != null && trkPnt.time > oldWp.time) {
398 double vel = c.greatCircleDistance(oldWp.getCoor())
399 / (trkPnt.time - oldWp.time);
400 velocities.add(vel);
401 }
402 oldWp = trkPnt;
403 }
404 }
405 Collections.sort(velocities);
406 if (velocities.isEmpty()) {
407 velocityScale.setRange(0, 120/3.6);
408 } else {
409 minval = velocities.get(velocities.size() / 20); // 5% percentile to remove outliers
410 maxval = velocities.get(velocities.size() * 19 / 20); // 95% percentile to remove outliers
411 velocityScale.setRange(minval, maxval);
412 }
413 } else if (colored == ColorMode.HDOP) {
414 for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
415 for (WayPoint trkPnt : segment) {
416 Object val = trkPnt.get(GpxConstants.PT_HDOP);
417 if (val != null) {
418 double hdop = ((Float) val).doubleValue();
419 if (hdop > maxval) {
420 maxval = hdop;
421 }
422 if (hdop < minval) {
423 minval = hdop;
424 }
425 }
426 }
427 }
428 if (minval >= maxval) {
429 hdopScale.setRange(0, 100);
430 } else {
431 hdopScale.setRange(minval, maxval);
432 }
433 }
434 oldWp = null;
435 } else { // color mode not dynamic
436 velocityScale.setRange(0, colorTracksTune);
437 hdopScale.setRange(0, hdoprange);
438 }
439 double now = System.currentTimeMillis()/1000.0;
440 if (colored == ColorMode.TIME) {
441 Date[] bounds = data.getMinMaxTimeForAllTracks();
442 if (bounds.length >= 2) {
443 minval = bounds[0].getTime()/1000.0;
444 maxval = bounds[1].getTime()/1000.0;
445 } else {
446 minval = 0;
447 maxval = now;
448 }
449 dateScale.setRange(minval, maxval);
450 }
451
452 // Now the colors for all the points will be assigned
453 for (Collection<WayPoint> segment : data.getLinesIterable(null)) {
454 if (!forceLines) { // don't draw lines between segments, unless forced to
455 oldWp = null;
456 }
457 for (WayPoint trkPnt : segment) {
458 LatLon c = trkPnt.getCoor();
459 trkPnt.customColoring = neutralColor;
460 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
461 continue;
462 }
463 // now we are sure some color will be assigned
464 Color color = null;
465
466 if (colored == ColorMode.HDOP) {
467 Float hdop = (Float) trkPnt.get(GpxConstants.PT_HDOP);
468 color = hdopScale.getColor(hdop);
469 }
470 if (oldWp != null) { // other coloring modes need segment for calcuation
471 double dist = c.greatCircleDistance(oldWp.getCoor());
472 boolean noDraw = false;
473 switch (colored) {
474 case VELOCITY:
475 double dtime = trkPnt.time - oldWp.time;
476 if (dtime > 0) {
477 color = velocityScale.getColor(dist / dtime);
478 } else {
479 color = velocityScale.getNoDataColor();
480 }
481 break;
482 case DIRECTION:
483 double dirColor = oldWp.getCoor().bearing(trkPnt.getCoor());
484 color = directionScale.getColor(dirColor);
485 break;
486 case TIME:
487 double t = trkPnt.time;
488 // skip bad timestamps and very short tracks
489 if (t > 0 && t <= now && maxval - minval > minTrackDurationForTimeColoring) {
490 color = dateScale.getColor(t);
491 } else {
492 color = dateScale.getNoDataColor();
493 }
494 break;
495 default: // Do nothing
496 }
497 if (!noDraw && (maxLineLength == -1 || dist <= maxLineLength)) {
498 trkPnt.drawLine = true;
499 double bearing = oldWp.getCoor().bearing(trkPnt.getCoor());
500 trkPnt.dir = ((int) (bearing / Math.PI * 4 + 1.5)) % 8;
501 } else {
502 trkPnt.drawLine = false;
503 }
504 } else { // make sure we reset outdated data
505 trkPnt.drawLine = false;
506 color = neutralColor;
507 }
508 if (color != null) {
509 trkPnt.customColoring = color;
510 }
511 oldWp = trkPnt;
512 }
513 }
514
515 // heat mode
516 if (ColorMode.HEATMAP == colored && neutralColor != null) {
517
518 // generate new user color map
519 heatMapLutUserColor = createColorLut(Color.BLACK, neutralColor.darker(),
520 neutralColor, neutralColor.brighter(), Color.WHITE);
521
522 // decide what, keep order is sync with setting on GUI
523 Color[][] lut = {
524 heatMapLutUserColor,
525 heatMapLutColorJosmInferno,
526 heatMapLutColorJosmViridis,
527 heatMapLutColorJosmBrown2Green,
528 heatMapLutColorJosmRed2Blue
529 };
530
531 // select by index
532 if (heatMapDrawColorTableIdx < lut.length) {
533 heatMapLutColor = lut[ heatMapDrawColorTableIdx ];
534 } else {
535 // fallback
536 heatMapLutColor = heatMapLutUserColor;
537 }
538
539 // force redraw of image
540 heatMapCacheVisibleSegments = 0;
541 }
542
543 computeCacheInSync = true;
544 }
545
546 /**
547 * Draw all GPX ways segments
548 * @param g the common draw object to use
549 * @param mv the meta data to current displayed area
550 * @param visibleSegments segments visible in the current scope of mv
551 */
552 private void drawLines(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
553 if (lines) {
554 Point old = null;
555 for (WayPoint trkPnt : visibleSegments) {
556 LatLon c = trkPnt.getCoor();
557 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
558 continue;
559 }
560 Point screen = mv.getPoint(trkPnt.getEastNorth());
561 // skip points that are on the same screenposition
562 if (trkPnt.drawLine && old != null && ((old.x != screen.x) || (old.y != screen.y))) {
563 g.setColor(trkPnt.customColoring);
564 g.drawLine(old.x, old.y, screen.x, screen.y);
565 }
566 old = screen;
567 }
568 }
569 }
570
571 /**
572 * Draw all GPX arrays
573 * @param g the common draw object to use
574 * @param mv the meta data to current displayed area
575 * @param visibleSegments segments visible in the current scope of mv
576 */
577 private void drawArrows(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
578 /****************************************************************
579 ********** STEP 3b - DRAW NICE ARROWS **************************
580 ****************************************************************/
581 if (lines && direction && !alternateDirection) {
582 Point old = null;
583 Point oldA = null; // last arrow painted
584 for (WayPoint trkPnt : visibleSegments) {
585 LatLon c = trkPnt.getCoor();
586 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
587 continue;
588 }
589 if (trkPnt.drawLine) {
590 Point screen = mv.getPoint(trkPnt.getEastNorth());
591 // skip points that are on the same screenposition
592 if (old != null
593 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
594 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
595 g.setColor(trkPnt.customColoring);
596 double t = Math.atan2((double) screen.y - old.y, (double) screen.x - old.x) + Math.PI;
597 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t - PHI)),
598 (int) (screen.y + 10 * Math.sin(t - PHI)));
599 g.drawLine(screen.x, screen.y, (int) (screen.x + 10 * Math.cos(t + PHI)),
600 (int) (screen.y + 10 * Math.sin(t + PHI)));
601 oldA = screen;
602 }
603 old = screen;
604 }
605 } // end for trkpnt
606 }
607
608 /****************************************************************
609 ********** STEP 3c - DRAW FAST ARROWS **************************
610 ****************************************************************/
611 if (lines && direction && alternateDirection) {
612 Point old = null;
613 Point oldA = null; // last arrow painted
614 for (WayPoint trkPnt : visibleSegments) {
615 LatLon c = trkPnt.getCoor();
616 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
617 continue;
618 }
619 if (trkPnt.drawLine) {
620 Point screen = mv.getPoint(trkPnt.getEastNorth());
621 // skip points that are on the same screenposition
622 if (old != null
623 && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
624 || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
625 g.setColor(trkPnt.customColoring);
626 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
627 + dir[trkPnt.dir][1]);
628 g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][2], screen.y
629 + dir[trkPnt.dir][3]);
630 oldA = screen;
631 }
632 old = screen;
633 }
634 } // end for trkpnt
635 }
636 }
637
638 /**
639 * Draw all GPX points
640 * @param g the common draw object to use
641 * @param mv the meta data to current displayed area
642 * @param visibleSegments segments visible in the current scope of mv
643 */
644 private void drawPoints(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
645 /****************************************************************
646 ********** STEP 3d - DRAW LARGE POINTS AND HDOP CIRCLE *********
647 ****************************************************************/
648 if (large || hdopCircle) {
649 final int halfSize = largesize/2;
650 for (WayPoint trkPnt : visibleSegments) {
651 LatLon c = trkPnt.getCoor();
652 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
653 continue;
654 }
655 Point screen = mv.getPoint(trkPnt.getEastNorth());
656
657
658 if (hdopCircle && trkPnt.get(GpxConstants.PT_HDOP) != null) {
659 // hdop value
660 float hdop = (Float) trkPnt.get(GpxConstants.PT_HDOP);
661 if (hdop < 0) {
662 hdop = 0;
663 }
664 Color customColoringTransparent = hdopAlpha < 0 ? trkPnt.customColoring :
665 new Color((trkPnt.customColoring.getRGB() & 0x00ffffff) | (hdopAlpha << 24), true);
666 g.setColor(customColoringTransparent);
667 // hdop circles
668 int hdopp = mv.getPoint(new LatLon(
669 trkPnt.getCoor().lat(),
670 trkPnt.getCoor().lon() + 2d*6*hdop*360/40000000d)).x - screen.x;
671 g.drawArc(screen.x-hdopp/2, screen.y-hdopp/2, hdopp, hdopp, 0, 360);
672 }
673 if (large) {
674 // color the large GPS points like the gps lines
675 if (trkPnt.customColoring != null) {
676 Color customColoringTransparent = largePointAlpha < 0 ? trkPnt.customColoring :
677 new Color((trkPnt.customColoring.getRGB() & 0x00ffffff) | (largePointAlpha << 24), true);
678
679 g.setColor(customColoringTransparent);
680 }
681 g.fillRect(screen.x-halfSize, screen.y-halfSize, largesize, largesize);
682 }
683 } // end for trkpnt
684 } // end if large || hdopcircle
685
686 /****************************************************************
687 ********** STEP 3e - DRAW SMALL POINTS FOR LINES ***************
688 ****************************************************************/
689 if (!large && lines) {
690 g.setColor(neutralColor);
691 for (WayPoint trkPnt : visibleSegments) {
692 LatLon c = trkPnt.getCoor();
693 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
694 continue;
695 }
696 if (!trkPnt.drawLine) {
697 Point screen = mv.getPoint(trkPnt.getEastNorth());
698 g.drawRect(screen.x, screen.y, 0, 0);
699 }
700 } // end for trkpnt
701 } // end if large
702
703 /****************************************************************
704 ********** STEP 3f - DRAW SMALL POINTS INSTEAD OF LINES ********
705 ****************************************************************/
706 if (!large && !lines) {
707 g.setColor(neutralColor);
708 for (WayPoint trkPnt : visibleSegments) {
709 LatLon c = trkPnt.getCoor();
710 if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
711 continue;
712 }
713 Point screen = mv.getPoint(trkPnt.getEastNorth());
714 g.setColor(trkPnt.customColoring);
715 g.drawRect(screen.x, screen.y, 0, 0);
716 } // end for trkpnt
717 } // end if large
718 }
719
720 /**
721 * Draw GPX lines by using alpha blending
722 * @param g the common draw object to use
723 * @param mv the meta data to current displayed area
724 * @param visibleSegments segments visible in the current scope of mv
725 * @param layerAlpha the color alpha value set for that operation
726 */
727 private void drawLinesAlpha(Graphics2D g, MapView mv, List<WayPoint> visibleSegments, float layerAlpha) {
728
729 // 1st. backup the paint environment ----------------------------------
730 Composite oldComposite = g.getComposite();
731 Stroke oldStroke = g.getStroke();
732 Paint oldPaint = g.getPaint();
733
734 // 2nd. determine current scale factors -------------------------------
735
736 // adjust global settings
737 final int globalLineWidth = Math.min(Math.max(lineWidth, 1), 20);
738
739 // cache scale of view
740 final double zoomScale = mv.getScale();
741
742 // 3rd. determine current paint parameters -----------------------------
743
744 // alpha value is based on zoom and line with combined with global layer alpha
745 float theLineAlpha = Math.min(Math.max((0.50f/(float) zoomScale)/(globalLineWidth + 1), 0.01f), 0.50f) * layerAlpha;
746 final int theLineWith = (int) (lineWidth / zoomScale) + 1;
747
748 // 4th setup virtual paint area ----------------------------------------
749
750 // set line format and alpha channel for all overlays (more lines -> few overlap -> more transparency)
751 g.setStroke(new BasicStroke(theLineWith, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
752 g.setComposite(AlphaComposite.SrcOver.derive(theLineAlpha));
753
754 // last used / calculated entries
755 Point lastPaintPnt = null;
756
757 // 5th draw the layer ---------------------------------------------------
758
759 // for all points
760 for (WayPoint trkPnt : visibleSegments) {
761
762 // transform coordinates
763 final Point paintPnt = mv.getPoint(trkPnt.getEastNorth());
764
765 // skip single points
766 if (lastPaintPnt != null && trkPnt.drawLine && !lastPaintPnt.equals(paintPnt)) {
767
768 // set different color
769 g.setColor(trkPnt.customColoring);
770
771 // draw it
772 g.drawLine(lastPaintPnt.x, lastPaintPnt.y, paintPnt.x, paintPnt.y);
773 }
774
775 lastPaintPnt = paintPnt;
776 }
777
778 // @last restore modified paint environment -----------------------------
779 g.setPaint(oldPaint);
780 g.setStroke(oldStroke);
781 g.setComposite(oldComposite);
782 }
783
784 /**
785 * Creates a linear distributed colormap by linear blending between colors
786 * @param colors 1..n colors
787 * @return array of Color objects
788 */
789 protected static Color[] createColorLut(Color... colors) {
790
791 // number of lookup entries
792 int tableSize = 256;
793
794 // create image an paint object
795 BufferedImage img = new BufferedImage(tableSize, 1, BufferedImage.TYPE_INT_RGB);
796 Graphics2D g = img.createGraphics();
797
798 float[] fract = new float[ colors.length ];
799
800 // distribute fractions (define position of color in map)
801 for (int i = 0; i < colors.length; ++i) {
802 fract[i] = i * (1.0f / colors.length);
803 }
804
805 // draw the gradient map
806 LinearGradientPaint gradient = new LinearGradientPaint(0, 0, tableSize, 1, fract, colors,
807 MultipleGradientPaint.CycleMethod.NO_CYCLE);
808 g.setPaint(gradient);
809 g.fillRect(0, 0, tableSize, 1);
810 g.dispose();
811
812 // access it via raw interface
813 final Raster imgRaster = img.getData();
814
815 // the pixel storage
816 int[] pixel = new int[1];
817
818 Color[] colorTable = new Color[tableSize];
819
820 // map the range 0..255 to 0..pi/2
821 final double mapTo90Deg = Math.PI / 2.0 / 255.0;
822
823 // create the lookup table
824 for (int i = 0; i < tableSize; i++) {
825
826 // get next single pixel
827 imgRaster.getDataElements((int) (i * (double) img.getWidth() / tableSize), 0, pixel);
828
829 // get color and map
830 Color c = new Color(pixel[0]);
831
832 // smooth alpha like sin curve
833 int alpha = (int) (Math.sin(i * mapTo90Deg) * 255);
834
835 // alpha with pre-offset, first color -> full transparent
836 alpha = i > 0 ? (10 + alpha) : 0;
837
838 // shrink to maximum bound
839 if (alpha > 255) {
840 alpha = 255;
841 }
842
843 // increase transparency for higher values ( avoid big saturation )
844 if (i > 240 && 255 == alpha) {
845 alpha -= (i - 240);
846 }
847
848 // fill entry in table, assign a alpha value
849 colorTable[i] = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
850 }
851
852 // transform into lookup table
853 return colorTable;
854 }
855
856 /**
857 * Creates a darker color
858 * @param in Color object
859 * @param adjust darker adjustment amount
860 * @return new Color
861 */
862 protected static Color darkerColor(Color in, float adjust) {
863
864 final float r = ((float) in.getRed()/255);
865 final float g = ((float) in.getGreen()/255);
866 final float b = ((float) in.getBlue()/255);
867
868 return new Color(r*adjust, g*adjust, b*adjust);
869 }
870
871 /**
872 * Creates a colormap by using a static color map with 1..n colors (RGB 0.0 ..1.0)
873 * @param str the filename (without extension) to look for into data/gpx
874 * @return the parsed colormap
875 */
876 protected static Color[] createColorFromResource(String str) {
877
878 // create resource string
879 final String colorFile = "resource://data/gpx/" + str + ".txt";
880
881 List<Color> colorList = new ArrayList<>();
882
883 // try to load the file
884 try (CachedFile cf = new CachedFile(colorFile); BufferedReader br = cf.getContentReader()) {
885
886 String line;
887
888 // process lines
889 while ((line = br.readLine()) != null) {
890
891 // use comma as separator
892 String[] column = line.split(",");
893
894 // empty or comment line
895 if (column.length < 3 || column[0].startsWith("#")) {
896 continue;
897 }
898
899 // extract RGB value
900 float r = Float.parseFloat(column[0]);
901 float g = Float.parseFloat(column[1]);
902 float b = Float.parseFloat(column[2]);
903
904 // some color tables are 0..1.0 and some 0.255
905 float scale = (r < 1 && g < 1 && b < 1) ? 1 : 255;
906
907 colorList.add(new Color(r/scale, g/scale, b/scale));
908 }
909 } catch (IOException e) {
910 throw new JosmRuntimeException(e);
911 }
912
913 // fallback if empty or failed
914 if (colorList.isEmpty()) {
915 colorList.add(Color.BLACK);
916 colorList.add(Color.WHITE);
917 } else {
918 // add additional darker elements to end of list
919 final Color lastColor = colorList.get(colorList.size() - 1);
920 colorList.add(darkerColor(lastColor, 0.975f));
921 colorList.add(darkerColor(lastColor, 0.950f));
922 }
923
924 return createColorLut(colorList.toArray(new Color[ colorList.size() ]));
925 }
926
927 /**
928 * Draw gray heat map with current Graphics2D setting
929 * @param gB the common draw object to use
930 * @param mv the meta data to current displayed area
931 * @param listSegm segments visible in the current scope of mv
932 * @param foreComp composite use to draw foreground objects
933 * @param foreStroke stroke use to draw foreground objects
934 * @param backComp composite use to draw background objects
935 * @param backStroke stroke use to draw background objects
936 */
937 private void drawHeatGrayMap(Graphics2D gB, MapView mv, List<WayPoint> listSegm,
938 Composite foreComp, Stroke foreStroke,
939 Composite backComp, Stroke backStroke) {
940
941 // draw foreground
942 boolean drawForeground = foreComp != null && foreStroke != null;
943
944 // set initial values
945 gB.setStroke(backStroke); gB.setComposite(backComp);
946
947 // get last point in list
948 final WayPoint lastPnt = !listSegm.isEmpty() ? listSegm.get(listSegm.size() - 1) : null;
949
950 // for all points, draw single lines by using optimized drawing
951 for (WayPoint trkPnt : listSegm) {
952
953 // get transformed coordinates
954 final Point paintPnt = mv.getPoint(trkPnt.getEastNorth());
955
956 // end of line segment or end of list reached
957 if (!trkPnt.drawLine || (lastPnt == trkPnt)) {
958
959 // convert to primitive type
960 final int[] polyXArr = heatMapPolyX.stream().mapToInt(Integer::intValue).toArray();
961 final int[] polyYArr = heatMapPolyY.stream().mapToInt(Integer::intValue).toArray();
962
963 // a.) draw background
964 gB.drawPolyline(polyXArr, polyYArr, polyXArr.length);
965
966 // b.) draw extra foreground
967 if (drawForeground && heatMapDrawExtraLine) {
968
969 gB.setStroke(foreStroke); gB.setComposite(foreComp);
970 gB.drawPolyline(polyXArr, polyYArr, polyXArr.length);
971 gB.setStroke(backStroke); gB.setComposite(backComp);
972 }
973
974 // drop used points
975 heatMapPolyX.clear(); heatMapPolyY.clear();
976 }
977
978 // store only the integer part (make sense because pixel is 1:1 here)
979 heatMapPolyX.add((int) paintPnt.getX());
980 heatMapPolyY.add((int) paintPnt.getY());
981 }
982 }
983
984 /**
985 * Map the gray map to heat map and draw them with current Graphics2D setting
986 * @param g the common draw object to use
987 * @param imgGray gray scale input image
988 * @param sampleRaster the line with for drawing
989 * @param outlineWidth line width for outlines
990 */
991 private void drawHeatMapGrayMap(Graphics2D g, BufferedImage imgGray, int sampleRaster, int outlineWidth) {
992
993 final int[] imgPixels = ((DataBufferInt) imgGray.getRaster().getDataBuffer()).getData();
994
995 // samples offset and bounds are scaled with line width derived from zoom level
996 final int offX = Math.max(1, sampleRaster);
997 final int offY = Math.max(1, sampleRaster);
998
999 final int maxPixelX = imgGray.getWidth();
1000 final int maxPixelY = imgGray.getHeight();
1001
1002 // always full or outlines at big samples rasters
1003 final boolean drawOutlines = (outlineWidth > 0) && ((0 == sampleRaster) || (sampleRaster > 8));
1004
1005 // backup stroke
1006 final Stroke oldStroke = g.getStroke();
1007
1008 // use basic stroke for outlines and default transparency
1009 g.setStroke(new BasicStroke(outlineWidth));
1010
1011 int lastPixelY = 0;
1012 int lastPixelColor = 0;
1013
1014 // resample gray scale image with line linear weight of next sample in line
1015 // process each line and draw pixels / rectangles with same color with one operations
1016 for (int x = 0; x < maxPixelX; x += offX) {
1017 for (int y = 0; y < maxPixelY; y += offY) {
1018
1019 int thePixelColor = 0;
1020
1021 // sample the image (it is gray scale)
1022 int offset = (x * maxPixelX) + y;
1023
1024 // merge next pixels of window of line
1025 for (int k = 0; k < offX && offset + k < imgPixels.length; k++) {
1026 thePixelColor += imgPixels[offset+k] & 0xFF;
1027 }
1028
1029 // mean value
1030 thePixelColor /= offX;
1031
1032 // restart -> use initial sample
1033 if (0 == y) {
1034 lastPixelY = 0; lastPixelColor = thePixelColor - 1;
1035 }
1036
1037 boolean bDrawIt = false;
1038
1039 // when one of segment is mapped to black
1040 bDrawIt = bDrawIt || (lastPixelColor == 0) || (thePixelColor == 0);
1041
1042 // different color
1043 bDrawIt = bDrawIt || (Math.abs(lastPixelColor-thePixelColor) > 0);
1044
1045 // when line is finished draw always
1046 bDrawIt = bDrawIt || (y >= (maxPixelY-offY));
1047
1048 if (bDrawIt) {
1049
1050 // draw only foreground pixels
1051 if (lastPixelColor > 0) {
1052
1053 // gray to RGB mapping
1054 g.setColor(heatMapLutColor[ lastPixelColor ]);
1055
1056 // box from from last Y pixel to current pixel
1057 if (drawOutlines) {
1058 g.drawRect(lastPixelY, x, offY + y - lastPixelY, offX);
1059 } else {
1060 g.fillRect(lastPixelY, x, offY + y - lastPixelY, offX);
1061 }
1062 }
1063
1064 // restart detection
1065 lastPixelY = y; lastPixelColor = thePixelColor;
1066 }
1067 }
1068 }
1069
1070 // recover
1071 g.setStroke(oldStroke);
1072 }
1073
1074 /**
1075 * Collect and draw GPS segments and displays a heat-map
1076 * @param g the common draw object to use
1077 * @param mv the meta data to current displayed area
1078 * @param visibleSegments segments visible in the current scope of mv
1079 */
1080 private void drawHeatMap(Graphics2D g, MapView mv, List<WayPoint> visibleSegments) {
1081
1082 // get bounds of screen image and projection, zoom and adjust input parameters
1083 final Rectangle screenBounds = g.getDeviceConfiguration().getBounds();
1084 final double zoomScale = mv.getScale();
1085
1086 // adjust global settings ( zero = default line width )
1087 final int globalLineWidth = (0 == lineWidth) ? 1 : Math.min(Math.max(lineWidth, 1), 20);
1088
1089 // 1st setup virtual paint area ----------------------------------------
1090
1091 // HACK: sometime screen bounds does not return valid values when picture is shifted
1092 // therefore we use a bigger area to avoid missing parts of image
1093 screenBounds.width = screenBounds.width * 3 / 2;
1094 screenBounds.height = screenBounds.height * 3 / 2;
1095
1096 // new image buffer needed
1097 final boolean imageSetup = null == heatMapImgGray || !heatMapCacheScreenBounds.equals(screenBounds);
1098
1099 // screen bounds changed, need new image buffer ?
1100 if (imageSetup) {
1101 // we would use a "pure" grayscale image, but there is not efficient way to map gray scale values to RGB)
1102 heatMapImgGray = new BufferedImage(screenBounds.width, screenBounds.height, BufferedImage.TYPE_INT_ARGB);
1103 heatMapGraph2d = heatMapImgGray.createGraphics();
1104 heatMapGraph2d.setBackground(new Color(0, 0, 0, 255));
1105 heatMapGraph2d.setColor(Color.WHITE);
1106
1107 // cache it
1108 heatMapCacheScreenBounds = screenBounds;
1109 }
1110
1111 // 2nd. determine current scale factors -------------------------------
1112
1113 // the line width (foreground: draw extra small footprint line of track)
1114 final int lineWidthB = (int) Math.max(1.5f * (globalLineWidth / zoomScale) + 1, 2);
1115 final int lineWidthF = lineWidthB > 2 ? (globalLineWidth - 1) : 0;
1116
1117 // recalculation of image needed
1118 final boolean imageRecalc = heatMapCacheVisibleSegments != visibleSegments.size() ||
1119 heatMapCacheZoomScale != zoomScale ||
1120 heatMapCacheLineWith != globalLineWidth;
1121
1122 // 3rd Calculate the heat map data by draw GPX traces with alpha value ----------
1123
1124 // need re-generation of gray image ?
1125 if (imageSetup || imageRecalc) {
1126
1127 // clear background
1128 heatMapGraph2d.clearRect(0, 0, heatMapImgGray.getWidth(), heatMapImgGray.getHeight());
1129
1130 // alpha combines both values, therefore the foreground shall be lighter
1131 final float lineAlphaB = Math.min(Math.max((0.40f/(float) zoomScale)/(globalLineWidth + 1), 0.01f), 0.40f);
1132 final float lineAlphaF = lineAlphaB / 1.5f;
1133
1134 // derive draw parameters and draw
1135 drawHeatGrayMap(heatMapGraph2d, mv, visibleSegments,
1136 lineWidthF > 1 ? AlphaComposite.SrcOver.derive(lineAlphaF) : null,
1137 new BasicStroke(lineWidthF, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND),
1138 AlphaComposite.SrcOver.derive(lineAlphaB),
1139 new BasicStroke(lineWidthB, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
1140
1141 // remember draw parameters
1142 heatMapCacheVisibleSegments = visibleSegments.size();
1143 heatMapCacheZoomScale = zoomScale;
1144 heatMapCacheLineWith = lineWidth;
1145 }
1146
1147 // 4th. Draw data on target layer, map data via color lookup table --------------
1148 drawHeatMapGrayMap(g, heatMapImgGray,
1149 lineWidthB > 2 ? (lineWidthB / 2) : 1,
1150 lineWidth > 2 ? (lineWidth - 2) : 1);
1151 }
1152
1153 /**
1154 * Apply default color configuration to way segments
1155 * @param visibleSegments segments visible in the current scope of mv
1156 */
1157 private void fixColors(List<WayPoint> visibleSegments) {
1158 for (WayPoint trkPnt : visibleSegments) {
1159 if (trkPnt.customColoring == null) {
1160 trkPnt.customColoring = neutralColor;
1161 }
1162 }
1163 }
1164
1165 /**
1166 * Check cache validity set necessary flags
1167 */
1168 private void checkCache() {
1169 if ((computeCacheMaxLineLengthUsed != maxLineLength) || (!neutralColor.equals(computeCacheColorUsed))
1170 || (computeCacheColored != colored) || (computeCacheColorTracksTune != colorTracksTune)
1171 || (computeCacheColorDynamic != colorModeDynamic)
1172 || (computeCacheHeatMapDrawColorTableIdx != heatMapDrawColorTableIdx)
1173 ) {
1174 computeCacheMaxLineLengthUsed = maxLineLength;
1175 computeCacheInSync = false;
1176 computeCacheColorUsed = neutralColor;
1177 computeCacheColored = colored;
1178 computeCacheColorTracksTune = colorTracksTune;
1179 computeCacheColorDynamic = colorModeDynamic;
1180 computeCacheHeatMapDrawColorTableIdx = heatMapDrawColorTableIdx;
1181 }
1182 }
1183
1184 /**
1185 * callback when data is changed, invalidate cached configuration parameters
1186 */
1187 public void dataChanged() {
1188 computeCacheInSync = false;
1189 }
1190
1191 /**
1192 * Draw all GPX arrays
1193 * @param g the common draw object to use
1194 * @param mv the meta data to current displayed area
1195 */
1196 public void drawColorBar(Graphics2D g, MapView mv) {
1197 int w = mv.getWidth();
1198
1199 // set do default
1200 g.setComposite(AlphaComposite.SrcOver.derive(1.00f));
1201
1202 if (colored == ColorMode.HDOP) {
1203 hdopScale.drawColorBar(g, w-30, 50, 20, 100, 1.0);
1204 } else if (colored == ColorMode.VELOCITY) {
1205 SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
1206 velocityScale.drawColorBar(g, w-30, 50, 20, 100, som.speedValue);
1207 } else if (colored == ColorMode.DIRECTION) {
1208 directionScale.drawColorBar(g, w-30, 50, 20, 100, 180.0/Math.PI);
1209 }
1210 }
1211}
Note: See TracBrowser for help on using the repository browser.