001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import java.awt.Color; 005import java.util.ArrayList; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.HashMap; 009import java.util.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.Optional; 013 014import org.openstreetmap.josm.data.Bounds; 015import org.openstreetmap.josm.tools.ListenerList; 016import org.openstreetmap.josm.tools.Logging; 017 018/** 019 * GPX track. 020 * Note that the color attributes are not immutable and may be modified by the user. 021 * @since 15496 022 */ 023public class GpxTrack extends WithAttributes implements IGpxTrack { 024 025 private final List<IGpxTrackSegment> segments; 026 private final double length; 027 private final Bounds bounds; 028 private Color colorCache; 029 private final ListenerList<IGpxTrack.GpxTrackChangeListener> listeners = ListenerList.create(); 030 private static final HashMap<Color, String> closestGarminColorCache = new HashMap<>(); 031 private ColorFormat colorFormat; 032 033 /** 034 * Constructs a new {@code GpxTrack}. 035 * @param trackSegs track segments 036 * @param attributes track attributes 037 */ 038 public GpxTrack(Collection<Collection<WayPoint>> trackSegs, Map<String, Object> attributes) { 039 List<IGpxTrackSegment> newSegments = new ArrayList<>(); 040 for (Collection<WayPoint> trackSeg: trackSegs) { 041 if (trackSeg != null && !trackSeg.isEmpty()) { 042 newSegments.add(new GpxTrackSegment(trackSeg)); 043 } 044 } 045 this.segments = Collections.unmodifiableList(newSegments); 046 this.length = calculateLength(); 047 this.bounds = calculateBounds(); 048 this.attr = new HashMap<>(attributes); 049 } 050 051 /** 052 * Constructs a new {@code GpxTrack} from {@code GpxTrackSegment} objects. 053 * @param trackSegs The segments to build the track from. Input is not deep-copied, 054 * which means the caller may reuse the same segments to build 055 * multiple GpxTrack instances from. This should not be 056 * a problem, since this object cannot modify {@code this.segments}. 057 * @param attributes Attributes for the GpxTrack, the input map is copied. 058 */ 059 public GpxTrack(List<IGpxTrackSegment> trackSegs, Map<String, Object> attributes) { 060 this.attr = new HashMap<>(attributes); 061 this.segments = Collections.unmodifiableList(trackSegs); 062 this.length = calculateLength(); 063 this.bounds = calculateBounds(); 064 } 065 066 private double calculateLength() { 067 double result = 0.0; // in meters 068 069 for (IGpxTrackSegment trkseg : segments) { 070 result += trkseg.length(); 071 } 072 return result; 073 } 074 075 private Bounds calculateBounds() { 076 Bounds result = null; 077 for (IGpxTrackSegment segment: segments) { 078 Bounds segBounds = segment.getBounds(); 079 if (segBounds != null) { 080 if (result == null) { 081 result = new Bounds(segBounds); 082 } else { 083 result.extend(segBounds); 084 } 085 } 086 } 087 return result; 088 } 089 090 @Override 091 public void setColor(Color color) { 092 setColorExtension(color); 093 colorCache = color; 094 } 095 096 private void setColorExtension(Color color) { 097 getExtensions().findAndRemove("gpxx", "DisplayColor"); 098 if (color == null) { 099 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", "TrackExtensions").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}