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

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

fix #14247 - add icon as a visual indication of chosen gpx heatmap (patch by kidelo)

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