001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.gpx; 003 004import java.awt.Color; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.HashMap; 008import java.util.List; 009import java.util.Map; 010import java.util.Map.Entry; 011import java.util.Optional; 012 013import org.openstreetmap.josm.data.Bounds; 014import org.openstreetmap.josm.tools.ListenerList; 015import org.openstreetmap.josm.tools.Logging; 016import org.openstreetmap.josm.tools.StreamUtils; 017import org.openstreetmap.josm.tools.Utils; 018 019/** 020 * GPX track. 021 * Note that the color attributes are not immutable and may be modified by the user. 022 * @since 15496 023 */ 024public class GpxTrack extends WithAttributes implements IGpxTrack { 025 026 private final List<IGpxTrackSegment> segments; 027 private final double length; 028 private final Bounds bounds; 029 private Color colorCache; 030 private final ListenerList<IGpxTrack.GpxTrackChangeListener> listeners = ListenerList.create(); 031 private static final HashMap<Color, String> closestGarminColorCache = new HashMap<>(); 032 private ColorFormat colorFormat; 033 034 /** 035 * Constructs a new {@code GpxTrack}. 036 * @param trackSegs track segments 037 * @param attributes track attributes 038 */ 039 public GpxTrack(Collection<Collection<WayPoint>> trackSegs, Map<String, Object> attributes) { 040 this.segments = trackSegs.stream() 041 .filter(trackSeg -> !Utils.isEmpty(trackSeg)) 042 .map(GpxTrackSegment::new) 043 .collect(StreamUtils.toUnmodifiableList()); 044 this.length = calculateLength(); 045 this.bounds = calculateBounds(); 046 this.attr = new HashMap<>(attributes); 047 } 048 049 /** 050 * Constructs a new {@code GpxTrack} from {@code GpxTrackSegment} objects. 051 * @param trackSegs The segments to build the track from. Input is not deep-copied, 052 * which means the caller may reuse the same segments to build 053 * multiple GpxTrack instances from. This should not be 054 * a problem, since this object cannot modify {@code this.segments}. 055 * @param attributes Attributes for the GpxTrack, the input map is copied. 056 */ 057 public GpxTrack(List<IGpxTrackSegment> trackSegs, Map<String, Object> attributes) { 058 this.attr = new HashMap<>(attributes); 059 this.segments = Collections.unmodifiableList(trackSegs); 060 this.length = calculateLength(); 061 this.bounds = calculateBounds(); 062 } 063 064 private double calculateLength() { 065 return segments.stream().mapToDouble(IGpxTrackSegment::length).sum(); 066 } 067 068 private Bounds calculateBounds() { 069 Bounds result = null; 070 for (IGpxTrackSegment segment: segments) { 071 Bounds segBounds = segment.getBounds(); 072 if (segBounds != null) { 073 if (result == null) { 074 result = new Bounds(segBounds); 075 } else { 076 result.extend(segBounds); 077 } 078 } 079 } 080 return result; 081 } 082 083 @Override 084 public void setColor(Color color) { 085 setColorExtension(color); 086 colorCache = color; 087 } 088 089 private void setColorExtension(Color color) { 090 getExtensions().findAndRemove("gpxx", "DisplayColor"); 091 if (color == null) { 092 getExtensions().findAndRemove("gpxd", "color"); 093 } else { 094 getExtensions().addOrUpdate("gpxd", "color", String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue())); 095 } 096 fireInvalidate(); 097 } 098 099 @Override 100 public Color getColor() { 101 if (colorCache == null) { 102 colorCache = getColorFromExtension(); 103 } 104 return colorCache; 105 } 106 107 private Color getColorFromExtension() { 108 if (!hasExtensions()) { 109 return null; 110 } 111 GpxExtension gpxd = getExtensions().find("gpxd", "color"); 112 if (gpxd != null) { 113 colorFormat = ColorFormat.GPXD; 114 String cs = gpxd.getValue(); 115 try { 116 return Color.decode(cs); 117 } catch (NumberFormatException ex) { 118 Logging.warn("Could not read gpxd color: " + cs); 119 } 120 } else { 121 GpxExtension gpxx = getExtensions().find("gpxx", "DisplayColor"); 122 if (gpxx != null) { 123 colorFormat = ColorFormat.GPXX; 124 String cs = gpxx.getValue(); 125 if (cs != null) { 126 Color cc = GARMIN_COLORS.get(cs); 127 if (cc != null) { 128 return cc; 129 } 130 } 131 Logging.warn("Could not read garmin color: " + cs); 132 } 133 } 134 return null; 135 } 136 137 /** 138 * Converts the color to the given format, if present. 139 * @param cFormat can be a {@link GpxConstants.ColorFormat} 140 */ 141 public void convertColor(ColorFormat cFormat) { 142 Color c = getColor(); 143 if (c == null) return; 144 145 if (cFormat != this.colorFormat) { 146 if (cFormat == null) { 147 // just hide the extensions, don't actually remove them 148 Optional.ofNullable(getExtensions().find("gpxx", "DisplayColor")).ifPresent(GpxExtension::hide); 149 Optional.ofNullable(getExtensions().find("gpxd", "color")).ifPresent(GpxExtension::hide); 150 } else if (cFormat == ColorFormat.GPXX) { 151 getExtensions().findAndRemove("gpxd", "color"); 152 String colorString = null; 153 if (closestGarminColorCache.containsKey(c)) { 154 colorString = closestGarminColorCache.get(c); 155 } else { 156 //find closest garmin color 157 double closestDiff = -1; 158 for (Entry<String, Color> e : GARMIN_COLORS.entrySet()) { 159 double diff = colorDist(e.getValue(), c); 160 if (closestDiff < 0 || diff < closestDiff) { 161 colorString = e.getKey(); 162 closestDiff = diff; 163 if (closestDiff == 0) break; 164 } 165 } 166 } 167 closestGarminColorCache.put(c, colorString); 168 getExtensions().addIfNotPresent("gpxx", "TrackExtension").getExtensions().addOrUpdate("gpxx", "DisplayColor", colorString); 169 } else if (cFormat == ColorFormat.GPXD) { 170 setColor(c); 171 } 172 colorFormat = cFormat; 173 } 174 } 175 176 private double colorDist(Color c1, Color c2) { 177 // Simple Euclidean distance between two colors 178 return Math.sqrt(Math.pow(c1.getRed() - c2.getRed(), 2) 179 + Math.pow(c1.getGreen() - c2.getGreen(), 2) 180 + Math.pow(c1.getBlue() - c2.getBlue(), 2)); 181 } 182 183 @Override 184 public void put(String key, Object value) { 185 super.put(key, value); 186 fireInvalidate(); 187 } 188 189 private void fireInvalidate() { 190 listeners.fireEvent(l -> l.gpxDataChanged(new IGpxTrack.GpxTrackChangeEvent(this))); 191 } 192 193 @Override 194 public Bounds getBounds() { 195 return bounds == null ? null : new Bounds(bounds); 196 } 197 198 @Override 199 public double length() { 200 return length; 201 } 202 203 @Override 204 public Collection<IGpxTrackSegment> getSegments() { 205 return segments; 206 } 207 208 @Override 209 public int hashCode() { 210 return 31 * super.hashCode() + ((segments == null) ? 0 : segments.hashCode()); 211 } 212 213 @Override 214 public boolean equals(Object obj) { 215 if (this == obj) 216 return true; 217 if (obj == null) 218 return false; 219 if (!super.equals(obj)) 220 return false; 221 if (getClass() != obj.getClass()) 222 return false; 223 GpxTrack other = (GpxTrack) obj; 224 if (segments == null) { 225 if (other.segments != null) 226 return false; 227 } else if (!segments.equals(other.segments)) 228 return false; 229 return true; 230 } 231 232 @Override 233 public void addListener(IGpxTrack.GpxTrackChangeListener l) { 234 listeners.addListener(l); 235 } 236 237 @Override 238 public void removeListener(IGpxTrack.GpxTrackChangeListener l) { 239 listeners.removeListener(l); 240 } 241 242 /** 243 * Resets the color cache 244 */ 245 public void invalidate() { 246 colorCache = null; 247 } 248}