[6380] | 1 | // License: GPL. For details, see LICENSE file.
|
---|
[444] | 2 | package org.openstreetmap.josm.data.gpx;
|
---|
| 3 |
|
---|
[15496] | 4 | import java.awt.Color;
|
---|
[444] | 5 | import java.util.Collection;
|
---|
[15496] | 6 | import java.util.Collections;
|
---|
| 7 | import java.util.HashMap;
|
---|
| 8 | import java.util.List;
|
---|
[2907] | 9 | import java.util.Map;
|
---|
[15496] | 10 | import java.util.Map.Entry;
|
---|
| 11 | import java.util.Optional;
|
---|
[444] | 12 |
|
---|
[2907] | 13 | import org.openstreetmap.josm.data.Bounds;
|
---|
[15496] | 14 | import org.openstreetmap.josm.tools.ListenerList;
|
---|
| 15 | import org.openstreetmap.josm.tools.Logging;
|
---|
[16436] | 16 | import org.openstreetmap.josm.tools.StreamUtils;
|
---|
[2151] | 17 |
|
---|
[2907] | 18 | /**
|
---|
[15496] | 19 | * GPX track.
|
---|
| 20 | * Note that the color attributes are not immutable and may be modified by the user.
|
---|
| 21 | * @since 15496
|
---|
[2907] | 22 | */
|
---|
[15496] | 23 | public class GpxTrack extends WithAttributes implements IGpxTrack {
|
---|
[2907] | 24 |
|
---|
[15496] | 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;
|
---|
[8510] | 32 |
|
---|
[9949] | 33 | /**
|
---|
[15496] | 34 | * Constructs a new {@code GpxTrack}.
|
---|
| 35 | * @param trackSegs track segments
|
---|
| 36 | * @param attributes track attributes
|
---|
[9949] | 37 | */
|
---|
[15496] | 38 | public GpxTrack(Collection<Collection<WayPoint>> trackSegs, Map<String, Object> attributes) {
|
---|
[16436] | 39 | this.segments = trackSegs.stream()
|
---|
| 40 | .filter(trackSeg -> trackSeg != null && !trackSeg.isEmpty())
|
---|
| 41 | .map(GpxTrackSegment::new)
|
---|
| 42 | .collect(StreamUtils.toUnmodifiableList());
|
---|
[15496] | 43 | this.length = calculateLength();
|
---|
| 44 | this.bounds = calculateBounds();
|
---|
| 45 | this.attr = new HashMap<>(attributes);
|
---|
| 46 | }
|
---|
[8510] | 47 |
|
---|
[9949] | 48 | /**
|
---|
[15496] | 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.
|
---|
[9949] | 55 | */
|
---|
[15496] | 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 | }
|
---|
[8510] | 62 |
|
---|
[15496] | 63 | private double calculateLength() {
|
---|
[16436] | 64 | return segments.stream().mapToDouble(IGpxTrackSegment::length).sum();
|
---|
[15496] | 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) {
|
---|
[15497] | 89 | getExtensions().findAndRemove("gpxx", "DisplayColor");
|
---|
[15496] | 90 | if (color == null) {
|
---|
[15497] | 91 | getExtensions().findAndRemove("gpxd", "color");
|
---|
[15496] | 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 | GpxExtension gpxd = getExtensions().find("gpxd", "color");
|
---|
| 108 | if (gpxd != null) {
|
---|
| 109 | colorFormat = ColorFormat.GPXD;
|
---|
| 110 | String cs = gpxd.getValue();
|
---|
| 111 | try {
|
---|
| 112 | return Color.decode(cs);
|
---|
| 113 | } catch (NumberFormatException ex) {
|
---|
| 114 | Logging.warn("Could not read gpxd color: " + cs);
|
---|
| 115 | }
|
---|
| 116 | } else {
|
---|
| 117 | GpxExtension gpxx = getExtensions().find("gpxx", "DisplayColor");
|
---|
| 118 | if (gpxx != null) {
|
---|
| 119 | colorFormat = ColorFormat.GPXX;
|
---|
| 120 | String cs = gpxx.getValue();
|
---|
| 121 | if (cs != null) {
|
---|
| 122 | Color cc = GARMIN_COLORS.get(cs);
|
---|
| 123 | if (cc != null) {
|
---|
| 124 | return cc;
|
---|
| 125 | }
|
---|
| 126 | }
|
---|
| 127 | Logging.warn("Could not read garmin color: " + cs);
|
---|
| 128 | }
|
---|
| 129 | }
|
---|
| 130 | return null;
|
---|
| 131 | }
|
---|
| 132 |
|
---|
[9949] | 133 | /**
|
---|
[15496] | 134 | * Converts the color to the given format, if present.
|
---|
| 135 | * @param cFormat can be a {@link GpxConstants.ColorFormat}
|
---|
[9949] | 136 | */
|
---|
[15496] | 137 | public void convertColor(ColorFormat cFormat) {
|
---|
| 138 | Color c = getColor();
|
---|
| 139 | if (c == null) return;
|
---|
[7509] | 140 |
|
---|
[15496] | 141 | if (cFormat != this.colorFormat) {
|
---|
| 142 | if (cFormat == null) {
|
---|
| 143 | // just hide the extensions, don't actually remove them
|
---|
| 144 | Optional.ofNullable(getExtensions().find("gpxx", "DisplayColor")).ifPresent(GpxExtension::hide);
|
---|
| 145 | Optional.ofNullable(getExtensions().find("gpxd", "color")).ifPresent(GpxExtension::hide);
|
---|
| 146 | } else if (cFormat == ColorFormat.GPXX) {
|
---|
| 147 | getExtensions().findAndRemove("gpxd", "color");
|
---|
| 148 | String colorString = null;
|
---|
| 149 | if (closestGarminColorCache.containsKey(c)) {
|
---|
| 150 | colorString = closestGarminColorCache.get(c);
|
---|
| 151 | } else {
|
---|
| 152 | //find closest garmin color
|
---|
| 153 | double closestDiff = -1;
|
---|
| 154 | for (Entry<String, Color> e : GARMIN_COLORS.entrySet()) {
|
---|
| 155 | double diff = colorDist(e.getValue(), c);
|
---|
| 156 | if (closestDiff < 0 || diff < closestDiff) {
|
---|
| 157 | colorString = e.getKey();
|
---|
| 158 | closestDiff = diff;
|
---|
| 159 | if (closestDiff == 0) break;
|
---|
| 160 | }
|
---|
| 161 | }
|
---|
| 162 | }
|
---|
| 163 | closestGarminColorCache.put(c, colorString);
|
---|
[15560] | 164 | getExtensions().addIfNotPresent("gpxx", "TrackExtension").getExtensions().addOrUpdate("gpxx", "DisplayColor", colorString);
|
---|
[15496] | 165 | } else if (cFormat == ColorFormat.GPXD) {
|
---|
| 166 | setColor(c);
|
---|
| 167 | }
|
---|
| 168 | colorFormat = cFormat;
|
---|
| 169 | }
|
---|
[12156] | 170 | }
|
---|
| 171 |
|
---|
[15496] | 172 | private double colorDist(Color c1, Color c2) {
|
---|
| 173 | // Simple Euclidean distance between two colors
|
---|
| 174 | return Math.sqrt(Math.pow(c1.getRed() - c2.getRed(), 2)
|
---|
| 175 | + Math.pow(c1.getGreen() - c2.getGreen(), 2)
|
---|
| 176 | + Math.pow(c1.getBlue() - c2.getBlue(), 2));
|
---|
| 177 | }
|
---|
| 178 |
|
---|
| 179 | @Override
|
---|
| 180 | public void put(String key, Object value) {
|
---|
| 181 | super.put(key, value);
|
---|
| 182 | fireInvalidate();
|
---|
| 183 | }
|
---|
| 184 |
|
---|
| 185 | private void fireInvalidate() {
|
---|
| 186 | listeners.fireEvent(l -> l.gpxDataChanged(new IGpxTrack.GpxTrackChangeEvent(this)));
|
---|
| 187 | }
|
---|
| 188 |
|
---|
| 189 | @Override
|
---|
| 190 | public Bounds getBounds() {
|
---|
| 191 | return bounds == null ? null : new Bounds(bounds);
|
---|
| 192 | }
|
---|
| 193 |
|
---|
| 194 | @Override
|
---|
| 195 | public double length() {
|
---|
| 196 | return length;
|
---|
| 197 | }
|
---|
| 198 |
|
---|
| 199 | @Override
|
---|
| 200 | public Collection<IGpxTrackSegment> getSegments() {
|
---|
| 201 | return segments;
|
---|
| 202 | }
|
---|
| 203 |
|
---|
| 204 | @Override
|
---|
| 205 | public int hashCode() {
|
---|
| 206 | return 31 * super.hashCode() + ((segments == null) ? 0 : segments.hashCode());
|
---|
| 207 | }
|
---|
| 208 |
|
---|
| 209 | @Override
|
---|
| 210 | public boolean equals(Object obj) {
|
---|
| 211 | if (this == obj)
|
---|
| 212 | return true;
|
---|
| 213 | if (obj == null)
|
---|
| 214 | return false;
|
---|
| 215 | if (!super.equals(obj))
|
---|
| 216 | return false;
|
---|
| 217 | if (getClass() != obj.getClass())
|
---|
| 218 | return false;
|
---|
| 219 | GpxTrack other = (GpxTrack) obj;
|
---|
| 220 | if (segments == null) {
|
---|
| 221 | if (other.segments != null)
|
---|
| 222 | return false;
|
---|
| 223 | } else if (!segments.equals(other.segments))
|
---|
| 224 | return false;
|
---|
| 225 | return true;
|
---|
| 226 | }
|
---|
| 227 |
|
---|
| 228 | @Override
|
---|
| 229 | public void addListener(IGpxTrack.GpxTrackChangeListener l) {
|
---|
| 230 | listeners.addListener(l);
|
---|
| 231 | }
|
---|
| 232 |
|
---|
| 233 | @Override
|
---|
| 234 | public void removeListener(IGpxTrack.GpxTrackChangeListener l) {
|
---|
| 235 | listeners.removeListener(l);
|
---|
| 236 | }
|
---|
| 237 |
|
---|
[12156] | 238 | /**
|
---|
[15496] | 239 | * Resets the color cache
|
---|
[12156] | 240 | */
|
---|
[15496] | 241 | public void invalidate() {
|
---|
| 242 | colorCache = null;
|
---|
[12156] | 243 | }
|
---|
| 244 |
|
---|
| 245 | /**
|
---|
| 246 | * A listener that listens to GPX track changes.
|
---|
[15496] | 247 | * @deprecated use {@link IGpxTrack.GpxTrackChangeListener} instead
|
---|
[12156] | 248 | */
|
---|
[15496] | 249 | @Deprecated
|
---|
[12156] | 250 | @FunctionalInterface
|
---|
[12171] | 251 | interface GpxTrackChangeListener {
|
---|
[12156] | 252 | void gpxDataChanged(GpxTrackChangeEvent e);
|
---|
| 253 | }
|
---|
| 254 |
|
---|
| 255 | /**
|
---|
| 256 | * A track change event for the current track.
|
---|
[15496] | 257 | * @deprecated use {@link IGpxTrack.GpxTrackChangeEvent} instead
|
---|
[12156] | 258 | */
|
---|
[15496] | 259 | @Deprecated
|
---|
| 260 | static class GpxTrackChangeEvent extends IGpxTrack.GpxTrackChangeEvent {
|
---|
| 261 | GpxTrackChangeEvent(IGpxTrack source) {
|
---|
| 262 | super(source);
|
---|
[12156] | 263 | }
|
---|
[15496] | 264 | }
|
---|
[12156] | 265 |
|
---|
[444] | 266 | }
|
---|