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

Last change on this file since 17846 was 17846, checked in by simon04, 3 years ago

fix #20793 - Reduce memory consumption for GpxExtensionCollection (patch by Bjoeni, modified)

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