source: josm/trunk/src/org/openstreetmap/josm/data/gpx/GpxTrack.java@ 15560

Last change on this file since 15560 was 15560, checked in by GerdP, 4 years ago

fix #18389: GPX track with color black is invisible (Patch by Bjoeni)
Show popup that track is not visible because color matches backgroud color. This shows a popup only if

  • background color of all tracks in the file equals background color and
  • no imagery layer is currently visible

"I noticed that Garmin actually uses gpxx:TrackExtension instead of gpxx:TrackExtensions, so the abbreviations don't work and files are sometimes not written according to the standard."

  • Property svn:eol-style set to native
File size: 8.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.gpx;
3
4import java.awt.Color;
5import java.util.ArrayList;
6import java.util.Collection;
7import java.util.Collections;
8import java.util.HashMap;
9import java.util.List;
10import java.util.Map;
11import java.util.Map.Entry;
12import java.util.Optional;
13
14import org.openstreetmap.josm.data.Bounds;
15import org.openstreetmap.josm.tools.ListenerList;
16import org.openstreetmap.josm.tools.Logging;
17
18/**
19 * GPX track.
20 * Note that the color attributes are not immutable and may be modified by the user.
21 * @since 15496
22 */
23public class GpxTrack extends WithAttributes implements IGpxTrack {
24
25 private final List<IGpxTrackSegment> segments;
26 private final double length;
27 private final Bounds bounds;
28 private Color colorCache;
29 private final ListenerList<IGpxTrack.GpxTrackChangeListener> listeners = ListenerList.create();
30 private static final HashMap<Color, String> closestGarminColorCache = new HashMap<>();
31 private ColorFormat colorFormat;
32
33 /**
34 * Constructs a new {@code GpxTrack}.
35 * @param trackSegs track segments
36 * @param attributes track attributes
37 */
38 public GpxTrack(Collection<Collection<WayPoint>> trackSegs, Map<String, Object> attributes) {
39 List<IGpxTrackSegment> newSegments = new ArrayList<>();
40 for (Collection<WayPoint> trackSeg: trackSegs) {
41 if (trackSeg != null && !trackSeg.isEmpty()) {
42 newSegments.add(new GpxTrackSegment(trackSeg));
43 }
44 }
45 this.segments = Collections.unmodifiableList(newSegments);
46 this.length = calculateLength();
47 this.bounds = calculateBounds();
48 this.attr = new HashMap<>(attributes);
49 }
50
51 /**
52 * Constructs a new {@code GpxTrack} from {@code GpxTrackSegment} objects.
53 * @param trackSegs The segments to build the track from. Input is not deep-copied,
54 * which means the caller may reuse the same segments to build
55 * multiple GpxTrack instances from. This should not be
56 * a problem, since this object cannot modify {@code this.segments}.
57 * @param attributes Attributes for the GpxTrack, the input map is copied.
58 */
59 public GpxTrack(List<IGpxTrackSegment> trackSegs, Map<String, Object> attributes) {
60 this.attr = new HashMap<>(attributes);
61 this.segments = Collections.unmodifiableList(trackSegs);
62 this.length = calculateLength();
63 this.bounds = calculateBounds();
64 }
65
66 private double calculateLength() {
67 double result = 0.0; // in meters
68
69 for (IGpxTrackSegment trkseg : segments) {
70 result += trkseg.length();
71 }
72 return result;
73 }
74
75 private Bounds calculateBounds() {
76 Bounds result = null;
77 for (IGpxTrackSegment segment: segments) {
78 Bounds segBounds = segment.getBounds();
79 if (segBounds != null) {
80 if (result == null) {
81 result = new Bounds(segBounds);
82 } else {
83 result.extend(segBounds);
84 }
85 }
86 }
87 return result;
88 }
89
90 @Override
91 public void setColor(Color color) {
92 setColorExtension(color);
93 colorCache = color;
94 }
95
96 private void setColorExtension(Color color) {
97 getExtensions().findAndRemove("gpxx", "DisplayColor");
98 if (color == null) {
99 getExtensions().findAndRemove("gpxd", "color");
100 } else {
101 getExtensions().addOrUpdate("gpxd", "color", String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue()));
102 }
103 fireInvalidate();
104 }
105
106 @Override
107 public Color getColor() {
108 if (colorCache == null) {
109 colorCache = getColorFromExtension();
110 }
111 return colorCache;
112 }
113
114 private Color getColorFromExtension() {
115 GpxExtension gpxd = getExtensions().find("gpxd", "color");
116 if (gpxd != null) {
117 colorFormat = ColorFormat.GPXD;
118 String cs = gpxd.getValue();
119 try {
120 return Color.decode(cs);
121 } catch (NumberFormatException ex) {
122 Logging.warn("Could not read gpxd color: " + cs);
123 }
124 } else {
125 GpxExtension gpxx = getExtensions().find("gpxx", "DisplayColor");
126 if (gpxx != null) {
127 colorFormat = ColorFormat.GPXX;
128 String cs = gpxx.getValue();
129 if (cs != null) {
130 Color cc = GARMIN_COLORS.get(cs);
131 if (cc != null) {
132 return cc;
133 }
134 }
135 Logging.warn("Could not read garmin color: " + cs);
136 }
137 }
138 return null;
139 }
140
141 /**
142 * Converts the color to the given format, if present.
143 * @param cFormat can be a {@link GpxConstants.ColorFormat}
144 */
145 public void convertColor(ColorFormat cFormat) {
146 Color c = getColor();
147 if (c == null) return;
148
149 if (cFormat != this.colorFormat) {
150 if (cFormat == null) {
151 // just hide the extensions, don't actually remove them
152 Optional.ofNullable(getExtensions().find("gpxx", "DisplayColor")).ifPresent(GpxExtension::hide);
153 Optional.ofNullable(getExtensions().find("gpxd", "color")).ifPresent(GpxExtension::hide);
154 } else if (cFormat == ColorFormat.GPXX) {
155 getExtensions().findAndRemove("gpxd", "color");
156 String colorString = null;
157 if (closestGarminColorCache.containsKey(c)) {
158 colorString = closestGarminColorCache.get(c);
159 } else {
160 //find closest garmin color
161 double closestDiff = -1;
162 for (Entry<String, Color> e : GARMIN_COLORS.entrySet()) {
163 double diff = colorDist(e.getValue(), c);
164 if (closestDiff < 0 || diff < closestDiff) {
165 colorString = e.getKey();
166 closestDiff = diff;
167 if (closestDiff == 0) break;
168 }
169 }
170 }
171 closestGarminColorCache.put(c, colorString);
172 getExtensions().addIfNotPresent("gpxx", "TrackExtension").getExtensions().addOrUpdate("gpxx", "DisplayColor", colorString);
173 } else if (cFormat == ColorFormat.GPXD) {
174 setColor(c);
175 }
176 colorFormat = cFormat;
177 }
178 }
179
180 private double colorDist(Color c1, Color c2) {
181 // Simple Euclidean distance between two colors
182 return Math.sqrt(Math.pow(c1.getRed() - c2.getRed(), 2)
183 + Math.pow(c1.getGreen() - c2.getGreen(), 2)
184 + Math.pow(c1.getBlue() - c2.getBlue(), 2));
185 }
186
187 @Override
188 public void put(String key, Object value) {
189 super.put(key, value);
190 fireInvalidate();
191 }
192
193 private void fireInvalidate() {
194 listeners.fireEvent(l -> l.gpxDataChanged(new IGpxTrack.GpxTrackChangeEvent(this)));
195 }
196
197 @Override
198 public Bounds getBounds() {
199 return bounds == null ? null : new Bounds(bounds);
200 }
201
202 @Override
203 public double length() {
204 return length;
205 }
206
207 @Override
208 public Collection<IGpxTrackSegment> getSegments() {
209 return segments;
210 }
211
212 @Override
213 public int hashCode() {
214 return 31 * super.hashCode() + ((segments == null) ? 0 : segments.hashCode());
215 }
216
217 @Override
218 public boolean equals(Object obj) {
219 if (this == obj)
220 return true;
221 if (obj == null)
222 return false;
223 if (!super.equals(obj))
224 return false;
225 if (getClass() != obj.getClass())
226 return false;
227 GpxTrack other = (GpxTrack) obj;
228 if (segments == null) {
229 if (other.segments != null)
230 return false;
231 } else if (!segments.equals(other.segments))
232 return false;
233 return true;
234 }
235
236 @Override
237 public void addListener(IGpxTrack.GpxTrackChangeListener l) {
238 listeners.addListener(l);
239 }
240
241 @Override
242 public void removeListener(IGpxTrack.GpxTrackChangeListener l) {
243 listeners.removeListener(l);
244 }
245
246 /**
247 * Resets the color cache
248 */
249 public void invalidate() {
250 colorCache = null;
251 }
252
253 /**
254 * A listener that listens to GPX track changes.
255 * @deprecated use {@link IGpxTrack.GpxTrackChangeListener} instead
256 */
257 @Deprecated
258 @FunctionalInterface
259 interface GpxTrackChangeListener {
260 void gpxDataChanged(GpxTrackChangeEvent e);
261 }
262
263 /**
264 * A track change event for the current track.
265 * @deprecated use {@link IGpxTrack.GpxTrackChangeEvent} instead
266 */
267 @Deprecated
268 static class GpxTrackChangeEvent extends IGpxTrack.GpxTrackChangeEvent {
269 GpxTrackChangeEvent(IGpxTrack source) {
270 super(source);
271 }
272 }
273
274}
Note: See TracBrowser for help on using the repository browser.