Changeset 15496 in josm for trunk/src/org


Ignore:
Timestamp:
2019-11-02T15:11:34+01:00 (5 years ago)
Author:
Don-vip
Message:

fix #16796 - Rework of GPX track colors / layer preferences (patch by Bjoeni)

Location:
trunk/src/org/openstreetmap/josm
Files:
4 added
1 deleted
47 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/org/openstreetmap/josm/actions/SaveAction.java

    r15404 r15496  
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66
     7import java.awt.GridBagLayout;
    78import java.awt.event.KeyEvent;
    89import java.beans.PropertyChangeListener;
    910import java.io.File;
    1011
     12import javax.swing.JCheckBox;
     13import javax.swing.JLabel;
     14import javax.swing.JPanel;
     15import javax.swing.SwingConstants;
     16
     17import org.openstreetmap.josm.data.gpx.GpxData.GpxDataChangeListener;
    1118import org.openstreetmap.josm.gui.ExtendedDialog;
    1219import org.openstreetmap.josm.gui.MainApplication;
     
    1724import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    1825import org.openstreetmap.josm.gui.layer.SaveToFile;
     26import org.openstreetmap.josm.gui.util.GuiHelper;
     27import org.openstreetmap.josm.spi.preferences.Config;
     28import org.openstreetmap.josm.tools.GBC;
    1929import org.openstreetmap.josm.tools.Shortcut;
    2030
     
    3343    };
    3444
     45    private final GpxDataChangeListener updateOnRequireSaveChangeGpx = evt -> updateEnabledState();
     46
    3547    /**
    3648     * Construct the action with "Save" as label.
     
    3850    private SaveAction() {
    3951        super(tr("Save"), "save", tr("Save the current data."),
    40                 Shortcut.registerShortcut("system:save", tr("File: {0}", tr("Save")), KeyEvent.VK_S, Shortcut.CTRL));
     52                Shortcut.registerShortcut("system:save", tr("File: {0}", tr("Save")), KeyEvent.VK_S, Shortcut.CTRL),
     53                true);
    4154        setHelpId(ht("/Action/Save"));
    4255    }
     
    5568            @Override
    5669            public void layerAdded(LayerAddEvent e) {
    57                 if (e.getAddedLayer() instanceof OsmDataLayer) {
    58                     e.getAddedLayer().addPropertyChangeListener(updateOnRequireSaveChange);
     70                Layer l = e.getAddedLayer();
     71                if (l instanceof OsmDataLayer) {
     72                    l.addPropertyChangeListener(updateOnRequireSaveChange);
     73                }
     74                if (l instanceof GpxLayer) {
     75                    ((GpxLayer) l).data.addWeakChangeListener(updateOnRequireSaveChangeGpx);
    5976                }
    6077                super.layerAdded(e);
     
    6380            @Override
    6481            public void layerRemoving(LayerRemoveEvent e) {
    65                 if (e.getRemovedLayer() instanceof OsmDataLayer) {
    66                     e.getRemovedLayer().removePropertyChangeListener(updateOnRequireSaveChange);
     82                Layer l = e.getRemovedLayer();
     83                if (l instanceof OsmDataLayer) {
     84                    l.removePropertyChangeListener(updateOnRequireSaveChange);
     85                }
     86                if (l instanceof GpxLayer) {
     87                    ((GpxLayer) l).data.removeChangeListener(updateOnRequireSaveChangeGpx);
    6788                }
    6889                super.layerRemoving(e);
     
    7495    protected void updateEnabledState() {
    7596        Layer activeLayer = getLayerManager().getActiveLayer();
    76         setEnabled(activeLayer != null && activeLayer.isSavable()
    77                 && (!(activeLayer.getAssociatedFile() != null
    78                     && activeLayer instanceof SaveToFile && !((SaveToFile) activeLayer).requiresSaveToFile())));
     97        boolean en = activeLayer != null
     98                && activeLayer.isSavable() && (!(activeLayer.getAssociatedFile() != null
     99                && activeLayer instanceof SaveToFile && !((SaveToFile) activeLayer).requiresSaveToFile()));
     100        GuiHelper.runInEDT(() -> setEnabled(en));
    79101    }
    80102
     
    86108        }
    87109
    88         // Ask for overwrite in case of GpxLayer: GpxLayers usually are imports
    89         // and modifying is an error most of the time.
    90         if (f != null && layer instanceof GpxLayer) {
     110        // Ask for overwrite in case of GpxLayer
     111        if (f != null && layer instanceof GpxLayer && !Config.getPref().getBoolean("gpx.export.overwrite", false)) {
     112            JPanel p = new JPanel(new GridBagLayout());
     113            JLabel label = new JLabel(tr("File {0} exists. Overwrite?", f.getName()));
     114            label.setHorizontalAlignment(SwingConstants.CENTER);
     115            JCheckBox remember = new JCheckBox(tr("Remember choice"));
     116            remember.setHorizontalAlignment(SwingConstants.CENTER);
     117            p.add(label, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 5, 5, 10));
     118            p.add(remember, GBC.eop().fill(GBC.HORIZONTAL));
    91119            ExtendedDialog dialog = new ExtendedDialog(
    92120                    MainApplication.getMainFrame(),
     
    94122                    tr("Overwrite"), tr("Cancel"))
    95123                .setButtonIcons("save_as", "cancel")
    96                 .setContent(tr("File {0} exists. Overwrite?", f.getName()));
     124                .setContent(p);
    97125            if (dialog.showDialog().getValue() != 1) {
    98126                f = null;
     127            } else if (remember.isSelected()) {
     128                Config.getPref().putBoolean("gpx.export.overwrite", true);
    99129            }
    100130        }
  • trunk/src/org/openstreetmap/josm/actions/SaveActionBase.java

    r15404 r15496  
    2020import org.openstreetmap.josm.gui.MainApplication;
    2121import org.openstreetmap.josm.gui.io.importexport.FileExporter;
     22import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
    2223import org.openstreetmap.josm.gui.layer.Layer;
    23 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
    24 import org.openstreetmap.josm.gui.layer.SaveToFile;
    2524import org.openstreetmap.josm.gui.util.GuiHelper;
    2625import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
     
    3635public abstract class SaveActionBase extends DiskAccessAction {
    3736
     37    private boolean quiet;
     38
    3839    /**
    3940     * Constructs a new {@code SaveActionBase}.
     
    4748    }
    4849
     50    /**
     51     * Constructs a new {@code SaveActionBase}.
     52     * @param name The action's text as displayed on the menu (if it is added to a menu)
     53     * @param iconName The filename of the icon to use
     54     * @param tooltip A longer description of the action that will be displayed in the tooltip
     55     * @param shortcut A ready-created shortcut object or {@code null} if you don't want a shortcut
     56     * @param quiet whether the quiet exporter is called
     57     * @since 15496
     58     */
     59    public SaveActionBase(String name, String iconName, String tooltip, Shortcut shortcut, boolean quiet) {
     60        super(name, iconName, tooltip, shortcut);
     61        this.quiet = quiet;
     62    }
     63
    4964    @Override
    5065    public void actionPerformed(ActionEvent e) {
    5166        if (!isEnabled())
    5267            return;
    53         doSave();
     68        doSave(quiet);
    5469    }
    5570
     
    5974     */
    6075    public boolean doSave() {
     76        return doSave(false);
     77    }
     78
     79    /**
     80     * Saves the active layer.
     81     * @param quiet If the file is saved without prompting the user
     82     * @return {@code true} if the save operation succeeds
     83     * @since 15496
     84     */
     85    public boolean doSave(boolean quiet) {
    6186        Layer layer = getLayerManager().getActiveLayer();
    6287        if (layer != null && layer.isSavable()) {
    63             return doSave(layer);
     88            return doSave(layer, quiet);
    6489        }
    6590        return false;
     
    7297     */
    7398    public boolean doSave(Layer layer) {
     99        return doSave(layer, false);
     100    }
     101
     102    /**
     103     * Saves the given layer.
     104     * @param layer layer to save
     105     * @param quiet If the file is saved without prompting the user
     106     * @return {@code true} if the save operation succeeds
     107     * @since 15496
     108     */
     109    public boolean doSave(Layer layer, boolean quiet) {
    74110        if (!layer.checkSaveConditions())
    75111            return false;
    76         final boolean requiresSave = layer instanceof SaveToFile && ((SaveToFile) layer).requiresSaveToFile();
    77         final boolean result = doInternalSave(layer, getFile(layer));
    78         if (!requiresSave) {
    79             updateEnabledState();
    80         }
     112        final boolean result = doInternalSave(layer, getFile(layer), quiet);
     113        updateEnabledState();
    81114        return result;
    82115    }
     
    87120     * @param file The destination file
    88121     * @param checkSaveConditions if {@code true}, checks preconditions before saving. Set it to {@code false} to skip it
    89      * if preconditions have already been checked (as this check can prompt UI dialog in EDT it may be best in some cases
    90      * to do it earlier).
     122     * and prevent dialogs from being shown.
    91123     * @return {@code true} if the layer has been successfully saved, {@code false} otherwise
    92124     * @since 7204
     
    95127        if (checkSaveConditions && !layer.checkSaveConditions())
    96128            return false;
    97         return doInternalSave(layer, file);
    98     }
    99 
    100     private static boolean doInternalSave(Layer layer, File file) {
     129        return doInternalSave(layer, file, !checkSaveConditions);
     130    }
     131
     132    private static boolean doInternalSave(Layer layer, File file, boolean quiet) {
    101133        if (file == null)
    102134            return false;
     
    107139            for (FileExporter exporter : ExtensionFileFilter.getExporters()) {
    108140                if (exporter.acceptFile(file, layer)) {
    109                     exporter.exportData(file, layer);
     141                    if (quiet) {
     142                        exporter.exportDataQuiet(file, layer);
     143                    } else {
     144                        exporter.exportData(file, layer);
     145                    }
    110146                    exported = true;
    111147                    canceled = exporter.isCanceled();
     
    125161            }
    126162            layer.setAssociatedFile(file);
    127             if (layer instanceof OsmDataLayer) {
    128                 ((OsmDataLayer) layer).onPostSaveToFile();
     163            if (layer instanceof AbstractModifiableLayer) {
     164                ((AbstractModifiableLayer) layer).onPostSaveToFile();
    129165            }
    130166        } catch (IOException | InvalidPathException e) {
  • trunk/src/org/openstreetmap/josm/actions/SaveAsAction.java

    r14397 r15496  
    2525        super(tr("Save As..."), "save_as", tr("Save the current data to a new file."),
    2626            Shortcut.registerShortcut("system:saveas", tr("File: {0}", tr("Save As...")),
    27             KeyEvent.VK_S, Shortcut.CTRL_SHIFT));
     27            KeyEvent.VK_S, Shortcut.CTRL_SHIFT), false);
    2828        setHelpId(ht("/Action/SaveAs"));
    2929    }
  • trunk/src/org/openstreetmap/josm/actions/ShowStatusReportAction.java

    r15206 r15496  
    294294
    295295        Preferences.main().getAllSettings().forEach((key, setting) -> {
    296             if (key.startsWith("marker.show")
    297                     || "file-open.history".equals(key)
     296            if ("file-open.history".equals(key)
    298297                    || "download.overpass.query".equals(key)
    299298                    || "download.overpass.queries".equals(key)
  • trunk/src/org/openstreetmap/josm/actions/relation/ExportRelationToGpxAction.java

    r15397 r15496  
    2626import org.openstreetmap.josm.actions.IPrimitiveAction;
    2727import org.openstreetmap.josm.data.gpx.GpxData;
    28 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
     28import org.openstreetmap.josm.data.gpx.GpxTrack;
    2929import org.openstreetmap.josm.data.gpx.WayPoint;
    3030import org.openstreetmap.josm.data.osm.IPrimitive;
     
    155155                if (!wayConnectionType.isOnewayLoopBackwardPart && !wayConnectionType.direction.isRoundabout()) {
    156156                    if (!wayConnectionType.linkPrev && !trkseg.isEmpty()) {
    157                         gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
     157                        gpxData.addTrack(new GpxTrack(trk, trkAttr));
    158158                        trkAttr.clear();
    159159                        trk.clear();
     
    180180                }
    181181            }
    182             gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
     182            gpxData.addTrack(new GpxTrack(trk, trkAttr));
    183183
    184184            String lprefix = relations.iterator().next().getName();
  • trunk/src/org/openstreetmap/josm/data/Preferences.java

    r15469 r15496  
    2222import java.util.HashSet;
    2323import java.util.Iterator;
     24import java.util.List;
    2425import java.util.Map;
    2526import java.util.Map.Entry;
     
    3031import java.util.concurrent.TimeUnit;
    3132import java.util.function.Predicate;
     33import java.util.stream.Collectors;
    3234import java.util.stream.Stream;
    3335
     
    8284public class Preferences extends AbstractPreferences {
    8385
     86    /** remove if key equals */
    8487    private static final String[] OBSOLETE_PREF_KEYS = {
    8588        "remotecontrol.https.enabled", /* remove entry after Dec. 2019 */
     
    8790    };
    8891
     92    /** remove if key starts with */
     93    private static final String[] OBSOLETE_PREF_KEYS_START = {
     94            //only remove layer specific prefs
     95            "draw.rawgps.layer.wpt.",
     96            "draw.rawgps.layer.audiowpt.",
     97            "draw.rawgps.lines.force.",
     98            "draw.rawgps.lines.alpha-blend.",
     99            "draw.rawgps.lines.",
     100            "markers.show ", //uses space as separator
     101            "marker.makeautomarker.",
     102            "clr.layer.",
     103
     104            //remove both layer specific and global prefs
     105            "draw.rawgps.colors",
     106            "draw.rawgps.direction",
     107            "draw.rawgps.alternatedirection",
     108            "draw.rawgps.linewidth",
     109            "draw.rawgps.max-line-length.local",
     110            "draw.rawgps.max-line-length",
     111            "draw.rawgps.large",
     112            "draw.rawgps.large.size",
     113            "draw.rawgps.hdopcircle",
     114            "draw.rawgps.min-arrow-distance",
     115            "draw.rawgps.colorTracksTune",
     116            "draw.rawgps.colors.dynamic",
     117            "draw.rawgps.lines.local",
     118            "draw.rawgps.heatmap"
     119    };
     120
     121    /** keep subkey even if it starts with any of {@link #OBSOLETE_PREF_KEYS_START} */
     122    private static final List<String> KEEP_PREF_KEYS = Arrays.asList(
     123            "draw.rawgps.lines.alpha-blend",
     124            "draw.rawgps.lines.arrows",
     125            "draw.rawgps.lines.arrows.fast",
     126            "draw.rawgps.lines.arrows.min-distance",
     127            "draw.rawgps.lines.force",
     128            "draw.rawgps.lines.max-length",
     129            "draw.rawgps.lines.max-length.local",
     130            "draw.rawgps.lines.width"
     131    );
     132
     133    /** rename keys that equal */
     134    private final static Map<String, String> UPDATE_PREF_KEYS = getUpdatePrefKeys();
     135
     136    private static Map<String, String> getUpdatePrefKeys() {
     137        HashMap<String, String> m = new HashMap<>();
     138        m.put("draw.rawgps.direction", "draw.rawgps.lines.arrows");
     139        m.put("draw.rawgps.alternatedirection", "draw.rawgps.lines.arrows.fast");
     140        m.put("draw.rawgps.min-arrow-distance", "draw.rawgps.lines.arrows.min-distance");
     141        m.put("draw.rawgps.linewidth", "draw.rawgps.lines.width");
     142        m.put("draw.rawgps.max-line-length.local", "draw.rawgps.lines.max-length.local");
     143        m.put("draw.rawgps.max-line-length", "draw.rawgps.lines.max-length");
     144        m.put("draw.rawgps.large", "draw.rawgps.points.large");
     145        m.put("draw.rawgps.large.alpha", "draw.rawgps.points.large.alpha");
     146        m.put("draw.rawgps.large.size", "draw.rawgps.points.large.size");
     147        m.put("draw.rawgps.hdopcircle", "draw.rawgps.points.hdopcircle");
     148        m.put("draw.rawgps.layer.wpt.pattern", "draw.rawgps.markers.pattern");
     149        m.put("draw.rawgps.layer.audiowpt.pattern", "draw.rawgps.markers.audio.pattern");
     150        m.put("draw.rawgps.colors", "draw.rawgps.colormode");
     151        m.put("draw.rawgps.colorTracksTune", "draw.rawgps.colormode.velocity.tune");
     152        m.put("draw.rawgps.colors.dynamic", "draw.rawgps.colormode.dynamic-range");
     153        m.put("draw.rawgps.heatmap.line-extra", "draw.rawgps.colormode.heatmap.line-extra");
     154        m.put("draw.rawgps.heatmap.colormap", "draw.rawgps.colormode.heatmap.colormap");
     155        m.put("draw.rawgps.heatmap.use-points", "draw.rawgps.colormode.heatmap.use-points");
     156        m.put("draw.rawgps.heatmap.gain", "draw.rawgps.colormode.heatmap.gain");
     157        m.put("draw.rawgps.heatmap.lower-limit", "draw.rawgps.colormode.heatmap.lower-limit");
     158        m.put("draw.rawgps.date-coloring-min-dt", "draw.rawgps.colormode.time.min-distance");
     159        return Collections.unmodifiableMap(m);
     160    }
     161
    89162    private static final long MAX_AGE_DEFAULT_PREFERENCES = TimeUnit.DAYS.toSeconds(50);
    90163
    91164    private final IBaseDirectories dirs;
     165    boolean modifiedDefault;
    92166
    93167    /**
     
    416490        settingsMap.clear();
    417491        settingsMap.putAll(reader.getSettings());
    418         removeObsolete(reader.getVersion());
     492        removeAndUpdateObsolete(reader.getVersion());
    419493    }
    420494
     
    521595            return;
    522596        }
     597        File def = getDefaultsCacheFile();
     598        if (def.exists()) {
     599            try {
     600                loadDefaults();
     601            } catch (IOException | XMLStreamException | SAXException e) {
     602                Logging.error(e);
     603                Logging.warn(tr("Failed to load defaults cache file: {0}", def));
     604                defaultsMap.clear();
     605                if (!def.delete()) {
     606                    Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
     607                }
     608            }
     609        }
    523610        try {
    524611            load();
     
    544631                Logging.error(e1);
    545632                Logging.warn(tr("Failed to initialize preferences. Failed to reset preference file to default: {0}", getPreferenceFile()));
    546             }
    547         }
    548         File def = getDefaultsCacheFile();
    549         if (def.exists()) {
    550             try {
    551                 loadDefaults();
    552             } catch (IOException | XMLStreamException | SAXException e) {
    553                 Logging.error(e);
    554                 Logging.warn(tr("Failed to load defaults cache file: {0}", def));
    555                 defaultsMap.clear();
    556                 if (!def.delete()) {
    557                     Logging.warn(tr("Failed to delete faulty defaults cache file: {0}", def));
    558                 }
    559633            }
    560634        }
     
    756830
    757831    /**
    758      * Removes obsolete preference settings. If you throw out a once-used preference
     832     * Removes and updates obsolete preference settings. If you throw out a once-used preference
    759833     * setting, add it to the list here with an expiry date (written as comment). If you
    760834     * see something with an expiry date in the past, remove it from the list.
    761835     * @param loadedVersion JOSM version when the preferences file was written
    762836     */
    763     private void removeObsolete(int loadedVersion) {
     837    private void removeAndUpdateObsolete(int loadedVersion) {
     838        Logging.trace("Update obsolete preference keys for version {0}", Integer.toString(loadedVersion));
     839        for (Entry<String, String> e : UPDATE_PREF_KEYS.entrySet()) {
     840            String oldkey = e.getKey();
     841            String newkey = e.getValue();
     842            if (settingsMap.containsKey(oldkey)) {
     843                Setting<?> value = settingsMap.remove(oldkey);
     844                settingsMap.putIfAbsent(newkey, value);
     845                Logging.info(tr("Updated preference setting {0} to {1}", oldkey, newkey));
     846            }
     847        }
     848
    764849        Logging.trace("Remove obsolete preferences for version {0}", Integer.toString(loadedVersion));
    765850        for (String key : OBSOLETE_PREF_KEYS) {
    766851            if (settingsMap.containsKey(key)) {
    767852                settingsMap.remove(key);
    768                 Logging.info(tr("Preference setting {0} has been removed since it is no longer used.", key));
    769             }
     853                Logging.info(tr("Removed preference setting {0} since it is no longer used", key));
     854            }
     855            if (defaultsMap.containsKey(key)) {
     856                defaultsMap.remove(key);
     857                Logging.info(tr("Removed preference default {0} since it is no longer used", key));
     858                modifiedDefault = true;
     859            }
     860        }
     861        for (String key : OBSOLETE_PREF_KEYS_START) {
     862            settingsMap.entrySet().stream()
     863            .filter(e -> e.getKey().startsWith(key))
     864            .collect(Collectors.toSet())
     865            .forEach(e -> {
     866                String k = e.getKey();
     867                if (!KEEP_PREF_KEYS.contains(k)) {
     868                    settingsMap.remove(k);
     869                    Logging.info(tr("Removed preference setting {0} since it is no longer used", k));
     870                }
     871            });
     872            defaultsMap.entrySet().stream()
     873            .filter(e -> e.getKey().startsWith(key))
     874            .collect(Collectors.toSet())
     875            .forEach(e -> {
     876                String k = e.getKey();
     877                if (!KEEP_PREF_KEYS.contains(k)) {
     878                    defaultsMap.remove(k);
     879                    Logging.info(tr("Removed preference default {0} since it is no longer used", k));
     880                    modifiedDefault = true;
     881                }
     882            });
     883        }
     884        if (modifiedDefault) {
     885            try {
     886                saveDefaults();
     887                Logging.info(tr("Saved updated default preferences."));
     888            } catch (IOException ex) {
     889                Logging.log(Logging.LEVEL_WARN, tr("Failed to save default preferences."), ex);
     890            }
     891            modifiedDefault = false;
    770892        }
    771893    }
  • trunk/src/org/openstreetmap/josm/data/gpx/GpxConstants.java

    r15419 r15496  
    22package org.openstreetmap.josm.data.gpx;
    33
     4import java.awt.Color;
    45import java.util.Arrays;
    56import java.util.Collection;
    67import java.util.Collections;
    78import java.util.List;
     9import java.util.Map;
     10import java.util.TreeMap;
    811
    912import org.openstreetmap.josm.data.Bounds;
     
    9699     */
    97100    String META_BOUNDS = META_PREFIX + "bounds";
    98     /**
    99      * A constant for the metadata hash map: the extension data. This is a {@link Extensions} object
    100      * @see GpxData#addExtension(String, String)
    101      * @see GpxData#get(String)
    102      */
    103     String META_EXTENSIONS = META_PREFIX + "extensions";
    104 
    105     /**
    106      * A namespace for josm GPX extensions
    107      */
    108     String JOSM_EXTENSIONS_NAMESPACE_URI = Config.getUrls().getXMLBase() + "/gpx-extensions-1.0";
     101
     102    /**
     103     * Namespace for the XSD
     104     */
     105    String XML_URI_XSD = "http://www.w3.org/2001/XMLSchema-instance";
     106
     107    /**
     108     * Namespace for JOSM GPX extensions
     109     */
     110    String XML_URI_EXTENSIONS_JOSM = Config.getUrls().getXMLBase() + "/gpx-extensions-1.1";
     111    /**
     112     * Location of the XSD schema for JOSM GPX extensions
     113     */
     114    String XML_XSD_EXTENSIONS_JOSM = Config.getUrls().getXMLBase() + "/gpx-extensions-1.1.xsd";
     115
     116    /**
     117     * Namespace for GPX drawing extensions
     118     */
     119    String XML_URI_EXTENSIONS_DRAWING = Config.getUrls().getXMLBase() + "/gpx-drawing-extensions-1.0";
     120    /**
     121     * Location of the XSD schema for GPX drawing extensions
     122     */
     123    String XML_XSD_EXTENSIONS_DRAWING = Config.getUrls().getXMLBase() + "/gpx-drawing-extensions-1.0.xsd";
     124
     125    /**
     126     * Namespace for Garmin GPX extensions
     127     */
     128    String XML_URI_EXTENSIONS_GARMIN = "http://www.garmin.com/xmlschemas/GpxExtensions/v3";
     129    /**
     130     * Location of the XSD schema for GPX drawing extensions
     131     */
     132    String XML_XSD_EXTENSIONS_GARMIN = "http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd";
    109133
    110134    /** Elevation (in meters) of the point. */
     
    155179    List<String> WPT_KEYS = Collections.unmodifiableList(Arrays.asList(PT_ELE, PT_TIME, PT_MAGVAR, PT_GEOIDHEIGHT,
    156180            GPX_NAME, GPX_CMT, GPX_DESC, GPX_SRC, META_LINKS, PT_SYM, PT_TYPE,
    157             PT_FIX, PT_SAT, PT_HDOP, PT_VDOP, PT_PDOP, PT_AGEOFDGPSDATA, PT_DGPSID, META_EXTENSIONS));
     181            PT_FIX, PT_SAT, PT_HDOP, PT_VDOP, PT_PDOP, PT_AGEOFDGPSDATA, PT_DGPSID));
    158182
    159183    /**
     
    161185     */
    162186    List<String> RTE_TRK_KEYS = Collections.unmodifiableList(Arrays.asList(
    163             GPX_NAME, GPX_CMT, GPX_DESC, GPX_SRC, META_LINKS, "number", PT_TYPE, META_EXTENSIONS));
     187            GPX_NAME, GPX_CMT, GPX_DESC, GPX_SRC, META_LINKS, "number", PT_TYPE));
     188
     189    /**
     190     * Map with all supported Garmin colors
     191     */
     192    Map<String, Color> GARMIN_COLORS = getGarminColors();
     193
     194    /**
     195     * Helper method for {@link #GARMIN_COLORS}
     196     * @return Map with all supported Garmin colors
     197     */
     198    static Map<String, Color> getGarminColors() {
     199        TreeMap<String, Color> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
     200        m.put("Black", Color.BLACK);
     201        m.put("DarkRed", new Color(139, 0, 0));
     202        m.put("DarkGreen", new Color(0, 100, 0));
     203        m.put("DarkYellow", new Color(255, 170, 0));
     204        m.put("DarkBlue", new Color(0, 0, 139));
     205        m.put("DarkMagenta", new Color(139, 0, 139));
     206        m.put("DarkCyan", new Color(0, 139, 139));
     207        m.put("LightGray", Color.LIGHT_GRAY);
     208        m.put("DarkGray", Color.DARK_GRAY);
     209        m.put("Red", Color.RED);
     210        m.put("Green", Color.GREEN);
     211        m.put("Yellow", Color.YELLOW);
     212        m.put("Blue", Color.BLUE);
     213        m.put("Magenta", Color.MAGENTA);
     214        m.put("Cyan", Color.CYAN);
     215        m.put("White", Color.WHITE);
     216        m.put("Transparent", new Color(0, 0, 0, 255));
     217        return Collections.unmodifiableMap(m);
     218    }
     219
     220    /**
     221     * Enum with color formats that can be written by JOSM
     222     */
     223    enum ColorFormat {
     224        /** Drawing extension format */
     225        GPXD,
     226        /** Garmin track extension format */
     227        GPXX
     228    }
     229
     230    /**
     231     * Map with all supported extension abbreviations for easier readability in OSM layers
     232     */
     233    Map<String, String> EXTENSION_ABBREVIATIONS = getExtensionAbbreviations();
     234
     235    /**
     236     * Helper method for {@link #EXTENSION_ABBREVIATIONS}
     237     * @return Map with all supported extension abbreviations
     238     */
     239    static Map<String, String> getExtensionAbbreviations() {
     240        TreeMap<String, String> m = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
     241        m.put("gpx:extension:gpxx:TrackExtensions:DisplayColor", "gpxx:DisplayColor");
     242        m.put("gpx:extension:gpxd:color", "gpxd:color");
     243        return m;
     244    }
    164245
    165246    /**
  • trunk/src/org/openstreetmap/josm/data/gpx/GpxData.java

    r15427 r15496  
    1616import java.util.Map;
    1717import java.util.NoSuchElementException;
     18import java.util.Objects;
     19import java.util.Optional;
    1820import java.util.Set;
    1921import java.util.stream.Collectors;
     
    2426import org.openstreetmap.josm.data.DataSource;
    2527import org.openstreetmap.josm.data.coor.EastNorth;
    26 import org.openstreetmap.josm.data.gpx.GpxTrack.GpxTrackChangeListener;
     28import org.openstreetmap.josm.data.gpx.IGpxTrack.GpxTrackChangeListener;
    2729import org.openstreetmap.josm.data.projection.ProjectionRegistry;
    2830import org.openstreetmap.josm.gui.MainApplication;
     
    4143
    4244    /**
     45     * Constructs a new GpxData.
     46     */
     47    public GpxData() {}
     48
     49    /**
     50     * Constructs a new GpxData that is currently being initialized, so no listeners will be fired until {@link #endUpdate()} is called.
     51     * @param initializing true
     52     * @since 15496
     53     */
     54    public GpxData(boolean initializing) {
     55        this.initializing = initializing;
     56    }
     57
     58    /**
    4359     * The disk file this layer is stored in, if it is a local layer. May be <code>null</code>.
    4460     */
     
    6682     */
    6783    private final ArrayList<WayPoint> privateWaypoints = new ArrayList<>();
    68     private final GpxTrackChangeListener proxy = e -> fireInvalidate();
     84    /**
     85     * All namespaces read from the original file
     86     */
     87    private final List<XMLNamespace> namespaces = new ArrayList<>();
     88    /**
     89     * The layer specific prefs formerly saved in the preferences, e.g. drawing options.
     90     * NOT the track specific settings (e.g. color, width)
     91     */
     92    private final Map<String, String> layerPrefs = new HashMap<>();
     93
     94    private final GpxTrackChangeListener proxy = e -> invalidate();
     95    private boolean modified, updating, initializing;
     96    private boolean suppressedInvalidate;
    6997
    7098    /**
     
    72100     * @see #getTracks()
    73101     */
    74     public final Collection<GpxTrack> tracks = new ListeningCollection<GpxTrack>(privateTracks, this::fireInvalidate) {
     102    public final Collection<GpxTrack> tracks = new ListeningCollection<GpxTrack>(privateTracks, this::invalidate) {
    75103
    76104        @Override
     
    91119     * @see #getRoutes()
    92120     */
    93     public final Collection<GpxRoute> routes = new ListeningCollection<>(privateRoutes, this::fireInvalidate);
     121    public final Collection<GpxRoute> routes = new ListeningCollection<>(privateRoutes, this::invalidate);
    94122
    95123    /**
     
    97125     * @see #getWaypoints()
    98126     */
    99     public final Collection<WayPoint> waypoints = new ListeningCollection<>(privateWaypoints, this::fireInvalidate);
     127    public final Collection<WayPoint> waypoints = new ListeningCollection<>(privateWaypoints, this::invalidate);
    100128
    101129    /**
     
    108136
    109137    private final ListenerList<GpxDataChangeListener> listeners = ListenerList.create();
    110 
    111     static class TimestampConfictException extends Exception {}
    112138
    113139    private List<GpxTrackSegmentSpan> segSpans;
     
    157183        other.privateWaypoints.forEach(this::addWaypoint);
    158184        dataSources.addAll(other.dataSources);
    159         fireInvalidate();
     185        invalidate();
    160186    }
    161187
    162188    private void cutOverlapping(GpxTrack trk, boolean connect) {
    163         List<GpxTrackSegment> segsOld = new ArrayList<>(trk.getSegments());
    164         List<GpxTrackSegment> segsNew = new ArrayList<>();
    165         for (GpxTrackSegment seg : segsOld) {
     189        List<IGpxTrackSegment> segsOld = new ArrayList<>(trk.getSegments());
     190        List<IGpxTrackSegment> segsNew = new ArrayList<>();
     191        for (IGpxTrackSegment seg : segsOld) {
    166192            GpxTrackSegmentSpan s = GpxTrackSegmentSpan.tryGetFromSegment(seg);
    167193            if (s != null && anySegmentOverlapsWith(s)) {
     
    204230                                    // because other high priority tracks between the same waypoints could follow
    205231                                    if (!wpsNew.isEmpty()) {
    206                                         segsNew.add(new ImmutableGpxTrackSegment(wpsNew));
     232                                        segsNew.add(new GpxTrackSegment(wpsNew));
    207233                                    }
    208234                                    if (!segsNew.isEmpty()) {
    209                                         privateTracks.add(new ImmutableGpxTrack(segsNew, trk.getAttributes()));
     235                                        privateTracks.add(new GpxTrack(segsNew, trk.getAttributes()));
    210236                                    }
    211237                                    segsNew = new ArrayList<>();
     
    222248                            //track has to be split, because we have an overlapping short track in the middle
    223249                            if (!wpsNew.isEmpty()) {
    224                                 segsNew.add(new ImmutableGpxTrackSegment(wpsNew));
     250                                segsNew.add(new GpxTrackSegment(wpsNew));
    225251                            }
    226252                            if (!segsNew.isEmpty()) {
    227                                 privateTracks.add(new ImmutableGpxTrack(segsNew, trk.getAttributes()));
     253                                privateTracks.add(new GpxTrack(segsNew, trk.getAttributes()));
    228254                            }
    229255                            segsNew = new ArrayList<>();
     
    239265                }
    240266                if (!wpsNew.isEmpty()) {
    241                     segsNew.add(new ImmutableGpxTrackSegment(wpsNew));
     267                    segsNew.add(new GpxTrackSegment(wpsNew));
    242268                }
    243269            } else {
     
    248274            privateTracks.add(trk);
    249275        } else if (!segsNew.isEmpty()) {
    250             privateTracks.add(new ImmutableGpxTrack(segsNew, trk.getAttributes()));
     276            privateTracks.add(new GpxTrack(segsNew, trk.getAttributes()));
    251277        }
    252278    }
     
    254280    private void connectTracks(WayPoint prevWp, GpxTrackSegmentSpan span, Map<String, Object> attr) {
    255281        if (prevWp != null && !span.lastEquals(prevWp)) {
    256             privateTracks.add(new ImmutableGpxTrack(Arrays.asList(Arrays.asList(new WayPoint(prevWp), span.getFirstWp())), attr));
     282            privateTracks.add(new GpxTrack(Arrays.asList(Arrays.asList(new WayPoint(prevWp), span.getFirstWp())), attr));
    257283        }
    258284    }
     
    310336        }
    311337
    312         static GpxTrackSegmentSpan tryGetFromSegment(GpxTrackSegment seg) {
     338        static GpxTrackSegmentSpan tryGetFromSegment(IGpxTrackSegment seg) {
    313339            WayPoint b = getNextWpWithTime(seg, true);
    314340            if (b != null) {
     
    321347        }
    322348
    323         private static WayPoint getNextWpWithTime(GpxTrackSegment seg, boolean forward) {
     349        private static WayPoint getNextWpWithTime(IGpxTrackSegment seg, boolean forward) {
    324350            List<WayPoint> wps = new ArrayList<>(seg.getWayPoints());
    325351            for (int i = forward ? 0 : wps.size() - 1; i >= 0 && i < wps.size(); i += forward ? 1 : -1) {
     
    341367            segSpans = new ArrayList<>();
    342368            for (GpxTrack trk : privateTracks) {
    343                 for (GpxTrackSegment seg : trk.getSegments()) {
     369                for (IGpxTrackSegment seg : trk.getSegments()) {
    344370                    GpxTrackSegmentSpan s = GpxTrackSegmentSpan.tryGetFromSegment(seg);
    345371                    if (s != null) {
     
    374400     * @return {@code Stream<GPXTrack>}
    375401     */
    376     private synchronized Stream<GpxTrackSegment> getTrackSegmentsStream() {
     402    public synchronized Stream<IGpxTrackSegment> getTrackSegmentsStream() {
    377403        return getTracks().stream().flatMap(trk -> trk.getSegments().stream());
    378404    }
     
    398424        privateTracks.add(track);
    399425        track.addListener(proxy);
    400         fireInvalidate();
     426        invalidate();
    401427    }
    402428
     
    411437        }
    412438        track.removeListener(proxy);
    413         fireInvalidate();
     439        invalidate();
    414440    }
    415441
     
    421447     */
    422448    public synchronized void combineTracksToSegmentedTrack() {
    423         List<GpxTrackSegment> segs = getTrackSegmentsStream()
    424                 .collect(Collectors.toCollection(ArrayList<GpxTrackSegment>::new));
     449        List<IGpxTrackSegment> segs = getTrackSegmentsStream()
     450                .collect(Collectors.toCollection(ArrayList<IGpxTrackSegment>::new));
    425451        Map<String, Object> attrs = new HashMap<>(privateTracks.get(0).getAttributes());
    426452
     
    432458
    433459        clearTracks();
    434         addTrack(new ImmutableGpxTrack(segs, attrs));
     460        addTrack(new GpxTrack(segs, attrs));
    435461    }
    436462
     
    468494                    HashMap<String, Object> attrs = new HashMap<>(trk.getAttributes());
    469495                    ensureUniqueName(attrs, counts, srcLayerName);
    470                     return new ImmutableGpxTrack(Arrays.asList(seg), attrs);
     496                    return new GpxTrack(Arrays.asList(seg), attrs);
    471497                }))
    472498            .collect(Collectors.toCollection(ArrayList<GpxTrack>::new));
     
    539565        }
    540566        privateRoutes.add(route);
    541         fireInvalidate();
     567        invalidate();
    542568    }
    543569
     
    551577            throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", route));
    552578        }
    553         fireInvalidate();
     579        invalidate();
    554580    }
    555581
     
    573599        }
    574600        privateWaypoints.add(waypoint);
    575         fireInvalidate();
     601        invalidate();
    576602    }
    577603
     
    585611            throw new IllegalArgumentException(MessageFormat.format("The route was not in this data: {0}", waypoint));
    586612        }
    587         fireInvalidate();
     613        invalidate();
    588614    }
    589615
     
    601627     * @see #getTracks()
    602628     * @see GpxTrack#getSegments()
    603      * @see GpxTrackSegment#getWayPoints()
     629     * @see IGpxTrackSegment#getWayPoints()
    604630     * @since 12156
    605631     */
     
    768794        double rx = 0.0, ry = 0.0, sx, sy, x, y;
    769795        for (GpxTrack track : privateTracks) {
    770             for (GpxTrackSegment seg : track.getSegments()) {
     796            for (IGpxTrackSegment seg : track.getSegments()) {
    771797                WayPoint r = null;
    772798                for (WayPoint wpSeg : seg.getWayPoints()) {
     
    883909        private Iterator<GpxTrack> itTracks;
    884910        private int idxTracks;
    885         private Iterator<GpxTrackSegment> itTrackSegments;
     911        private Iterator<IGpxTrackSegment> itTrackSegments;
    886912        private final Iterator<GpxRoute> itRoutes;
    887913
     
    889915        private final boolean[] trackVisibility;
    890916        private Map<String, Object> trackAttributes;
     917        private GpxTrack curTrack;
    891918
    892919        /**
     
    922949            if (itTracks != null) {
    923950                if (itTrackSegments != null && itTrackSegments.hasNext()) {
    924                     return new Line(itTrackSegments.next(), trackAttributes);
     951                    return new Line(itTrackSegments.next(), trackAttributes, curTrack.getColor());
    925952                } else {
    926953                    while (itTracks.hasNext()) {
    927                         GpxTrack nxtTrack = itTracks.next();
    928                         trackAttributes = nxtTrack.getAttributes();
     954                        curTrack = itTracks.next();
     955                        trackAttributes = curTrack.getAttributes();
    929956                        idxTracks++;
    930957                        if (trackVisibility != null && !trackVisibility[idxTracks])
    931958                            continue;
    932                         itTrackSegments = nxtTrack.getSegments().iterator();
     959                        itTrackSegments = curTrack.getSegments().iterator();
    933960                        if (itTrackSegments.hasNext()) {
    934                             return new Line(itTrackSegments.next(), trackAttributes);
     961                            return new Line(itTrackSegments.next(), trackAttributes, curTrack.getColor());
    935962                        }
    936963                    }
     
    957984    }
    958985
     986    /**
     987     * The layer specific prefs formerly saved in the preferences, e.g. drawing options.
     988     * NOT the track specific settings (e.g. color, width)
     989     * @return Modifiable map
     990     * @since 15496
     991     */
     992    public Map<String, String> getLayerPrefs() {
     993        return layerPrefs;
     994    }
     995
     996    /**
     997     * All XML namespaces read from the original file
     998     * @return Modifiable list
     999     * @since 15496
     1000     */
     1001    public List<XMLNamespace> getNamespaces() {
     1002        return namespaces;
     1003    }
     1004
    9591005    @Override
    9601006    public synchronized int hashCode() {
    9611007        final int prime = 31;
    962         int result = 1;
     1008        int result = prime + super.hashCode();
     1009        result = prime * result + ((namespaces == null) ? 0 : namespaces.hashCode());
     1010        result = prime * result + ((layerPrefs == null) ? 0 : layerPrefs.hashCode());
    9631011        result = prime * result + ((dataSources == null) ? 0 : dataSources.hashCode());
    9641012        result = prime * result + ((privateRoutes == null) ? 0 : privateRoutes.hashCode());
     
    9731021            return true;
    9741022        if (obj == null)
     1023            return false;
     1024        if (!super.equals(obj))
    9751025            return false;
    9761026        if (getClass() != obj.getClass())
     
    9811031                return false;
    9821032        } else if (!dataSources.equals(other.dataSources))
     1033            return false;
     1034        if (layerPrefs == null) {
     1035            if (other.layerPrefs != null)
     1036                return false;
     1037        } else if (!layerPrefs.equals(other.layerPrefs))
    9831038            return false;
    9841039        if (privateRoutes == null) {
     
    9971052        } else if (!privateWaypoints.equals(other.privateWaypoints))
    9981053            return false;
     1054        if (namespaces == null) {
     1055            if (other.namespaces != null)
     1056                return false;
     1057        } else if (!namespaces.equals(other.namespaces))
     1058            return false;
    9991059        return true;
     1060    }
     1061
     1062    @Override
     1063    public void put(String key, Object value) {
     1064        super.put(key, value);
     1065        invalidate();
    10001066    }
    10011067
     
    10261092    }
    10271093
    1028     private void fireInvalidate() {
    1029         if (listeners.hasListeners()) {
    1030             GpxDataChangeEvent e = new GpxDataChangeEvent(this);
    1031             listeners.fireEvent(l -> l.gpxDataChanged(e));
     1094    /**
     1095     * Fires event listeners and sets the modified flag to true.
     1096     */
     1097    public void invalidate() {
     1098        fireInvalidate(true);
     1099    }
     1100
     1101    private void fireInvalidate(boolean setModified) {
     1102        if (updating || initializing) {
     1103            suppressedInvalidate = true;
     1104        } else {
     1105            if (setModified) {
     1106                modified = true;
     1107            }
     1108            if (listeners.hasListeners()) {
     1109                GpxDataChangeEvent e = new GpxDataChangeEvent(this);
     1110                listeners.fireEvent(l -> l.gpxDataChanged(e));
     1111            }
     1112        }
     1113    }
     1114
     1115    /**
     1116     * Begins updating this GpxData and prevents listeners from being fired.
     1117     * @since 15496
     1118     */
     1119    public void beginUpdate() {
     1120        updating = true;
     1121    }
     1122
     1123    /**
     1124     * Finishes updating this GpxData and fires listeners if required.
     1125     * @since 15496
     1126     */
     1127    public void endUpdate() {
     1128        boolean setModified = updating;
     1129        updating = initializing = false;
     1130        if (suppressedInvalidate) {
     1131            fireInvalidate(setModified);
     1132            suppressedInvalidate = false;
    10321133        }
    10331134    }
     
    10681169        }
    10691170    }
     1171
     1172    /**
     1173     * @return whether anything has been modified (e.g. colors)
     1174     * @since 15496
     1175     */
     1176    public boolean isModified() {
     1177        return modified;
     1178    }
     1179
     1180    /**
     1181     * Sets the modified flag to the value.
     1182     * @param value modified flag
     1183     * @since 15496
     1184     */
     1185    public void setModified(boolean value) {
     1186        modified = value;
     1187    }
     1188
     1189    /**
     1190     * A class containing prefix, URI and location of a namespace
     1191     * @since 15496
     1192     */
     1193    public static class XMLNamespace {
     1194        private final String uri, prefix;
     1195        private String location;
     1196
     1197        /**
     1198         * Creates a schema with prefix and URI, tries to determine prefix from URI
     1199         * @param fallbackPrefix the namespace prefix, if not determined from URI
     1200         * @param uri the namespace URI
     1201         */
     1202        public XMLNamespace(String fallbackPrefix, String uri) {
     1203            this.prefix = Optional.ofNullable(GpxExtension.findPrefix(uri)).orElse(fallbackPrefix);
     1204            this.uri = uri;
     1205        }
     1206
     1207        /**
     1208         * Creates a schema with prefix, URI and location.
     1209         * Does NOT try to determine prefix from URI!
     1210         * @param prefix
     1211         * @param uri
     1212         * @param location
     1213         */
     1214        public XMLNamespace(String prefix, String uri, String location) {
     1215            this.prefix = prefix;
     1216            this.uri = uri;
     1217            this.location = location;
     1218        }
     1219
     1220        /**
     1221         * @return the URI of the namesapce
     1222         */
     1223        public String getURI() {
     1224            return uri;
     1225        }
     1226
     1227        /**
     1228         * @return the prefix of the namespace, determined from URI if possible
     1229         */
     1230        public String getPrefix() {
     1231            return prefix;
     1232        }
     1233
     1234        /**
     1235         * @return the location of the schema
     1236         */
     1237        public String getLocation() {
     1238            return location;
     1239        }
     1240
     1241        /**
     1242         * Sets the location of the schema
     1243         * @param location the location of the schema
     1244         */
     1245        public void setLocation(String location) {
     1246            this.location = location;
     1247        }
     1248
     1249        @Override
     1250        public int hashCode() {
     1251            return Objects.hash(prefix, uri, location);
     1252        }
     1253
     1254        @Override
     1255        public boolean equals(Object obj) {
     1256            if (this == obj)
     1257                return true;
     1258            if (obj == null)
     1259                return false;
     1260            if (getClass() != obj.getClass())
     1261                return false;
     1262            XMLNamespace other = (XMLNamespace) obj;
     1263            if (prefix == null) {
     1264                if (other.prefix != null)
     1265                    return false;
     1266            } else if (!prefix.equals(other.prefix))
     1267                return false;
     1268            if (uri == null) {
     1269                if (other.uri != null)
     1270                    return false;
     1271            } else if (!uri.equals(other.uri))
     1272                return false;
     1273            if (location == null) {
     1274                if (other.location != null)
     1275                    return false;
     1276            } else if (!location.equals(other.location))
     1277                return false;
     1278            return true;
     1279        }
     1280    }
    10701281}
  • trunk/src/org/openstreetmap/josm/data/gpx/GpxImageCorrelation.java

    r14797 r15496  
    4040        for (GpxTrack trk : selectedGpx.tracks) {
    4141            List<List<WayPoint>> segs = new ArrayList<>();
    42             for (GpxTrackSegment seg : trk.getSegments()) {
     42            for (IGpxTrackSegment seg : trk.getSegments()) {
    4343                List<WayPoint> wps = new ArrayList<>(seg.getWayPoints());
    4444                if (!wps.isEmpty()) {
  • trunk/src/org/openstreetmap/josm/data/gpx/GpxTrack.java

    r12289 r15496  
    22package org.openstreetmap.josm.data.gpx;
    33
     4import java.awt.Color;
     5import java.util.ArrayList;
    46import java.util.Collection;
     7import java.util.Collections;
     8import java.util.HashMap;
     9import java.util.List;
    510import java.util.Map;
     11import java.util.Map.Entry;
     12import java.util.Optional;
    613
    714import org.openstreetmap.josm.data.Bounds;
     15import org.openstreetmap.josm.tools.ListenerList;
     16import org.openstreetmap.josm.tools.Logging;
    817
    918/**
    10  * Read-only gpx track. Implementations doesn't have to be immutable, but should always be thread safe.
    11  * @since 444
     19 * GPX track.
     20 * Note that the color attributes are not immutable and may be modified by the user.
     21 * @since 15496
    1222 */
    13 public interface GpxTrack extends IWithAttributes {
    14 
    15     /**
    16      * Returns the track segments.
    17      * @return the track segments
    18      */
    19     Collection<GpxTrackSegment> getSegments();
    20 
    21     /**
    22      * Returns the track attributes.
    23      * @return the track attributes
    24      */
    25     Map<String, Object> getAttributes();
    26 
    27     /**
    28      * Returns the track bounds.
    29      * @return the track bounds
    30      */
    31     Bounds getBounds();
    32 
    33     /**
    34      * Returns the track length.
    35      * @return the track length
    36      */
    37     double length();
    38 
    39     /**
    40      * Add a listener that listens to changes in the GPX track.
    41      * @param l The listener
    42      */
    43     default void addListener(GpxTrackChangeListener l) {
    44         // nop
    45     }
    46 
    47     /**
    48      * Remove a listener that listens to changes in the GPX track.
    49      * @param l The listener
    50      */
    51     default void removeListener(GpxTrackChangeListener l) {
    52         // nop
     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", "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;
    53251    }
    54252
    55253    /**
    56254     * A listener that listens to GPX track changes.
    57      * @author Michael Zangl
    58      * @since 12156
    59      */
     255     * @deprecated use {@link IGpxTrack.GpxTrackChangeListener} instead
     256     */
     257    @Deprecated
    60258    @FunctionalInterface
    61259    interface GpxTrackChangeListener {
    62         /**
    63          * Called when the gpx data changed.
    64          * @param e The event
    65          */
    66260        void gpxDataChanged(GpxTrackChangeEvent e);
    67261    }
     
    69263    /**
    70264     * A track change event for the current track.
    71      * @author Michael Zangl
    72      * @since 12156
    73      */
    74     class GpxTrackChangeEvent {
    75         private final GpxTrack source;
    76 
    77         GpxTrackChangeEvent(GpxTrack source) {
    78             super();
    79             this.source = source;
    80         }
    81 
    82         /**
    83          * Get the track that was changed.
    84          * @return The track.
    85          */
    86         public GpxTrack getSource() {
    87             return source;
    88         }
    89     }
     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
    90274}
  • trunk/src/org/openstreetmap/josm/data/gpx/GpxTrackSegment.java

    r9949 r15496  
    22package org.openstreetmap.josm.data.gpx;
    33
     4import java.util.ArrayList;
    45import java.util.Collection;
     6import java.util.Collections;
     7import java.util.List;
    58
    69import org.openstreetmap.josm.data.Bounds;
    710
    811/**
    9  * Read-only gpx track segments. Implementations doesn't have to be immutable, but should always be thread safe.
    10  * @since 2907
     12 * A gpx track segment consisting of multiple waypoints.
     13 * @since 15496
    1114 */
    12 public interface GpxTrackSegment {
     15public class GpxTrackSegment extends WithAttributes implements IGpxTrackSegment {
     16
     17    private final List<WayPoint> wayPoints;
     18    private final Bounds bounds;
     19    private final double length;
    1320
    1421    /**
    15      * Returns the segment bounds.
    16      * @return the segment bounds
     22     * Constructs a new {@code GpxTrackSegment}.
     23     * @param wayPoints list of waypoints
    1724     */
    18     Bounds getBounds();
     25    public GpxTrackSegment(Collection<WayPoint> wayPoints) {
     26        this.wayPoints = Collections.unmodifiableList(new ArrayList<>(wayPoints));
     27        this.bounds = calculateBounds();
     28        this.length = calculateLength();
     29    }
    1930
    20     /**
    21      * Returns the segment waypoints.
    22      * @return the segment waypoints
    23      */
    24     Collection<WayPoint> getWayPoints();
     31    private Bounds calculateBounds() {
     32        Bounds result = null;
     33        for (WayPoint wpt: wayPoints) {
     34            if (result == null) {
     35                result = new Bounds(wpt.getCoor());
     36            } else {
     37                result.extend(wpt.getCoor());
     38            }
     39        }
     40        return result;
     41    }
    2542
    26     /**
    27      * Returns the segment length.
    28      * @return the segment length
    29      */
    30     double length();
     43    private double calculateLength() {
     44        double result = 0.0; // in meters
     45        WayPoint last = null;
     46        for (WayPoint tpt : wayPoints) {
     47            if (last != null) {
     48                Double d = last.getCoor().greatCircleDistance(tpt.getCoor());
     49                if (!d.isNaN() && !d.isInfinite()) {
     50                    result += d;
     51                }
     52            }
     53            last = tpt;
     54        }
     55        return result;
     56    }
    3157
    32     /**
    33      * Returns the number of times this track has been changed
    34      * @return Number of times this track has been changed. Always 0 for read-only segments
    35      */
    36     int getUpdateCount();
     58    @Override
     59    public Bounds getBounds() {
     60        return bounds == null ? null : new Bounds(bounds);
     61    }
     62
     63    @Override
     64    public Collection<WayPoint> getWayPoints() {
     65        return Collections.unmodifiableList(wayPoints);
     66    }
     67
     68    @Override
     69    public double length() {
     70        return length;
     71    }
     72
     73    @Override
     74    public int getUpdateCount() {
     75        return 0;
     76    }
     77
     78    @Override
     79    public int hashCode() {
     80        final int prime = 31;
     81        int result = prime + super.hashCode();
     82        result = prime * result + ((wayPoints == null) ? 0 : wayPoints.hashCode());
     83        return result;
     84    }
     85
     86    @Override
     87    public boolean equals(Object obj) {
     88        if (this == obj)
     89            return true;
     90        if (obj == null)
     91            return false;
     92        if (!super.equals(obj))
     93            return false;
     94        if (getClass() != obj.getClass())
     95            return false;
     96        GpxTrackSegment other = (GpxTrackSegment) obj;
     97        if (wayPoints == null) {
     98            if (other.wayPoints != null)
     99                return false;
     100        } else if (!wayPoints.equals(other.wayPoints))
     101            return false;
     102        return true;
     103    }
    37104}
  • trunk/src/org/openstreetmap/josm/data/gpx/IWithAttributes.java

    r9246 r15496  
    33
    44import java.util.Collection;
     5import java.util.Map;
    56
    67/**
     
    5152
    5253    /**
    53      * Add a key / value pair that is not part of the GPX schema as an extension.
    54      *
    55      * @param key the key
    56      * @param value the value
     54     * Returns the attributes
     55     * @return the attributes
    5756     */
    58     void addExtension(String key, String value);
     57    Map<String, Object> getAttributes();
     58
     59    /**
     60     * Returns the extensions
     61     * @return the extensions
     62     */
     63    GpxExtensionCollection getExtensions();
    5964
    6065}
  • trunk/src/org/openstreetmap/josm/data/gpx/ImmutableGpxTrack.java

    r13210 r15496  
    22package org.openstreetmap.josm.data.gpx;
    33
    4 import java.util.ArrayList;
    54import java.util.Collection;
    6 import java.util.Collections;
    7 import java.util.HashMap;
    85import java.util.List;
    96import java.util.Map;
    107
    11 import org.openstreetmap.josm.data.Bounds;
    12 
    138/**
    14  * Immutable GPX track.
     9 * GPX track, NOT immutable
    1510 * @since 2907
     11 * @deprecated Use {@link GpxTrack} instead!
    1612 */
    17 public class ImmutableGpxTrack extends WithAttributes implements GpxTrack {
    18 
    19     private final List<GpxTrackSegment> segments;
    20     private final double length;
    21     private final Bounds bounds;
     13@Deprecated
     14public class ImmutableGpxTrack extends GpxTrack {
    2215
    2316    /**
     
    2720     */
    2821    public ImmutableGpxTrack(Collection<Collection<WayPoint>> trackSegs, Map<String, Object> attributes) {
    29         List<GpxTrackSegment> newSegments = new ArrayList<>();
    30         for (Collection<WayPoint> trackSeg: trackSegs) {
    31             if (trackSeg != null && !trackSeg.isEmpty()) {
    32                 newSegments.add(new ImmutableGpxTrackSegment(trackSeg));
    33             }
    34         }
    35         this.attr = Collections.unmodifiableMap(new HashMap<>(attributes));
    36         this.segments = Collections.unmodifiableList(newSegments);
    37         this.length = calculateLength();
    38         this.bounds = calculateBounds();
     22        super(trackSegs, attributes);
    3923    }
    4024
     
    4832     * @since 13210
    4933     */
    50     public ImmutableGpxTrack(List<GpxTrackSegment> segments, Map<String, Object> attributes) {
    51         this.attr = Collections.unmodifiableMap(new HashMap<>(attributes));
    52         this.segments = Collections.unmodifiableList(segments);
    53         this.length = calculateLength();
    54         this.bounds = calculateBounds();
    55     }
    56 
    57     private double calculateLength() {
    58         double result = 0.0; // in meters
    59 
    60         for (GpxTrackSegment trkseg : segments) {
    61             result += trkseg.length();
    62         }
    63         return result;
    64     }
    65 
    66     private Bounds calculateBounds() {
    67         Bounds result = null;
    68         for (GpxTrackSegment segment: segments) {
    69             Bounds segBounds = segment.getBounds();
    70             if (segBounds != null) {
    71                 if (result == null) {
    72                     result = new Bounds(segBounds);
    73                 } else {
    74                     result.extend(segBounds);
    75                 }
    76             }
    77         }
    78         return result;
    79     }
    80 
    81     @Override
    82     public Map<String, Object> getAttributes() {
    83         return attr;
    84     }
    85 
    86     @Override
    87     public Bounds getBounds() {
    88         return bounds == null ? null : new Bounds(bounds);
    89     }
    90 
    91     @Override
    92     public double length() {
    93         return length;
    94     }
    95 
    96     @Override
    97     public Collection<GpxTrackSegment> getSegments() {
    98         return segments;
    99     }
    100 
    101     @Override
    102     public int hashCode() {
    103         return 31 * super.hashCode() + ((segments == null) ? 0 : segments.hashCode());
    104     }
    105 
    106     @Override
    107     public boolean equals(Object obj) {
    108         if (this == obj)
    109             return true;
    110         if (!super.equals(obj))
    111             return false;
    112         if (getClass() != obj.getClass())
    113             return false;
    114         ImmutableGpxTrack other = (ImmutableGpxTrack) obj;
    115         if (segments == null) {
    116             if (other.segments != null)
    117                 return false;
    118         } else if (!segments.equals(other.segments))
    119             return false;
    120         return true;
     34    public ImmutableGpxTrack(List<IGpxTrackSegment> segments, Map<String, Object> attributes) {
     35        super(segments, attributes);
    12136    }
    12237}
  • trunk/src/org/openstreetmap/josm/data/gpx/ImmutableGpxTrackSegment.java

    r12186 r15496  
    22package org.openstreetmap.josm.data.gpx;
    33
    4 import java.util.ArrayList;
    54import java.util.Collection;
    6 import java.util.Collections;
    7 import java.util.List;
    8 
    9 import org.openstreetmap.josm.data.Bounds;
    105
    116/**
    127 * A gpx track segment consisting of multiple waypoints, that cannot be changed.
     8 * @deprecated Use {@link GpxTrackSegment} instead!
    139 */
    14 public class ImmutableGpxTrackSegment implements GpxTrackSegment {
    15 
    16     private final List<WayPoint> wayPoints;
    17     private final Bounds bounds;
    18     private final double length;
     10@Deprecated
     11public class ImmutableGpxTrackSegment extends GpxTrackSegment {
    1912
    2013    /**
     
    2316     */
    2417    public ImmutableGpxTrackSegment(Collection<WayPoint> wayPoints) {
    25         this.wayPoints = Collections.unmodifiableList(new ArrayList<>(wayPoints));
    26         this.bounds = calculateBounds();
    27         this.length = calculateLength();
    28     }
    29 
    30     private Bounds calculateBounds() {
    31         Bounds result = null;
    32         for (WayPoint wpt: wayPoints) {
    33             if (result == null) {
    34                 result = new Bounds(wpt.getCoor());
    35             } else {
    36                 result.extend(wpt.getCoor());
    37             }
    38         }
    39         return result;
    40     }
    41 
    42     private double calculateLength() {
    43         double result = 0.0; // in meters
    44         WayPoint last = null;
    45         for (WayPoint tpt : wayPoints) {
    46             if (last != null) {
    47                 Double d = last.getCoor().greatCircleDistance(tpt.getCoor());
    48                 if (!d.isNaN() && !d.isInfinite()) {
    49                     result += d;
    50                 }
    51             }
    52             last = tpt;
    53         }
    54         return result;
    55     }
    56 
    57     @Override
    58     public Bounds getBounds() {
    59         return bounds == null ? null : new Bounds(bounds);
    60     }
    61 
    62     @Override
    63     public Collection<WayPoint> getWayPoints() {
    64         return Collections.unmodifiableList(wayPoints);
    65     }
    66 
    67     @Override
    68     public double length() {
    69         return length;
    70     }
    71 
    72     @Override
    73     public int getUpdateCount() {
    74         return 0;
    75     }
    76 
    77     @Override
    78     public int hashCode() {
    79         return 31 + ((wayPoints == null) ? 0 : wayPoints.hashCode());
    80     }
    81 
    82     @Override
    83     public boolean equals(Object obj) {
    84         if (this == obj)
    85             return true;
    86         if (obj == null)
    87             return false;
    88         if (getClass() != obj.getClass())
    89             return false;
    90         ImmutableGpxTrackSegment other = (ImmutableGpxTrackSegment) obj;
    91         if (wayPoints == null) {
    92             if (other.wayPoints != null)
    93                 return false;
    94         } else if (!wayPoints.equals(other.wayPoints))
    95             return false;
    96         return true;
     18        super(wayPoints);
    9719    }
    9820}
  • trunk/src/org/openstreetmap/josm/data/gpx/Line.java

    r14451 r15496  
    22package org.openstreetmap.josm.data.gpx;
    33
     4import java.awt.Color;
    45import java.util.Collection;
    56import java.util.Iterator;
     
    1415    private final Collection<WayPoint> waypoints;
    1516    private final boolean unordered;
     17    private final Color color;
    1618
    1719    /**
     
    1921     * @param waypoints collection of waypoints
    2022     * @param attributes track/route attributes
     23     * @param color color of the track
     24     * @since 15496
    2125     */
    22     public Line(Collection<WayPoint> waypoints, Map<String, Object> attributes) {
     26    public Line(Collection<WayPoint> waypoints, Map<String, Object> attributes, Color color) {
     27        this.color = color;
    2328        this.waypoints = Objects.requireNonNull(waypoints);
    2429        unordered = attributes.isEmpty() && waypoints.stream().allMatch(x -> x.get(GpxConstants.PT_TIME) == null);
     
    2934     * @param trackSegment track segment
    3035     * @param trackAttributes track attributes
     36     * @param color color of the track
     37     * @since 15496
    3138     */
    32     public Line(GpxTrackSegment trackSegment, Map<String, Object> trackAttributes) {
    33         this(trackSegment.getWayPoints(), trackAttributes);
     39    public Line(IGpxTrackSegment trackSegment, Map<String, Object> trackAttributes, Color color) {
     40        this(trackSegment.getWayPoints(), trackAttributes, color);
    3441    }
    3542
     
    3946     */
    4047    public Line(GpxRoute route) {
    41         this(route.routePoints, route.attr);
     48        this(route.routePoints, route.attr, null);
    4249    }
    4350
     
    4855    public boolean isUnordered() {
    4956        return unordered;
     57    }
     58
     59    /**
     60     * Returns the track/route color
     61     * @return the color
     62     * @since 15496
     63     */
     64    public Color getColor() {
     65        return color;
    5066    }
    5167
  • trunk/src/org/openstreetmap/josm/data/gpx/WithAttributes.java

    r10906 r15496  
    55import java.util.HashMap;
    66import java.util.Map;
     7import java.util.Objects;
    78
    89/**
     
    2021     */
    2122    public Map<String, Object> attr = new HashMap<>(0);
     23
     24    /**
     25     * The "exts" collection contains all extensions.
     26     */
     27    private final GpxExtensionCollection exts = new GpxExtensionCollection(this);
    2228
    2329    /**
     
    6571    /**
    6672     * Put a key / value pair as a new attribute.
    67      *
    6873     * Overrides key / value pair with the same key (if present).
    6974     *
     
    7681    }
    7782
    78     /**
    79      * Add a key / value pair that is not part of the GPX schema as an extension.
    80      *
    81      * @param key the key
    82      * @param value the value
    83      */
    8483    @Override
    85     public void addExtension(String key, String value) {
    86         if (!attr.containsKey(META_EXTENSIONS)) {
    87             attr.put(META_EXTENSIONS, new Extensions());
    88         }
    89         Extensions ext = (Extensions) attr.get(META_EXTENSIONS);
    90         ext.put(key, value);
     84    public Map<String, Object> getAttributes() {
     85        return attr;
     86    }
     87
     88    @Override
     89    public GpxExtensionCollection getExtensions() {
     90        return exts;
    9191    }
    9292
    9393    @Override
    9494    public int hashCode() {
    95         return 31 + ((attr == null) ? 0 : attr.hashCode());
     95        return Objects.hash(attr, exts);
    9696    }
    9797
     
    110110        } else if (!attr.equals(other.attr))
    111111            return false;
     112        if (exts == null) {
     113            if (other.exts != null)
     114                return false;
     115        } else if (!exts.equals(other.exts))
     116            return false;
    112117        return true;
    113118    }
  • trunk/src/org/openstreetmap/josm/data/osm/DataSet.java

    r15418 r15496  
    3333import org.openstreetmap.josm.data.coor.EastNorth;
    3434import org.openstreetmap.josm.data.coor.LatLon;
     35import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
    3536import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionAddEvent;
    3637import org.openstreetmap.josm.data.osm.DataSelectionListener.SelectionChangeEvent;
     
    171172
    172173    /**
     174     * Used to temporarily store namespaces from the GPX file in case the user converts back and forth.
     175     * Will not be saved to .osm files, but that's not necessary because GPX files won't automatically be overridden after that.
     176     */
     177    private List<XMLNamespace> GPXNamespaces;
     178
     179    /**
    173180     * Constructs a new {@code DataSet}.
    174181     */
     
    11951202
    11961203    /**
     1204     * Gets the GPX (XML) namespaces if this DataSet was created from a GPX file
     1205     * @return the GPXNamespaces or <code>null</code>
     1206     */
     1207    public List<XMLNamespace> getGPXNamespaces() {
     1208        return GPXNamespaces;
     1209    }
     1210
     1211    /**
     1212     * Sets the GPX (XML) namespaces
     1213     * @param GPXNamespaces the GPXNamespaces to set
     1214     */
     1215    public void setGPXNamespaces(List<XMLNamespace> GPXNamespaces) {
     1216        this.GPXNamespaces = GPXNamespaces;
     1217    }
     1218
     1219    /**
    11971220     * @return true if this Dataset contains no primitives
    11981221     * @since 14835
  • trunk/src/org/openstreetmap/josm/data/preferences/NamedColorProperty.java

    r13543 r15496  
    2323    public static final String COLOR_CATEGORY_GENERAL = "general";
    2424    public static final String COLOR_CATEGORY_MAPPAINT = "mappaint";
    25     public static final String COLOR_CATEGORY_LAYER = "layer";
    2625
    2726    private final String category;
     
    3231     * Construct a new {@code NamedColorProperty}.
    3332     * @param category a category, can be any identifier, but the following values are recognized by
    34      * the GUI preferences: {@link #COLOR_CATEGORY_GENERAL}, {@link #COLOR_CATEGORY_MAPPAINT} and
    35      * {@link #COLOR_CATEGORY_LAYER}
     33     * the GUI preferences: {@link #COLOR_CATEGORY_GENERAL} and {@link #COLOR_CATEGORY_MAPPAINT}
    3634     * @param source a filename or similar associated with the color, can be null if not applicable
    3735     * @param name a short description of the color
  • trunk/src/org/openstreetmap/josm/gui/dialogs/LayerListDialog.java

    r15457 r15496  
    44import static org.openstreetmap.josm.tools.I18n.tr;
    55
    6 import java.awt.Color;
    76import java.awt.Component;
    87import java.awt.Dimension;
     
    1817import java.util.Arrays;
    1918import java.util.List;
    20 import java.util.Objects;
     19import java.util.Optional;
    2120import java.util.concurrent.CopyOnWriteArrayList;
    2221
     
    4342import org.openstreetmap.josm.data.coor.EastNorth;
    4443import org.openstreetmap.josm.data.imagery.OffsetBookmark;
    45 import org.openstreetmap.josm.data.preferences.AbstractProperty;
    4644import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
    4745import org.openstreetmap.josm.data.preferences.BooleanProperty;
     
    612610            }
    613611            if (Config.getPref().getBoolean("dialog.layer.colorname", true)) {
    614                 AbstractProperty<Color> prop = layer.getColorProperty();
    615                 Color c = prop == null ? null : prop.get();
    616                 if (c == null || model.getLayers().stream()
    617                         .map(Layer::getColorProperty)
    618                         .filter(Objects::nonNull)
    619                         .map(AbstractProperty::get)
    620                         .noneMatch(oc -> oc != null && !oc.equals(c))) {
    621                     /* not more than one color, don't use coloring */
    622                     label.setForeground(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground"));
    623                 } else {
    624                     label.setForeground(c);
    625                 }
     612                label.setForeground(Optional
     613                        .ofNullable(layer.getColor())
     614                        .orElse(UIManager.getColor(isSelected ? "Table.selectionForeground" : "Table.foreground")));
    626615            }
    627616            label.setIcon(layer.getIcon());
  • trunk/src/org/openstreetmap/josm/gui/dialogs/layer/LayerVisibilityAction.java

    r14387 r15496  
    594594                    for (Layer l : layers) {
    595595                        if (l instanceof GpxLayer) {
    596                             l.getColorProperty().put(color);
     596                            l.setColor(color);
    597597                        }
    598598                    }
     
    603603            panels.put(color, colorPanel);
    604604
    605             List<Color> colors = layerSupplier.get().stream().map(l -> l.getColorProperty().get()).distinct().collect(Collectors.toList());
     605            List<Color> colors = layerSupplier.get().stream().map(l -> l.getColor()).distinct().collect(Collectors.toList());
    606606            if (colors.size() == 1) {
    607607                highlightColor(colors.get(0));
     
    612612        public void updateLayers(List<Layer> layers, boolean allVisible, boolean allHidden) {
    613613            List<Color> colors = layers.stream().filter(l -> l instanceof GpxLayer)
    614                     .map(l -> ((GpxLayer) l).getColorProperty().get())
     614                    .map(l -> ((GpxLayer) l).getColor())
    615615                    .distinct()
    616616                    .collect(Collectors.toList());
  • trunk/src/org/openstreetmap/josm/gui/io/importexport/FileExporter.java

    r14950 r15496  
    5656
    5757    /**
     58     * Execute the data export without prompting the user. (To be overridden by subclasses.)
     59     *
     60     * @param file target file
     61     * @param layer the layer to export
     62     * @throws IOException in case of an IO error
     63     * @since 15496
     64     */
     65    public void exportDataQuiet(File file, Layer layer) throws IOException {
     66        exportData(file, layer); //backwards compatibility
     67    }
     68
     69    /**
    5870     * Returns the enabled state of this {@code FileExporter}. When enabled, it is listed and usable in "File-&gt;Save" dialogs.
    5971     * @return true if this {@code FileExporter} is enabled
  • trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxExporter.java

    r14950 r15496  
    2626import org.openstreetmap.josm.data.gpx.GpxConstants;
    2727import org.openstreetmap.josm.data.gpx.GpxData;
     28import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
    2829import org.openstreetmap.josm.gui.ExtendedDialog;
    2930import org.openstreetmap.josm.gui.MainApplication;
     
    7879    @Override
    7980    public void exportData(File file, Layer layer) throws IOException {
     81        exportData(file, layer, false);
     82    }
     83
     84    @Override
     85    public void exportDataQuiet(File file, Layer layer) throws IOException {
     86        exportData(file, layer, true);
     87    }
     88
     89    private void exportData(File file, Layer layer, boolean quiet) throws IOException {
    8090        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
    8191        if (!(layer instanceof OsmDataLayer) && !(layer instanceof GpxLayer))
     
    90100        }
    91101
     102        GpxData gpxData;
     103        if (quiet) {
     104            gpxData = getGpxData(layer, file);
     105            try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) {
     106                GpxWriter w = new GpxWriter(fo);
     107                w.write(gpxData);
     108                w.close();
     109                fo.flush();
     110            }
     111            return;
     112        }
     113
    92114        // open the dialog asking for options
    93115        JPanel p = new JPanel(new GridBagLayout());
    94116
    95         GpxData gpxData;
    96117        // At this moment, we only need to know the attributes of the GpxData,
    97118        // conversion of OsmDataLayer (if needed) will be done after the dialog is closed.
     
    147168        JosmTextField keywords = new JosmTextField();
    148169        keywords.setText(gpxData.getString(META_KEYWORDS));
    149         p.add(keywords, GBC.eop().fill(GBC.HORIZONTAL));
     170        p.add(keywords, GBC.eol().fill(GBC.HORIZONTAL));
     171
     172        boolean sel = Config.getPref().getBoolean("gpx.export.colors", true);
     173        JCheckBox colors = new JCheckBox(tr("Save track colors in GPX file"), sel);
     174        p.add(colors, GBC.eol().fill(GBC.HORIZONTAL));
     175        JCheckBox garmin = new JCheckBox(tr("Use Garmin compatible GPX extensions"),
     176                Config.getPref().getBoolean("gpx.export.colors.garmin", false));
     177        garmin.setEnabled(sel);
     178        p.add(garmin, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 0));
     179
     180        boolean hasPrefs = !gpxData.getLayerPrefs().isEmpty();
     181        JCheckBox layerPrefs = new JCheckBox(tr("Save layer specific preferences"),
     182                hasPrefs && Config.getPref().getBoolean("gpx.export.prefs", true));
     183        layerPrefs.setEnabled(hasPrefs);
     184        p.add(layerPrefs, GBC.eop().fill(GBC.HORIZONTAL));
    150185
    151186        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
     
    155190            .setContent(p);
    156191
     192        colors.addActionListener(l -> {
     193            garmin.setEnabled(colors.isSelected());
     194        });
     195
     196        garmin.addActionListener(l -> {
     197            if (garmin.isSelected() &&
     198                    !ConditionalOptionPaneUtil.showConfirmationDialog(
     199                            "gpx_color_garmin",
     200                            ed,
     201                            new JLabel(tr("<html>Garmin track extensions only support 16 colors.<br>If you continue, the closest supported track color will be used.</html>")),
     202                            tr("Information"),
     203                            JOptionPane.OK_CANCEL_OPTION,
     204                            JOptionPane.INFORMATION_MESSAGE,
     205                            JOptionPane.OK_OPTION)) {
     206                garmin.setSelected(false);
     207            }
     208        });
     209
    157210        if (ed.showDialog().getValue() != 1) {
    158211            setCanceled(true);
     
    168221            Config.getPref().put("lastCopyright", copyright.getText());
    169222        }
    170 
    171         if (layer instanceof OsmDataLayer) {
    172             gpxData = ((OsmDataLayer) layer).toGpxData();
    173         } else if (layer instanceof GpxLayer) {
    174             gpxData = ((GpxLayer) layer).data;
    175         } else {
    176             gpxData = OsmDataLayer.toGpxData(MainApplication.getLayerManager().getEditDataSet(), file);
    177         }
     223        Config.getPref().putBoolean("gpx.export.colors", colors.isSelected());
     224        Config.getPref().putBoolean("gpx.export.colors.garmin", garmin.isSelected());
     225        if (hasPrefs) {
     226            Config.getPref().putBoolean("gpx.export.prefs", layerPrefs.isSelected());
     227        }
     228        ColorFormat cFormat = null;
     229        if (colors.isSelected()) {
     230            cFormat = garmin.isSelected() ? ColorFormat.GPXX : ColorFormat.GPXD;
     231        }
     232
     233        gpxData = getGpxData(layer, file);
    178234
    179235        // add author and copyright details to the gpx data
     
    204260        }
    205261
    206         try (OutputStream fo = Compression.getCompressedFileOutputStream(file); GpxWriter writer = new GpxWriter(fo)) {
    207             writer.write(gpxData);
    208         }
     262        try (OutputStream fo = Compression.getCompressedFileOutputStream(file)) {
     263            GpxWriter w = new GpxWriter(fo);
     264            w.write(gpxData, cFormat, layerPrefs.isSelected());
     265            w.close();
     266            fo.flush();
     267        }
     268    }
     269
     270    private static GpxData getGpxData(Layer layer, File file) {
     271        if (layer instanceof OsmDataLayer) {
     272            return ((OsmDataLayer) layer).toGpxData();
     273        } else if (layer instanceof GpxLayer) {
     274            return ((GpxLayer) layer).data;
     275        }
     276        return OsmDataLayer.toGpxData(MainApplication.getLayerManager().getEditDataSet(), file);
    209277    }
    210278
  • trunk/src/org/openstreetmap/josm/gui/io/importexport/GpxImporter.java

    r14761 r15496  
    150150            if (markerLayer.data.isEmpty()) {
    151151                markerLayer = null;
     152            } else {
     153                gpxLayer.setLinkedMarkerLayer(markerLayer);
    152154            }
    153155        }
  • trunk/src/org/openstreetmap/josm/gui/layer/CustomizeColor.java

    r14153 r15496  
    1919import javax.swing.JOptionPane;
    2020
    21 import org.openstreetmap.josm.data.preferences.AbstractProperty;
    2221import org.openstreetmap.josm.gui.MainApplication;
    2322import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
    2423import org.openstreetmap.josm.gui.layer.Layer.LayerAction;
    2524import org.openstreetmap.josm.gui.layer.Layer.MultiLayerAction;
    26 import org.openstreetmap.josm.tools.CheckParameterUtil;
    2725import org.openstreetmap.josm.tools.ImageProvider;
    2826
     
    3432 */
    3533public class CustomizeColor extends AbstractAction implements LayerAction, MultiLayerAction {
    36     private final transient List<AbstractProperty<Color>> colors;
     34    private final transient List<Layer> colorLayers;
    3735
    3836    /**
     
    4341        super(tr("Customize Color"));
    4442        new ImageProvider("colorchooser").getResource().attachImageIcon(this, true);
    45         colors = l.stream().map(Layer::getColorProperty).collect(Collectors.toList());
    46         CheckParameterUtil.ensureThat(colors.stream().allMatch(Objects::nonNull), "All layers must have colors.");
     43        colorLayers = l.stream().filter(Objects::nonNull).filter(Layer::hasColor).collect(Collectors.toList());
    4744        putValue("help", ht("/Action/LayerCustomizeColor"));
    4845    }
     
    5855    @Override
    5956    public boolean supportLayers(List<Layer> layers) {
    60         return layers.stream().allMatch(l -> l.getColorProperty() != null);
     57        return layers.stream().allMatch(Layer::hasColor);
    6158    }
    6259
     
    7370    @Override
    7471    public void actionPerformed(ActionEvent e) {
    75         Color cl = colors.stream().map(AbstractProperty::get).filter(Objects::nonNull).findAny().orElse(Color.GRAY);
     72        Color cl = colorLayers.stream().filter(Objects::nonNull).map(Layer::getColor).filter(Objects::nonNull).findAny().orElse(Color.GRAY);
    7673        JColorChooser c = new JColorChooser(cl);
    7774        Object[] options = new Object[]{tr("OK"), tr("Cancel"), tr("Default")};
     
    8885        switch (answer) {
    8986        case 0:
    90             colors.stream().forEach(prop -> prop.put(c.getColor()));
     87            colorLayers.stream().forEach(l -> l.setColor(c.getColor()));
    9188            break;
    9289        case 1:
    9390            return;
    9491        case 2:
    95             colors.stream().forEach(prop -> prop.put(null));
     92            colorLayers.stream().forEach(l -> l.setColor(null));
    9693            break;
    9794        }
  • trunk/src/org/openstreetmap/josm/gui/layer/GpxLayer.java

    r15421 r15496  
    55import static org.openstreetmap.josm.tools.I18n.trn;
    66
     7import java.awt.Color;
    78import java.awt.Dimension;
    89import java.awt.Graphics2D;
     
    1415import java.util.Date;
    1516import java.util.List;
     17import java.util.stream.Collectors;
    1618
    1719import javax.swing.AbstractAction;
     
    3234import org.openstreetmap.josm.data.gpx.GpxTrack;
    3335import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    34 import org.openstreetmap.josm.data.preferences.NamedColorProperty;
    3536import org.openstreetmap.josm.data.projection.Projection;
    3637import org.openstreetmap.josm.gui.MapView;
     
    4748import org.openstreetmap.josm.gui.layer.gpx.ImportImagesAction;
    4849import org.openstreetmap.josm.gui.layer.gpx.MarkersFromNamedPointsAction;
     50import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
     51import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
    4952import org.openstreetmap.josm.gui.widgets.HtmlPanel;
    5053import org.openstreetmap.josm.tools.ImageProvider;
     54import org.openstreetmap.josm.tools.Logging;
    5155import org.openstreetmap.josm.tools.Utils;
    5256import org.openstreetmap.josm.tools.date.DateUtils;
     
    5559 * A layer that displays data from a Gpx file / the OSM gpx downloads.
    5660 */
    57 public class GpxLayer extends Layer implements ExpertModeChangeListener {
     61public class GpxLayer extends AbstractModifiableLayer implements ExpertModeChangeListener {
    5862
    5963    /** GPX data */
    6064    public GpxData data;
    61     private final boolean isLocalFile;
     65    private boolean isLocalFile;
    6266    private boolean isExpertMode;
     67
    6368    /**
    6469     * used by {@link ChooseTrackVisibilityAction} to determine which tracks to show/hide
     
    7378     */
    7479    private final GpxDataChangeListener dataChangeListener = e -> this.invalidate();
     80    /**
     81     * The MarkerLayer imported from the same file.
     82     */
     83    private MarkerLayer linkedMarkerLayer;
    7584
    7685    /**
     
    109118
    110119    @Override
    111     protected NamedColorProperty getBaseColorProperty() {
    112         return GpxDrawHelper.DEFAULT_COLOR;
     120    public Color getColor() {
     121        Color[] c = data.getTracks().stream().map(t -> t.getColor()).distinct().toArray(Color[]::new);
     122        return c.length == 1 ? c[0] : null; //only return if exactly one distinct color present
     123    }
     124
     125    @Override
     126    public void setColor(Color color) {
     127        data.beginUpdate();
     128        for (GpxTrack trk : data.getTracks()) {
     129            trk.setColor(color);
     130        }
     131        GPXSettingsPanel.putLayerPrefLocal(this, "colormode", "0");
     132        data.endUpdate();
     133    }
     134
     135    @Override
     136    public boolean hasColor() {
     137        return true;
    113138    }
    114139
     
    277302            .append(trn("{0} route, ", "{0} routes, ", data.getRoutes().size(), data.getRoutes().size()))
    278303            .append(trn("{0} waypoint", "{0} waypoints", data.getWaypoints().size(), data.getWaypoints().size())).append("<br>")
    279             .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())))
    280             .append("<br></html>");
     304            .append(tr("Length: {0}", SystemOfMeasurement.getSystemOfMeasurement().getDistText(data.length())));
     305
     306        if (Logging.isDebugEnabled() && !data.getLayerPrefs().isEmpty()) {
     307            info.append("<br><br>")
     308                .append(String.join("<br>", data.getLayerPrefs().entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.toList())));
     309        }
     310
     311        info.append("<br></html>");
     312
    281313        return info.toString();
    282314    }
     
    340372    public void setAssociatedFile(File file) {
    341373        data.storageFile = file;
     374    }
     375
     376    /**
     377     * @return the linked MarkerLayer (imported from the same file)
     378     * @since 15496
     379     */
     380    public MarkerLayer getLinkedMarkerLayer() {
     381        return linkedMarkerLayer;
     382    }
     383
     384    /**
     385     * @param linkedMarkerLayer the linked MarkerLayer
     386     * @since 15496
     387     */
     388    public void setLinkedMarkerLayer(MarkerLayer linkedMarkerLayer) {
     389        this.linkedMarkerLayer = linkedMarkerLayer;
    342390    }
    343391
     
    481529
    482530    @Override
     531    public boolean isModified() {
     532        return data.isModified();
     533    }
     534
     535    @Override
     536    public boolean requiresSaveToFile() {
     537        return isModified() && isLocalFile();
     538    }
     539
     540    @Override
     541    public void onPostSaveToFile() {
     542        isLocalFile = true;
     543        data.invalidate();
     544        data.setModified(false);
     545    }
     546
     547    @Override
    483548    public String getChangesetSourceTag() {
    484549        // no i18n for international values
  • trunk/src/org/openstreetmap/josm/gui/layer/Layer.java

    r15371 r15496  
    2626import org.openstreetmap.josm.data.ProjectionBounds;
    2727import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
    28 import org.openstreetmap.josm.data.preferences.AbstractProperty;
    29 import org.openstreetmap.josm.data.preferences.AbstractProperty.ValueChangeListener;
    30 import org.openstreetmap.josm.data.preferences.NamedColorProperty;
    3128import org.openstreetmap.josm.data.projection.Projection;
    3229import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
     
    165162    private File associatedFile;
    166163
    167     private final ValueChangeListener<Object> invalidateListener = change -> invalidate();
    168164    private boolean isDestroyed;
    169165
     
    199195
    200196    /**
    201      * Gets the color property to use for this layer.
    202      * @return The color property.
    203      * @since 10824
    204      */
    205     public AbstractProperty<Color> getColorProperty() {
    206         NamedColorProperty base = getBaseColorProperty();
    207         if (base != null) {
    208             return base.getChildColor(NamedColorProperty.COLOR_CATEGORY_LAYER, getName(), base.getName());
    209         } else {
    210             return null;
    211         }
    212     }
    213 
    214     /**
    215      * Gets the color property that stores the default color for this layer.
    216      * @return The property or <code>null</code> if this layer is not colored.
    217      * @since 10824
    218      */
    219     protected NamedColorProperty getBaseColorProperty() {
     197     * @return whether the layer has / can handle colors.
     198     * @since 15496
     199     */
     200    public boolean hasColor() {
     201        return false;
     202    }
     203
     204    /**
     205     * Return the current color of the layer
     206     * @return null when not present or not supported
     207     * @since 15496
     208     */
     209    public Color getColor() {
    220210        return null;
    221211    }
    222212
    223     private void addColorPropertyListener() {
    224         AbstractProperty<Color> colorProperty = getColorProperty();
    225         if (colorProperty != null) {
    226             colorProperty.addListener(invalidateListener);
    227         }
    228     }
    229 
    230     private void removeColorPropertyListener() {
    231         AbstractProperty<Color> colorProperty = getColorProperty();
    232         if (colorProperty != null) {
    233             colorProperty.removeListener(invalidateListener);
    234         }
     213    /**
     214     * Sets the color for this layer. Nothing happens if not supported by the layer
     215     * @param color the color to be set, <code>null</code> for default
     216     * @since 15496
     217     */
     218    public void setColor(Color color) {
    235219    }
    236220
     
    303287        isDestroyed = true;
    304288        // Override in subclasses if needed
    305         removeColorPropertyListener();
    306289    }
    307290
     
    340323     */
    341324    public void setName(String name) {
    342         if (this.name != null) {
    343             removeColorPropertyListener();
    344         }
    345325        String oldValue = this.name;
    346326        this.name = Optional.ofNullable(name).orElse("");
     
    348328            propertyChangeSupport.firePropertyChange(NAME_PROP, oldValue, this.name);
    349329        }
    350 
    351         // re-add listener
    352         addColorPropertyListener();
    353330        invalidate();
    354331    }
     
    540517        @Override
    541518        public void actionPerformed(ActionEvent e) {
    542             SaveAction.getInstance().doSave(layer);
     519            SaveAction.getInstance().doSave(layer, true);
    543520        }
    544521    }
  • trunk/src/org/openstreetmap/josm/gui/layer/OsmDataLayer.java

    r15422 r15496  
    3232import java.util.List;
    3333import java.util.Map;
     34import java.util.Map.Entry;
    3435import java.util.Optional;
    3536import java.util.Set;
     
    6162import org.openstreetmap.josm.data.gpx.GpxConstants;
    6263import org.openstreetmap.josm.data.gpx.GpxData;
     64import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
    6365import org.openstreetmap.josm.data.gpx.GpxLink;
    64 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
     66import org.openstreetmap.josm.data.gpx.GpxTrack;
     67import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     68import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    6569import org.openstreetmap.josm.data.gpx.WayPoint;
    6670import org.openstreetmap.josm.data.osm.DataIntegrityProblemException;
     
    758762    public static GpxData toGpxData(DataSet data, File file) {
    759763        GpxData gpxData = new GpxData();
     764        if (data.getGPXNamespaces() != null) {
     765            gpxData.getNamespaces().addAll(data.getGPXNamespaces());
     766        }
    760767        gpxData.storageFile = file;
    761768        Set<Node> doneNodes = new HashSet<>();
     
    778785                return;
    779786            }
    780             Collection<Collection<WayPoint>> trk = new ArrayList<>();
     787            List<IGpxTrackSegment> trk = new ArrayList<>();
    781788            Map<String, Object> trkAttr = new HashMap<>();
    782789
    783             String name = gpxVal(w, "name");
    784             if (name != null) {
    785                 trkAttr.put("name", name);
    786             }
    787 
    788             List<WayPoint> trkseg = null;
     790            GpxExtensionCollection trkExts = new GpxExtensionCollection();
     791            GpxExtensionCollection segExts = new GpxExtensionCollection();
     792            for (Entry<String, String> e : w.getKeys().entrySet()) {
     793                String k = e.getKey().startsWith(GpxConstants.GPX_PREFIX) ? e.getKey().substring(GpxConstants.GPX_PREFIX.length()) : e.getKey();
     794                String v = e.getValue();
     795                if (GpxConstants.RTE_TRK_KEYS.contains(k)) {
     796                    trkAttr.put(k, v);
     797                } else {
     798                    k = GpxConstants.EXTENSION_ABBREVIATIONS.entrySet()
     799                            .stream()
     800                            .filter(s -> s.getValue().equals(e.getKey()))
     801                            .map(s -> s.getKey().substring(GpxConstants.GPX_PREFIX.length()))
     802                            .findAny()
     803                            .orElse(k);
     804                    if (k.startsWith("extension")) {
     805                        String[] chain = k.split(":");
     806                        if (chain.length >= 3 && "segment".equals(chain[2])) {
     807                            segExts.addFlat(chain, v);
     808                        } else {
     809                            trkExts.addFlat(chain, v);
     810                        }
     811                    }
     812
     813                }
     814            }
     815            List<WayPoint> trkseg = new ArrayList<>();
    789816            for (Node n : w.getNodes()) {
    790817                if (!n.isUsable()) {
    791                     trkseg = null;
     818                    if (!trkseg.isEmpty()) {
     819                        trk.add(new GpxTrackSegment(trkseg));
     820                        trkseg.clear();
     821                    }
    792822                    continue;
    793                 }
    794                 if (trkseg == null) {
    795                     trkseg = new ArrayList<>();
    796                     trk.add(trkseg);
    797823                }
    798824                if (!n.isTagged() || containsOnlyGpxTags(n)) {
     
    801827                trkseg.add(nodeToWayPoint(n));
    802828            }
    803 
    804             gpxData.addTrack(new ImmutableGpxTrack(trk, trkAttr));
     829            trk.add(new GpxTrackSegment(trkseg));
     830            trk.forEach(gpxseg -> gpxseg.getExtensions().addAll(segExts));
     831            GpxTrack gpxtrk = new GpxTrack(trk, trkAttr);
     832            gpxtrk.getExtensions().addAll(trkExts);
     833            gpxData.addTrack(gpxtrk);
    805834        });
    806835    }
  • trunk/src/org/openstreetmap/josm/gui/layer/geoimage/CorrelateGpxWithImages.java

    r15247 r15496  
    7171import org.openstreetmap.josm.data.gpx.GpxTimezone;
    7272import org.openstreetmap.josm.data.gpx.GpxTrack;
    73 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     73import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    7474import org.openstreetmap.josm.data.gpx.WayPoint;
    7575import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
     
    12691269        // Finds first GPX point
    12701270        outer: for (GpxTrack trk : gpx.tracks) {
    1271             for (GpxTrackSegment segment : trk.getSegments()) {
     1271            for (IGpxTrackSegment segment : trk.getSegments()) {
    12721272                for (WayPoint curWp : segment.getWayPoints()) {
    12731273                    if (curWp.hasDate()) {
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/ChooseTrackVisibilityAction.java

    r14337 r15496  
    55import static org.openstreetmap.josm.tools.I18n.tr;
    66
     7import java.awt.Color;
    78import java.awt.Component;
    89import java.awt.Dimension;
     
    1314import java.awt.event.MouseListener;
    1415import java.io.Serializable;
     16import java.util.ArrayList;
    1517import java.util.Arrays;
    1618import java.util.Comparator;
     19import java.util.List;
    1720import java.util.Map;
     21import java.util.Objects;
    1822import java.util.Optional;
    1923
    2024import javax.swing.AbstractAction;
     25import javax.swing.JColorChooser;
    2126import javax.swing.JComponent;
    2227import javax.swing.JLabel;
     28import javax.swing.JOptionPane;
    2329import javax.swing.JPanel;
    2430import javax.swing.JScrollPane;
     
    2632import javax.swing.JToggleButton;
    2733import javax.swing.ListSelectionModel;
     34import javax.swing.event.TableModelEvent;
    2835import javax.swing.table.DefaultTableModel;
    2936import javax.swing.table.TableCellRenderer;
    3037import javax.swing.table.TableRowSorter;
    3138
     39import org.apache.commons.jcs.access.exception.InvalidArgumentException;
    3240import org.openstreetmap.josm.data.SystemOfMeasurement;
    3341import org.openstreetmap.josm.data.gpx.GpxConstants;
     
    3644import org.openstreetmap.josm.gui.MainApplication;
    3745import org.openstreetmap.josm.gui.layer.GpxLayer;
     46import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
    3847import org.openstreetmap.josm.gui.util.WindowGeometry;
    3948import org.openstreetmap.josm.tools.GBC;
     
    5665     */
    5766    public ChooseTrackVisibilityAction(final GpxLayer layer) {
    58         super(tr("Choose visible tracks"));
     67        super(tr("Choose track visibility and colors"));
    5968        new ImageProvider("dialogs/filter").getResource().attachImageIcon(this, true);
    6069        this.layer = layer;
     
    117126            TrackLength length = new TrackLength(trk.length());
    118127            String url = (String) Optional.ofNullable(attr.get("url")).orElse("");
    119             tracks[i] = new Object[]{name, desc, time, length, url};
     128            tracks[i] = new Object[]{name, desc, time, length, url, trk};
    120129            i++;
    121130        }
     
    123132    }
    124133
     134    private void showColorDialog(List<GpxTrack> tracks) {
     135        Color cl = tracks.stream().filter(Objects::nonNull)
     136                .map(GpxTrack::getColor).filter(Objects::nonNull)
     137                .findAny().orElse(GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get());
     138        JColorChooser c = new JColorChooser(cl);
     139        Object[] options = new Object[]{tr("OK"), tr("Cancel"), tr("Default")};
     140        int answer = JOptionPane.showOptionDialog(
     141                MainApplication.getMainFrame(),
     142                c,
     143                tr("Choose a color"),
     144                JOptionPane.OK_CANCEL_OPTION,
     145                JOptionPane.PLAIN_MESSAGE,
     146                null,
     147                options,
     148                options[0]
     149        );
     150        switch (answer) {
     151        case 0:
     152            tracks.stream().forEach(t -> t.setColor(c.getColor()));
     153            GPXSettingsPanel.putLayerPrefLocal(layer, "colormode", "0"); //set Colormode to none
     154            break;
     155        case 1:
     156            return;
     157        case 2:
     158            tracks.stream().forEach(t -> t.setColor(null));
     159            break;
     160        }
     161        table.repaint();
     162    }
     163
    125164    /**
    126      * Builds an non-editable table whose 5th column will open a browser when double clicked.
     165     * Builds an editable table whose 5th column will open a browser when double clicked.
    127166     * The table will fill its parent.
    128167     * @param content table data
     
    139178                    JComponent jc = (JComponent) c;
    140179                    jc.setToolTipText(getValueAt(row, col).toString());
     180                    if (content.length > row
     181                            && content[row].length > 5
     182                            && content[row][5] instanceof GpxTrack) {
     183                        Color color = ((GpxTrack) content[row][5]).getColor();
     184                        if (color != null) {
     185                            double brightness = Math.sqrt(Math.pow(color.getRed(), 2) * .241
     186                                    + Math.pow(color.getGreen(), 2) * .691
     187                                    + Math.pow(color.getBlue(), 2) * .068);
     188                            if (brightness > 250) {
     189                                color = color.darker();
     190                            }
     191                            if (isRowSelected(row)) {
     192                                jc.setBackground(color);
     193                                if (brightness <= 130) {
     194                                    jc.setForeground(Color.WHITE);
     195                                } else {
     196                                    jc.setForeground(Color.BLACK);
     197                                }
     198                            } else {
     199                                if (brightness > 200) {
     200                                    color = color.darker(); //brightness >250 is darkened twice on purpose
     201                                }
     202                                jc.setForeground(color);
     203                                jc.setBackground(Color.WHITE);
     204                            }
     205                        } else {
     206                            jc.setForeground(Color.BLACK);
     207                            if (isRowSelected(row)) {
     208                                jc.setBackground(new Color(175, 210, 210));
     209                            } else {
     210                                jc.setBackground(Color.WHITE);
     211                            }
     212                        }
     213                    }
    141214                }
    142215                return c;
     
    145218            @Override
    146219            public boolean isCellEditable(int rowIndex, int colIndex) {
    147                 return false;
     220                return colIndex <= 1;
     221            }
     222
     223            @Override
     224            public void tableChanged(TableModelEvent e) {
     225                super.tableChanged(e);
     226                int col = e.getColumn();
     227                int row = e.getFirstRow();
     228                if (row >= 0 && row < content.length && col >= 0 && col <= 1) {
     229                    Object t = content[row][5];
     230                    String val = (String) getValueAt(row, col);
     231                    if (t != null && t instanceof GpxTrack) {
     232                        GpxTrack trk = (GpxTrack) t;
     233                        if (col == 0) {
     234                            trk.put("name", val);
     235                        } else {
     236                            trk.put("desc", val);
     237                        }
     238                    } else {
     239                        throw new InvalidArgumentException("Invalid object in table, must be GpxTrack.");
     240                    }
     241                }
    148242            }
    149243        };
     
    181275        t.addMouseListener(urlOpener);
    182276        t.setFillsViewportHeight(true);
     277        t.putClientProperty("terminateEditOnFocusLost", true);
    183278        return t;
    184279    }
     
    250345        msg.add(new JLabel(tr("<html>Select all tracks that you want to be displayed. " +
    251346                "You can drag select a range of tracks or use CTRL+Click to select specific ones. " +
    252                 "The map is updated live in the background. Open the URLs by double clicking them.</html>")),
     347                "The map is updated live in the background. Open the URLs by double clicking them, " +
     348                "edit name and description by double clicking the cell.</html>")),
    253349                GBC.eop().fill(GBC.HORIZONTAL));
    254350        // build table
    255351        final boolean[] trackVisibilityBackup = layer.trackVisibility.clone();
    256         table = buildTable(buildTableContents());
     352        Object[][] content = buildTableContents();
     353        table = buildTable(content);
    257354        selectVisibleTracksInTable();
    258355        listenToSelectionChanges();
     
    263360        int v = 1;
    264361        // build dialog
    265         ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Set track visibility for {0}", layer.getName()),
    266                 tr("Show all"), tr("Show selected only"), tr("Cancel"));
    267         ed.setButtonIcons("eye", "dialogs/filter", "cancel");
     362        ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(),
     363                tr("Set track visibility for {0}", layer.getName()),
     364                tr("Set color for selected tracks..."), tr("Show all"), tr("Show selected only"), tr("Close")) {
     365            @Override
     366            protected void buttonAction(int buttonIndex, ActionEvent evt) {
     367                if (buttonIndex == 0) {
     368                    List<GpxTrack> trks = new ArrayList<>();
     369                    for (int i : table.getSelectedRows()) {
     370                        Object trk = content[i][5];
     371                        if (trk != null && trk instanceof GpxTrack) {
     372                            trks.add((GpxTrack) trk);
     373                        }
     374                    }
     375                    showColorDialog(trks);
     376                } else {
     377                    super.buttonAction(buttonIndex, evt);
     378                }
     379            }
     380        };
     381        ed.setButtonIcons("colorchooser", "eye", "dialogs/filter", "cancel");
    268382        ed.setContent(msg, false);
    269383        ed.setDefaultButton(2);
     
    276390        v = ed.getValue();
    277391        // cancel for unknown buttons and copy back original settings
    278         if (v != 1 && v != 2) {
     392        if (v != 2 && v != 3) {
    279393            layer.trackVisibility = Arrays.copyOf(trackVisibilityBackup, layer.trackVisibility.length);
    280394            MainApplication.getMap().repaint();
    281395            return;
    282396        }
    283         // set visibility (1 = show all, 2 = filter). If no tracks are selected
     397        // set visibility (2 = show all, 3 = filter). If no tracks are selected
    284398        // set all of them visible and...
    285399        ListSelectionModel s = table.getSelectionModel();
    286         final boolean all = v == 1 || s.isSelectionEmpty();
     400        final boolean all = v == 2 || s.isSelectionEmpty();
    287401        for (int i = 0; i < layer.trackVisibility.length; i++) {
    288402            layer.trackVisibility[table.convertRowIndexToModel(i)] = all || s.isSelectedIndex(i);
     
    291405        layer.invalidate();
    292406        // ...sync with layer visibility instead to avoid having two ways to hide everything
    293         layer.setVisible(v == 1 || !s.isSelectionEmpty());
     407        layer.setVisible(v == 2 || !s.isSelectionEmpty());
    294408    }
    295409}
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/ConvertFromGpxLayerAction.java

    r15419 r15496  
    88import java.awt.event.ActionListener;
    99import java.util.ArrayList;
    10 import java.util.Collection;
    1110import java.util.Date;
    1211import java.util.List;
     12import java.util.Map;
    1313import java.util.Map.Entry;
    1414
     
    2121
    2222import org.openstreetmap.josm.data.gpx.GpxConstants;
    23 import org.openstreetmap.josm.data.gpx.GpxTrack;
    24 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     23import org.openstreetmap.josm.data.gpx.GpxExtension;
     24import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
     25import org.openstreetmap.josm.data.gpx.IGpxTrack;
     26import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    2527import org.openstreetmap.josm.data.gpx.WayPoint;
    2628import org.openstreetmap.josm.data.osm.DataSet;
    2729import org.openstreetmap.josm.data.osm.Node;
     30import org.openstreetmap.josm.data.osm.OsmPrimitive;
    2831import org.openstreetmap.josm.data.osm.Way;
    2932import org.openstreetmap.josm.gui.ExtendedDialog;
     
    5457    public DataSet convert() {
    5558        final DataSet ds = new DataSet();
     59        ds.setGPXNamespaces(layer.data.getNamespaces());
    5660
    5761        List<String> keys = new ArrayList<>(); // note that items in this list don't have the GPX_PREFIX
     
    6064        boolean none = "no".equals(convertTags); // no need to convert tags when no dialog will be shown anyways
    6165
    62         for (GpxTrack trk : layer.data.getTracks()) {
    63             for (GpxTrackSegment segment : trk.getSegments()) {
     66        for (IGpxTrack trk : layer.data.getTracks()) {
     67            for (IGpxTrackSegment segment : trk.getSegments()) {
    6468                List<Node> nodes = new ArrayList<>();
    6569                for (WayPoint p : segment.getWayPoints()) {
    6670                    Node n = new Node(p.getCoor());
    67                     for (Entry<String, Object> entry : p.attr.entrySet()) {
    68                         String key = entry.getKey();
    69                         Object obj = p.get(key);
    70                         if (check && !keys.contains(key) && (obj instanceof String || obj instanceof Number || obj instanceof Date)) {
    71                             keys.add(key);
    72                         }
    73                         if (!none && (obj instanceof String || obj instanceof Number)) {
    74                             // only convert when required
    75                             n.put(GpxConstants.GPX_PREFIX + key, obj.toString());
    76                         } else if (obj instanceof Date && GpxConstants.PT_TIME.equals(key)) {
    77                             // timestamps should always be converted
    78                             Date date = (Date) obj;
    79                             if (!none) { //... but the tag will only be set when required
    80                                 n.put(GpxConstants.GPX_PREFIX + key, DateUtils.fromDate(date));
    81                             }
    82                             n.setTimestamp(date);
    83                         }
     71                    addAttributes(p.getAttributes(), n, keys, check, none);
     72                    if (!none) {
     73                        addExtensions(p.getExtensions(), n, false, keys, check);
    8474                    }
    8575                    ds.addPrimitive(n);
     
    8878                Way w = new Way();
    8979                w.setNodes(nodes);
     80                addAttributes(trk.getAttributes(), w, keys, check, none);
     81                addAttributes(segment.getAttributes(), w, keys, check, none);
     82                if (!none) {
     83                    addExtensions(trk.getExtensions(), w, false, keys, check);
     84                    addExtensions(segment.getExtensions(), w, true, keys, check);
     85                }
    9086                ds.addPrimitive(w);
    9187            }
     
    123119    }
    124120
     121    private static void addAttributes(Map<String, Object> attr, OsmPrimitive p, List<String> keys, boolean check, boolean none) {
     122        for (Entry<String, Object> entry : attr.entrySet()) {
     123            String key = entry.getKey();
     124            Object obj = entry.getValue();
     125            if (check && !keys.contains(key) && (obj instanceof String || obj instanceof Number || obj instanceof Date)) {
     126                keys.add(key);
     127            }
     128            if (!none && (obj instanceof String || obj instanceof Number)) {
     129                // only convert when required
     130                p.put(GpxConstants.GPX_PREFIX + key, obj.toString());
     131            } else if (obj instanceof Date && GpxConstants.PT_TIME.equals(key)) {
     132                // timestamps should always be converted
     133                Date date = (Date) obj;
     134                if (!none) { //... but the tag will only be set when required
     135                    p.put(GpxConstants.GPX_PREFIX + key, DateUtils.fromDate(date));
     136                }
     137                p.setTimestamp(date);
     138            }
     139        }
     140    }
     141
     142    private static void addExtensions(GpxExtensionCollection exts, OsmPrimitive p, boolean seg, List<String> keys, boolean check) {
     143        for (GpxExtension ext : exts) {
     144            String value = ext.getValue();
     145            if (value != null && !value.isEmpty()) {
     146                String extpre = "extension:";
     147                String pre = ext.getPrefix();
     148                if (pre == null || pre.isEmpty()) {
     149                    pre = "other";
     150                }
     151                String segpre = seg ? "segment:" : ""; //needs to be distinguished since both track and segment extensions are applied to the resulting way
     152                String key = ext.getFlatKey();
     153                String fullkey = GpxConstants.GPX_PREFIX + extpre + pre + ":" + segpre + key;
     154                if (GpxConstants.EXTENSION_ABBREVIATIONS.containsKey(fullkey)) {
     155                    fullkey = GpxConstants.EXTENSION_ABBREVIATIONS.get(fullkey);
     156                }
     157                if (check && !keys.contains(fullkey)) {
     158                    keys.add(fullkey);
     159                }
     160                p.put(fullkey, value);
     161            }
     162            addExtensions(ext.getExtensions(), p, seg, keys, check);
     163        }
     164    }
     165
    125166    /**
    126167     * Filters the tags of the given {@link DataSet}
     
    131172     */
    132173    public DataSet filterDataSet(DataSet ds, List<String> listPos) {
    133         Collection<Node> nodes = ds.getNodes();
    134         for (Node n : nodes) {
    135             for (String key : n.keySet()) {
    136                 if (listPos == null || !listPos.contains(key.substring(GpxConstants.GPX_PREFIX.length()))) {
    137                     n.put(key, null);
     174        for (OsmPrimitive p : ds.getPrimitives(p -> p instanceof Node || p instanceof Way)) {
     175            for (String key : p.keySet()) {
     176                String listkey;
     177                if (listPos != null && key.startsWith(GpxConstants.GPX_PREFIX)) {
     178                    listkey = key.substring(GpxConstants.GPX_PREFIX.length());
     179                } else {
     180                    listkey = key;
     181                }
     182                if (listPos == null || !listPos.contains(listkey)) {
     183                   p.put(key, null);
    138184                }
    139185            }
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/CustomizeDrawingAction.java

    r14153 r15496  
    1010import java.util.LinkedList;
    1111import java.util.List;
     12import java.util.stream.Collectors;
    1213
    1314import javax.swing.AbstractAction;
     
    6061    @Override
    6162    public boolean supportLayers(List<Layer> layers) {
    62         for (Layer layer : layers) {
    63             if (!(layer instanceof GpxLayer)) {
    64                 return false;
    65             }
    66         }
    67         return true;
     63        return layers.stream().allMatch(l -> l instanceof GpxLayer);
    6864    }
    6965
     
    8076    @Override
    8177    public void actionPerformed(ActionEvent e) {
    82         boolean hasLocal = false;
    83         boolean hasNonlocal = false;
    84         for (Layer layer : layers) {
    85             if (layer instanceof GpxLayer) {
    86                 if (((GpxLayer) layer).isLocalFile()) {
    87                     hasLocal = true;
    88                 } else {
    89                     hasNonlocal = true;
    90                 }
    91             }
    92         }
    93         GPXSettingsPanel panel = new GPXSettingsPanel(layers.get(0).getName(), hasLocal, hasNonlocal);
     78        GPXSettingsPanel panel = new GPXSettingsPanel(layers.stream().filter(l -> l instanceof GpxLayer).map(l -> (GpxLayer) l).collect(Collectors.toList()));
    9479        JScrollPane scrollpane = GuiHelper.embedInVerticalScrollPane(panel);
    9580        scrollpane.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
     
    10489            return;
    10590        }
    106         for (Layer layer : layers) {
    107             // save preferences for all layers
    108             boolean f = false;
    109             if (layer instanceof GpxLayer) {
    110                 f = ((GpxLayer) layer).isLocalFile();
    111             }
    112             panel.savePreferences(layer.getName(), f);
    113         }
    114         MainApplication.getMap().repaint();
     91        panel.savePreferences();
     92        MainApplication.getMainPanel().repaint();
     93        layers.stream().forEach(Layer::invalidate);
    11594    }
    11695
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/DownloadAlongTrackAction.java

    r15363 r15496  
    99import org.openstreetmap.josm.data.gpx.GpxData;
    1010import org.openstreetmap.josm.data.gpx.GpxTrack;
    11 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     11import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    1212import org.openstreetmap.josm.data.gpx.WayPoint;
    1313import org.openstreetmap.josm.gui.PleaseWaitRunnable;
     
    6161        if (near == NEAR_TRACK || near == NEAR_BOTH) {
    6262            for (GpxTrack trk : data.tracks) {
    63                 for (GpxTrackSegment segment : trk.getSegments()) {
     63                for (IGpxTrackSegment segment : trk.getSegments()) {
    6464                    boolean first = true;
    6565                    for (WayPoint p : segment.getWayPoints()) {
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/DownloadWmsAlongTrackAction.java

    r14427 r15496  
    1717import org.openstreetmap.josm.data.gpx.GpxData;
    1818import org.openstreetmap.josm.data.gpx.GpxTrack;
    19 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     19import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    2020import org.openstreetmap.josm.data.gpx.WayPoint;
    2121import org.openstreetmap.josm.gui.MainApplication;
     
    9393        List<LatLon> points = new ArrayList<>();
    9494        for (GpxTrack trk : data.tracks) {
    95             for (GpxTrackSegment segment : trk.getSegments()) {
     95            for (IGpxTrackSegment segment : trk.getSegments()) {
    9696                for (WayPoint p : segment.getWayPoints()) {
    9797                    points.add(p.getCoor());
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/GpxDrawHelper.java

    r15292 r15496  
    2828import java.util.LinkedList;
    2929import java.util.List;
     30import java.util.Objects;
    3031import java.util.Random;
    3132
     
    3334
    3435import org.openstreetmap.josm.data.Bounds;
    35 import org.openstreetmap.josm.data.PreferencesUtils;
    3636import org.openstreetmap.josm.data.SystemOfMeasurement;
    3737import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener;
     
    5252import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationEvent;
    5353import org.openstreetmap.josm.gui.layer.MapViewPaintable.PaintableInvalidationListener;
     54import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
    5455import org.openstreetmap.josm.io.CachedFile;
    5556import org.openstreetmap.josm.spi.preferences.Config;
     
    6667
    6768    /**
    68      * The color that is used for drawing GPX points.
    69      * @since 10824
    70      */
    71     public static final NamedColorProperty DEFAULT_COLOR = new NamedColorProperty(marktr("gps point"), Color.magenta);
     69     * The default color property that is used for drawing GPX points.
     70     * @since 15496
     71     */
     72    public static final NamedColorProperty DEFAULT_COLOR_PROPERTY = new NamedColorProperty(marktr("gps point"), Color.magenta);
    7273
    7374    private final GpxData data;
     
    7980    private boolean alphaLines;
    8081    // draw direction arrows on the lines
    81     private boolean direction;
     82    private boolean arrows;
    8283    /** width of line for paint **/
    8384    private int lineWidth;
     
    9192    private boolean hdopCircle;
    9293    /** paint direction arrow with alternate math. may be faster **/
    93     private boolean alternateDirection;
     94    private boolean arrowsFast;
    9495    /** don't draw arrows nearer to each other than this **/
    95     private int delta;
     96    private int arrowsDelta;
    9697    private double minTrackDurationForTimeColoring;
    9798
     
    107108    private boolean computeCacheColorDynamic;
    108109    private ColorMode computeCacheColored;
    109     private int computeCacheColorTracksTune;
     110    private int computeCacheVelocityTune;
    110111    private int computeCacheHeatMapDrawColorTableIdx;
    111112    private boolean computeCacheHeatMapDrawPointMode;
     
    113114    private int computeCacheHeatMapDrawLowerLimit;
    114115
     116    private Color colorCache;
     117    private Color colorCacheTransparent;
     118
    115119    //// Color-related fields
    116120    /** Mode of the line coloring **/
    117121    private ColorMode colored;
    118122    /** max speed for coloring - allows to tweak line coloring for different speed levels. **/
    119     private int colorTracksTune;
     123    private int velocityTune;
    120124    private boolean colorModeDynamic;
    121125    private Color neutralColor;
     
    146150    /** heat map parameters **/
    147151
    148     // enabled or not (override by settings)
    149     private boolean heatMapEnabled;
    150152    // draw small extra line
    151153    private boolean heatMapDrawExtraLine;
     
    269271    }
    270272
    271     private static String specName(String layerName) {
    272         return "layer " + layerName;
    273     }
    274 
    275     /**
    276      * Get the default color for gps tracks for specified layer
    277      * @param layerName name of the GpxLayer
    278      * @param ignoreCustom do not use preferences
    279      * @return the color or null if the color is not constant
    280      */
    281     public Color getColor(String layerName, boolean ignoreCustom) {
    282         if (ignoreCustom || getColorMode(layerName) == ColorMode.NONE) {
    283             return DEFAULT_COLOR.getChildColor(
    284                     NamedColorProperty.COLOR_CATEGORY_LAYER,
    285                     layerName,
    286                     DEFAULT_COLOR.getName()).get();
    287         } else {
    288             return null;
    289         }
    290     }
    291 
    292273    /**
    293274     * Read coloring mode for specified layer from preferences
    294      * @param layerName name of the GpxLayer
    295275     * @return coloring mode
    296276     */
    297     public ColorMode getColorMode(String layerName) {
     277    public ColorMode getColorMode() {
    298278        try {
    299             int i = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.colors", specName(layerName), 0);
     279            int i = optInt("colormode");
     280            if (i == -1) i = 0; //global
    300281            return ColorMode.fromIndex(i);
    301282        } catch (IndexOutOfBoundsException e) {
     
    305286    }
    306287
    307     /** Reads generic color from preferences (usually gray)
    308      * @return the color
     288    private String opt(String key) {
     289        return GPXSettingsPanel.getLayerPref(layer, key);
     290    }
     291
     292    private boolean optBool(String key) {
     293        return Boolean.parseBoolean(opt(key));
     294    }
     295
     296    private int optInt(String key) {
     297        return GPXSettingsPanel.getLayerPrefInt(layer, key);
     298    }
     299
     300    /**
     301     * Read all drawing-related settings from preferences
    309302     **/
    310     public static Color getGenericColor() {
    311         return DEFAULT_COLOR.get();
    312     }
    313 
    314     /**
    315      * Read all drawing-related settings from preferences
    316      * @param layerName layer name used to access its specific preferences
    317      **/
    318     public void readPreferences(String layerName) {
    319         String spec = specName(layerName);
    320         forceLines = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.lines.force", spec, false);
    321         direction = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.direction", spec, false);
    322         lineWidth = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.linewidth", spec, 0);
    323         alphaLines = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.lines.alpha-blend", spec, false);
    324 
     303    public void readPreferences() {
     304        forceLines = optBool("lines.force");
     305        arrows = optBool("lines.arrows");
     306        arrowsFast = optBool("lines.arrows.fast");
     307        arrowsDelta = optInt("lines.arrows.min-distance");
     308        lineWidth = optInt("lines.width");
     309        alphaLines = optBool("lines.alpha-blend");
     310
     311        int l = optInt("lines");
    325312        if (!data.fromServer) {
    326             maxLineLength = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.max-line-length.local", spec, -1);
    327             lines = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.lines.local", spec, true);
     313            maxLineLength = optInt("lines.max-length.local");
     314            lines = l != 0; //draw for -1 (default), 1 (local) and 2 (all)
    328315        } else {
    329             maxLineLength = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.max-line-length", spec, 200);
    330             lines = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.lines", spec, true);
    331         }
    332         large = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.large", spec, false);
    333         largesize = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.large.size", spec, 3);
    334         hdopCircle = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.hdopcircle", spec, false);
    335         colored = getColorMode(layerName);
    336         alternateDirection = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.alternatedirection", spec, false);
    337         delta = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.min-arrow-distance", spec, 40);
    338         colorTracksTune = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.colorTracksTune", spec, 45);
    339         colorModeDynamic = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.colors.dynamic", spec, false);
     316            maxLineLength = optInt("lines.max-length");
     317            lines = l == 2; //draw only for 2 (all)
     318        }
     319        large = optBool("points.large");
     320        largesize = optInt("points.large.size");
     321        hdopCircle = optBool("points.hdopcircle");
     322        colored = getColorMode();
     323        velocityTune = optInt("colormode.velocity.tune");
     324        colorModeDynamic = optBool("colormode.dynamic-range");
    340325        /* good HDOP's are between 1 and 3, very bad HDOP's go into 3 digit values */
    341326        hdoprange = Config.getPref().getInt("hdop.range", 7);
    342         minTrackDurationForTimeColoring = Config.getPref().getInt("draw.rawgps.date-coloring-min-dt", 60);
    343         largePointAlpha = Config.getPref().getInt("draw.rawgps.large.alpha", -1) & 0xFF;
     327        minTrackDurationForTimeColoring = optInt("colormode.time.min-distance");
     328        largePointAlpha = optInt("points.large.alpha") & 0xFF;
    344329
    345330        // get heatmap parameters
    346         heatMapEnabled = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.heatmap.enabled", spec, false);
    347         heatMapDrawExtraLine = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.heatmap.line-extra", spec, false);
    348         heatMapDrawColorTableIdx = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.heatmap.colormap", spec, 0);
    349         heatMapDrawPointMode = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.heatmap.use-points", spec, false);
    350         heatMapDrawGain = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.heatmap.gain", spec, 0);
    351         heatMapDrawLowerLimit = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.heatmap.lower-limit", spec, 0);
     331        heatMapDrawExtraLine = optBool("colormode.heatmap.line-extra");
     332        heatMapDrawColorTableIdx = optInt("colormode.heatmap.colormap");
     333        heatMapDrawPointMode = optBool("colormode.heatmap.use-points");
     334        heatMapDrawGain = optInt("colormode.heatmap.gain");
     335        heatMapDrawLowerLimit = optInt("colormode.heatmap.lower-limit");
    352336
    353337        // shrink to range
    354338        heatMapDrawGain = Utils.clamp(heatMapDrawGain, -10, 10);
    355 
    356         neutralColor = getColor(layerName, true);
     339        neutralColor = DEFAULT_COLOR_PROPERTY.get();
    357340        velocityScale.setNoDataColor(neutralColor);
    358341        dateScale.setNoDataColor(neutralColor);
     
    369352        List<WayPoint> visibleSegments = listVisibleSegments(clipBounds);
    370353        if (!visibleSegments.isEmpty()) {
    371             readPreferences(layer.getName());
     354            readPreferences();
    372355            drawAll(graphics.getDefaultGraphics(), graphics.getMapView(), visibleSegments, clipBounds);
    373356            if (graphics.getMapView().getLayerManager().getActiveLayer() == layer) {
     
    431414     * @since 14748 : new parameter clipBounds
    432415     */
    433 
    434416    public void drawAll(Graphics2D g, MapView mv, List<WayPoint> visibleSegments, Bounds clipBounds) {
    435417
     
    463445
    464446        // global enabled or select via color
    465         boolean useHeatMap = heatMapEnabled || ColorMode.HEATMAP == colored;
     447        boolean useHeatMap = ColorMode.HEATMAP == colored;
    466448
    467449        // default global alpha level
     
    571553            oldWp = null;
    572554        } else { // color mode not dynamic
    573             velocityScale.setRange(0, colorTracksTune);
     555            velocityScale.setRange(0, velocityTune);
    574556            hdopScale.setRange(0, hdoprange);
    575557            qualityScale.setRange(1, rtkLibQualityColors.length);
     
    595577            for (WayPoint trkPnt : segment) {
    596578                LatLon c = trkPnt.getCoor();
    597                 trkPnt.customColoring = neutralColor;
     579                trkPnt.customColoring = segment.getColor();
    598580                if (Double.isNaN(c.lat()) || Double.isNaN(c.lon())) {
    599581                    continue;
     
    643625                } else { // make sure we reset outdated data
    644626                    trkPnt.drawLine = false;
    645                     color = neutralColor;
     627                    color = segment.getColor();
    646628                }
    647629                if (color != null) {
     
    701683         ********** STEP 3b - DRAW NICE ARROWS **************************
    702684         ****************************************************************/
    703         if (lines && direction && !alternateDirection) {
     685        if (lines && arrows && !arrowsFast) {
    704686            Point old = null;
    705687            Point oldA = null; // last arrow painted
     
    713695                    // skip points that are on the same screenposition
    714696                    if (old != null
    715                             && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
    716                             || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
     697                            && (oldA == null || screen.x < oldA.x - arrowsDelta || screen.x > oldA.x + arrowsDelta
     698                            || screen.y < oldA.y - arrowsDelta || screen.y > oldA.y + arrowsDelta)) {
    717699                        g.setColor(trkPnt.customColoring);
    718700                        double t = Math.atan2((double) screen.y - old.y, (double) screen.x - old.x) + Math.PI;
     
    731713         ********** STEP 3c - DRAW FAST ARROWS **************************
    732714         ****************************************************************/
    733         if (lines && direction && alternateDirection) {
     715        if (lines && arrows && arrowsFast) {
    734716            Point old = null;
    735717            Point oldA = null; // last arrow painted
     
    743725                    // skip points that are on the same screenposition
    744726                    if (old != null
    745                             && (oldA == null || screen.x < oldA.x - delta || screen.x > oldA.x + delta
    746                             || screen.y < oldA.y - delta || screen.y > oldA.y + delta)) {
     727                            && (oldA == null || screen.x < oldA.x - arrowsDelta || screen.x > oldA.x + arrowsDelta
     728                            || screen.y < oldA.y - arrowsDelta || screen.y > oldA.y + arrowsDelta)) {
    747729                        g.setColor(trkPnt.customColoring);
    748730                        g.drawLine(screen.x, screen.y, screen.x + dir[trkPnt.dir][0], screen.y
     
    795777                    // color the large GPS points like the gps lines
    796778                    if (trkPnt.customColoring != null) {
    797                         Color customColoringTransparent = largePointAlpha < 0 ? trkPnt.customColoring :
    798                             new Color((trkPnt.customColoring.getRGB() & 0x00ffffff) | (largePointAlpha << 24), true);
    799 
    800                         g.setColor(customColoringTransparent);
     779                        if (trkPnt.customColoring.equals(colorCache) && colorCacheTransparent != null) {
     780                            g.setColor(colorCacheTransparent);
     781                        } else {
     782                            Color customColoringTransparent = largePointAlpha < 0 ? trkPnt.customColoring :
     783                                new Color((trkPnt.customColoring.getRGB() & 0x00ffffff) | (largePointAlpha << 24), true);
     784
     785                            g.setColor(customColoringTransparent);
     786                            colorCache = trkPnt.customColoring;
     787                            colorCacheTransparent = customColoringTransparent;
     788                        }
    801789                    }
    802790                    g.fillRect(screen.x-halfSize, screen.y-halfSize, largesize, largesize);
     
    816804                }
    817805                if (!trkPnt.drawLine) {
     806                    g.setColor(trkPnt.customColoring);
    818807                    Point screen = mv.getPoint(trkPnt);
    819808                    g.drawRect(screen.x, screen.y, 0, 0);
     
    14811470        if ((computeCacheMaxLineLengthUsed != maxLineLength)
    14821471                || (computeCacheColored != colored)
    1483                 || (computeCacheColorTracksTune != colorTracksTune)
     1472                || (computeCacheVelocityTune != velocityTune)
    14841473                || (computeCacheColorDynamic != colorModeDynamic)
    14851474                || (computeCacheHeatMapDrawColorTableIdx != heatMapDrawColorTableIdx)
    1486                 || (!neutralColor.equals(computeCacheColorUsed)
     1475                || !Objects.equals(neutralColor, computeCacheColorUsed)
    14871476                || (computeCacheHeatMapDrawPointMode != heatMapDrawPointMode)
    1488                 || (computeCacheHeatMapDrawGain != heatMapDrawGain))
     1477                || (computeCacheHeatMapDrawGain != heatMapDrawGain)
    14891478                || (computeCacheHeatMapDrawLowerLimit != heatMapDrawLowerLimit)
    14901479        ) {
     
    14941483            computeCacheColorUsed = neutralColor;
    14951484            computeCacheColored = colored;
    1496             computeCacheColorTracksTune = colorTracksTune;
     1485            computeCacheVelocityTune = velocityTune;
    14971486            computeCacheColorDynamic = colorModeDynamic;
    14981487            computeCacheHeatMapDrawColorTableIdx = heatMapDrawColorTableIdx;
  • trunk/src/org/openstreetmap/josm/gui/layer/gpx/ImportAudioAction.java

    r14456 r15496  
    2222import org.openstreetmap.josm.data.gpx.GpxData;
    2323import org.openstreetmap.josm.data.gpx.GpxTrack;
    24 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     24import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    2525import org.openstreetmap.josm.data.gpx.WayPoint;
    2626import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     
    148148        if (hasTracks) {
    149149            for (GpxTrack track : layer.data.tracks) {
    150                 for (GpxTrackSegment seg : track.getSegments()) {
     150                for (IGpxTrackSegment seg : track.getSegments()) {
    151151                    for (WayPoint w : seg.getWayPoints()) {
    152152                        firstTime = w.getTime();
     
    207207                && !layer.data.tracks.isEmpty()) {
    208208            for (GpxTrack track : layer.data.tracks) {
    209                 for (GpxTrackSegment seg : track.getSegments()) {
     209                for (IGpxTrackSegment seg : track.getSegments()) {
    210210                    for (WayPoint w : seg.getWayPoints()) {
    211211                        if (w.attr.containsKey(GpxConstants.GPX_NAME) || w.attr.containsKey(GpxConstants.GPX_DESC)) {
     
    228228
    229229            for (GpxTrack track : layer.data.tracks) {
    230                 for (GpxTrackSegment seg : track.getSegments()) {
     230                for (IGpxTrackSegment seg : track.getSegments()) {
    231231                    for (WayPoint w : seg.getWayPoints()) {
    232232                        if (startTime < w.getTime()) {
     
    264264            boolean gotOne = false;
    265265            for (GpxTrack track : layer.data.tracks) {
    266                 for (GpxTrackSegment seg : track.getSegments()) {
     266                for (IGpxTrackSegment seg : track.getSegments()) {
    267267                    for (WayPoint w : seg.getWayPoints()) {
    268268                        WayPoint wStart = new WayPoint(w.getCoor());
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/AudioMarker.java

    r13419 r15496  
    9494
    9595    @Override
    96     protected TemplateEntryProperty getTextTemplate() {
    97         return TemplateEntryProperty.forAudioMarker(parentLayer.getName());
     96    protected String getTextTemplateKey() {
     97        return "markers.audio.pattern";
    9898    }
    9999
     
    104104        link.type = "audio";
    105105        wpt.put(GpxConstants.META_LINKS, Collections.singleton(link));
    106         wpt.addExtension("offset", Double.toString(offset));
    107         wpt.addExtension("sync-offset", Double.toString(syncOffset));
     106        wpt.getExtensions().add("josm", "offset", Double.toString(offset));
     107        wpt.getExtensions().add("josm", "sync-offset", Double.toString(syncOffset));
    108108        return wpt;
    109109    }
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/DefaultMarkerProducers.java

    r12620 r15496  
    1010import java.util.Optional;
    1111
    12 import org.openstreetmap.josm.data.gpx.Extensions;
    1312import org.openstreetmap.josm.data.gpx.GpxConstants;
     13import org.openstreetmap.josm.data.gpx.GpxExtension;
    1414import org.openstreetmap.josm.data.gpx.GpxLink;
    1515import org.openstreetmap.josm.data.gpx.WayPoint;
     
    4646        } else if (Utils.hasExtension(urlStr, "wav", "mp3", "aac", "aif", "aiff")) {
    4747            final AudioMarker audioMarker = new AudioMarker(wpt.getCoor(), wpt, url, parentLayer, time, offset);
    48             Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
    49             if (exts != null && exts.containsKey("offset")) {
     48            GpxExtension offsetExt = wpt.getExtensions().get("josm", "sync-offset");
     49            if (offsetExt != null && offsetExt.getValue() != null) {
    5050                try {
    51                     audioMarker.syncOffset = Double.parseDouble(exts.get("sync-offset"));
     51                    audioMarker.syncOffset = Double.parseDouble(offsetExt.getValue());
    5252                } catch (NumberFormatException nfe) {
    5353                    Logging.warn(nfe);
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java

    r14823 r15496  
    1919import javax.swing.ImageIcon;
    2020
     21import org.openstreetmap.josm.data.Preferences;
    2122import org.openstreetmap.josm.data.coor.CachedLatLon;
    2223import org.openstreetmap.josm.data.coor.EastNorth;
     
    2627import org.openstreetmap.josm.data.gpx.WayPoint;
    2728import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
    28 import org.openstreetmap.josm.data.preferences.CachedProperty;
    2929import org.openstreetmap.josm.gui.MapView;
    30 import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
     30import org.openstreetmap.josm.gui.layer.GpxLayer;
     31import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
    3132import org.openstreetmap.josm.tools.ImageProvider;
    3233import org.openstreetmap.josm.tools.Logging;
     
    7374public class Marker implements TemplateEngineDataProvider, ILatLon {
    7475
    75     public static final class TemplateEntryProperty extends CachedProperty<TemplateEntry> {
    76         // This class is a bit complicated because it supports both global and per layer settings. I've added per layer settings because
    77         // GPXSettingsPanel had possibility to set waypoint label but then I've realized that markers use different layer then gpx data
    78         // so per layer settings is useless. Anyway it's possible to specify marker layer pattern in Einstein preferences and maybe somebody
    79         // will make gui for it so I'm keeping it here
    80 
    81         private static final Map<String, TemplateEntryProperty> CACHE = new HashMap<>();
    82 
    83         public static TemplateEntryProperty forMarker(String layerName) {
    84             String key = "draw.rawgps.layer.wpt.pattern";
    85             if (layerName != null) {
    86                 key += '.' + layerName;
    87             }
    88             TemplateEntryProperty result = CACHE.get(key);
    89             if (result == null) {
    90                 String defaultValue = layerName == null ? LABEL_PATTERN_AUTO : "";
    91                 TemplateEntryProperty parent = layerName == null ? null : forMarker(null);
    92                 result = new TemplateEntryProperty(key, defaultValue, parent);
    93                 CACHE.put(key, result);
    94             }
    95             return result;
    96         }
    97 
    98         public static TemplateEntryProperty forAudioMarker(String layerName) {
    99             String key = "draw.rawgps.layer.audiowpt.pattern";
    100             if (layerName != null) {
    101                 key += '.' + layerName;
    102             }
    103             TemplateEntryProperty result = CACHE.get(key);
    104             if (result == null) {
    105                 String defaultValue = layerName == null ? "?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }" : "";
    106                 TemplateEntryProperty parent = layerName == null ? null : forAudioMarker(null);
    107                 result = new TemplateEntryProperty(key, defaultValue, parent);
    108                 CACHE.put(key, result);
    109             }
    110             return result;
    111         }
    112 
    113         private final TemplateEntryProperty parent;
    114 
    115         private TemplateEntryProperty(String key, String defaultValue, TemplateEntryProperty parent) {
    116             super(key, defaultValue);
    117             this.parent = parent;
    118             updateValue(); // Needs to be called because parent wasn't know in super constructor
    119         }
    120 
    121         @Override
    122         protected TemplateEntry fromString(String s) {
    123             try {
    124                 return new TemplateParser(s).parse();
    125             } catch (ParseError e) {
    126                 Logging.debug(e);
    127                 Logging.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead",
    128                         s, getKey(), super.getDefaultValueAsString());
    129                 return getDefaultValue();
    130             }
    131         }
    132 
    133         @Override
    134         public String getDefaultValueAsString() {
    135             if (parent == null)
    136                 return super.getDefaultValueAsString();
    137             else
    138                 return parent.getAsString();
    139         }
    140 
    141         @Override
    142         public void preferenceChanged(PreferenceChangeEvent e) {
    143             if (e.getKey().equals(key) || (parent != null && e.getKey().equals(parent.getKey()))) {
    144                 updateValue();
    145             }
    146         }
    147     }
    148 
    14976    /**
    15077     * Plugins can add their Marker creation stuff at the bottom or top of this list
     
    217144
    218145    private String cachedText;
    219     private int textVersion = -1;
     146    private static Map<GpxLayer, String> cachedTemplates = new HashMap<>();
     147    private String cachedDefaultTemplate;
     148
    220149    private CachedLatLon coor;
    221150
     
    245174        this.dataProvider = dataProvider;
    246175        this.text = text;
     176
     177        Preferences.main().addKeyPreferenceChangeListener("draw.rawgps." + getTextTemplateKey(), l -> updateText());
    247178    }
    248179
     
    258189        wpt.setTimeInMillis((long) (time * 1000));
    259190        if (text != null) {
    260             wpt.addExtension("text", text);
     191            wpt.getExtensions().add("josm", "text", text);
    261192        } else if (dataProvider != null) {
    262193            for (String key : dataProvider.getTemplateKeys()) {
     
    373304    }
    374305
    375     protected TemplateEntryProperty getTextTemplate() {
    376         return TemplateEntryProperty.forMarker(parentLayer.getName());
     306    protected String getTextTemplateKey() {
     307        return "markers.pattern";
     308    }
     309
     310    private String getTextTemplate() {
     311        String tmpl;
     312        if (cachedTemplates.containsKey(parentLayer.fromLayer)) {
     313            tmpl = cachedTemplates.get(parentLayer.fromLayer);
     314        } else {
     315            tmpl = GPXSettingsPanel.getLayerPref(parentLayer.fromLayer, getTextTemplateKey());
     316            cachedTemplates.put(parentLayer.fromLayer, tmpl);
     317        }
     318        return tmpl;
     319    }
     320
     321    private String getDefaultTextTemplate() {
     322        if (cachedDefaultTemplate == null) {
     323            cachedDefaultTemplate = GPXSettingsPanel.getLayerPref(null,  getTextTemplateKey());
     324        }
     325        return cachedDefaultTemplate;
    377326    }
    378327
     
    382331     */
    383332    public String getText() {
    384         if (text != null)
     333        if (text != null) {
    385334            return text;
    386         else {
    387             TemplateEntryProperty property = getTextTemplate();
    388             if (property.getUpdateCount() != textVersion) {
    389                 TemplateEntry templateEntry = property.get();
    390                 StringBuilder sb = new StringBuilder();
    391                 templateEntry.appendText(sb, this);
    392 
    393                 cachedText = sb.toString();
    394                 textVersion = property.getUpdateCount();
     335        } else if (cachedText == null) {
     336            TemplateEntry template;
     337            String templateString = getTextTemplate();
     338            try {
     339                template = new TemplateParser(templateString).parse();
     340            } catch (ParseError e) {
     341                Logging.debug(e);
     342                String def = getDefaultTextTemplate();
     343                Logging.warn("Unable to parse template engine pattern ''{0}'' for property {1}. Using default (''{2}'') instead",
     344                        templateString, getTextTemplateKey(), def);
     345                try {
     346                    template = new TemplateParser(def).parse();
     347                } catch (ParseError e1) {
     348                    Logging.error(e1);
     349                    cachedText = "";
     350                    return "";
     351                }
    395352            }
    396             return cachedText;
    397         }
     353            StringBuilder sb = new StringBuilder();
     354            template.appendText(sb, this);
     355            cachedText = sb.toString();
     356
     357        }
     358        return cachedText;
     359    }
     360
     361    /**
     362     * Called when the template changes
     363     */
     364    public void updateText() {
     365        cachedText = null;
     366        cachedDefaultTemplate = null;
     367        cachedTemplates = new HashMap<>();
    398368    }
    399369
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/MarkerLayer.java

    r14456 r15496  
    2121import java.util.Comparator;
    2222import java.util.List;
     23import java.util.Optional;
    2324
    2425import javax.swing.AbstractAction;
     
    3132import org.openstreetmap.josm.data.Bounds;
    3233import org.openstreetmap.josm.data.coor.LatLon;
    33 import org.openstreetmap.josm.data.gpx.Extensions;
    3434import org.openstreetmap.josm.data.gpx.GpxConstants;
    3535import org.openstreetmap.josm.data.gpx.GpxData;
     36import org.openstreetmap.josm.data.gpx.GpxExtension;
    3637import org.openstreetmap.josm.data.gpx.GpxLink;
    3738import org.openstreetmap.josm.data.gpx.WayPoint;
     
    4950import org.openstreetmap.josm.gui.layer.Layer;
    5051import org.openstreetmap.josm.gui.layer.gpx.ConvertFromMarkerLayerAction;
     52import org.openstreetmap.josm.gui.preferences.display.GPXSettingsPanel;
    5153import org.openstreetmap.josm.io.audio.AudioPlayer;
    5254import org.openstreetmap.josm.spi.preferences.Config;
     
    7678    private Marker currentMarker;
    7779    public AudioMarker syncAudioMarker;
    78 
    79     private static final Color DEFAULT_COLOR = Color.magenta;
    80     private static final NamedColorProperty COLOR_PROPERTY = new NamedColorProperty(marktr("gps marker"), DEFAULT_COLOR);
     80    private Color color, realcolor;
     81
     82    /**
     83     * The default color that is used for drawing markers.
     84     */
     85    public static final NamedColorProperty DEFAULT_COLOR_PROPERTY = new NamedColorProperty(marktr("gps marker"), Color.magenta);
    8186
    8287    /**
     
    95100        String lastLinkedFile = "";
    96101
     102        Color c = null;
     103        String cs = GPXSettingsPanel.tryGetLayerPrefLocal(indata, "markers.color");
     104        if (cs != null) {
     105            try {
     106                c = Color.decode(cs);
     107            } catch (NumberFormatException ex) {
     108                Logging.warn("Could not read marker color: " + cs);
     109            }
     110        }
     111        setPrivateColors(c);
     112
    97113        for (WayPoint wpt : indata.waypoints) {
    98114            /* calculate time differences in waypoints */
     
    124140            // that group. This way the user can jump to the corresponding
    125141            // playback positions in a long audio track.
    126             Extensions exts = (Extensions) wpt.get(GpxConstants.META_EXTENSIONS);
    127             if (exts != null && exts.containsKey("offset")) {
     142            GpxExtension offsetExt = wpt.getExtensions().get("josm", "offset");
     143            if (offsetExt != null && offsetExt.getValue() != null) {
    128144                try {
    129                     offset = Double.valueOf(exts.get("offset"));
     145                    offset = Double.valueOf(offsetExt.getValue());
    130146                } catch (NumberFormatException nfe) {
    131147                    Logging.warn(nfe);
     
    174190
    175191    @Override
    176     protected NamedColorProperty getBaseColorProperty() {
    177         return COLOR_PROPERTY;
    178     }
    179 
    180     /* for preferences */
    181     public static Color getGenericColor() {
    182         return COLOR_PROPERTY.get();
    183     }
    184 
    185     @Override
    186192    public void paint(Graphics2D g, MapView mv, Bounds box) {
    187193        boolean showTextOrIcon = isTextOrIconShown();
    188         g.setColor(getColorProperty().get());
    189 
     194        g.setColor(realcolor);
    190195        if (mousePressed) {
    191196            boolean mousePressedTmp = mousePressed;
     
    451456     */
    452457    private boolean isTextOrIconShown() {
    453         String current = Config.getPref().get("marker.show "+getName(), "show");
    454         return "show".equalsIgnoreCase(current);
     458        return Boolean.parseBoolean(GPXSettingsPanel.getLayerPref(fromLayer, "markers.show-text"));
     459    }
     460
     461    @Override
     462    public boolean hasColor() {
     463        return true;
     464    }
     465
     466    @Override
     467    public Color getColor() {
     468        return color;
     469    }
     470
     471    @Override
     472    public void setColor(Color color) {
     473        setPrivateColors(color);
     474        if (fromLayer != null) {
     475            String cs = null;
     476            if (color != null) {
     477                cs = String.format("#%02X%02X%02X", color.getRed(), color.getGreen(), color.getBlue());
     478            }
     479            GPXSettingsPanel.putLayerPrefLocal(fromLayer, "markers.color", cs);
     480        }
     481        invalidate();
     482    }
     483
     484    private void setPrivateColors(Color color) {
     485        this.color = color;
     486        this.realcolor = Optional.ofNullable(color).orElse(DEFAULT_COLOR_PROPERTY.get());
    455487    }
    456488
     
    504536        @Override
    505537        public void actionPerformed(ActionEvent e) {
    506             Config.getPref().put("marker.show "+layer.getName(), layer.isTextOrIconShown() ? "hide" : "show");
     538            GPXSettingsPanel.putLayerPrefLocal(layer.fromLayer, "markers.show-text", Boolean.toString(!layer.isTextOrIconShown()));
    507539            layer.invalidate();
    508540        }
  • trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/PlayHeadMarker.java

    r14456 r15496  
    1919import org.openstreetmap.josm.data.coor.LatLon;
    2020import org.openstreetmap.josm.data.gpx.GpxTrack;
    21 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     21import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    2222import org.openstreetmap.josm.data.gpx.WayPoint;
    2323import org.openstreetmap.josm.data.projection.ProjectionRegistry;
     
    328328
    329329        for (GpxTrack track : trackLayer.data.getTracks()) {
    330             for (GpxTrackSegment trackseg : track.getSegments()) {
     330            for (IGpxTrackSegment trackseg : track.getSegments()) {
    331331                for (WayPoint w: trackseg.getWayPoints()) {
    332332                    if (audioTime < w.getTime()) {
  • trunk/src/org/openstreetmap/josm/gui/preferences/display/ColorPreference.java

    r15121 r15496  
    101101        public String getDisplay() {
    102102            switch (info.getCategory()) {
    103                 case NamedColorProperty.COLOR_CATEGORY_LAYER:
    104                     String v = null;
    105                     if (info.getSource() != null) {
    106                         v = info.getSource();
    107                     }
    108                     if (!info.getName().isEmpty()) {
    109                         if (v == null) {
    110                             v = tr(I18n.escape(info.getName()));
    111                         } else {
    112                             v += " - " + tr(I18n.escape(info.getName()));
    113                         }
    114                     }
    115                     return tr("Layer: {0}", v);
    116103                case NamedColorProperty.COLOR_CATEGORY_MAPPAINT:
    117104                    if (info.getSource() != null)
     
    252239            case NamedColorProperty.COLOR_CATEGORY_GENERAL: return 1;
    253240            case NamedColorProperty.COLOR_CATEGORY_MAPPAINT: return 2;
    254             case NamedColorProperty.COLOR_CATEGORY_LAYER: return 3;
    255             default: return 4;
     241            default: return 3;
    256242        }
    257243    }
     
    380366    }
    381367
     368    @SuppressWarnings("PMD.UnusedFormalParameter")
    382369    private static boolean isRemoveColor(ColorEntry ce) {
    383         return NamedColorProperty.COLOR_CATEGORY_LAYER.equals(ce.info.getCategory());
     370        return false;
     371        //COLOR_CATEGORY_LAYER is no longer supported and was the only one that could be removed.
     372        //Maybe this is useful for other categories in the future.
     373        //return NamedColorProperty.COLOR_CATEGORY_LAYER.equals(ce.info.getCategory());
    384374    }
    385375
     
    391381        ConflictColors.getColors();
    392382        Severity.getColors();
    393         MarkerLayer.getGenericColor();
    394         GpxDrawHelper.getGenericColor();
     383        MarkerLayer.DEFAULT_COLOR_PROPERTY.get();
     384        GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get();
    395385        OsmDataLayer.getOutsideColor();
    396386        MapScaler.getColor();
  • trunk/src/org/openstreetmap/josm/gui/preferences/display/GPXSettingsPanel.java

    r15247 r15496  
    55import static org.openstreetmap.josm.tools.I18n.trc;
    66
    7 import java.awt.Color;
    87import java.awt.Component;
    98import java.awt.Dimension;
    109import java.awt.GridBagLayout;
    1110import java.awt.event.ActionListener;
     11import java.util.Collections;
    1212import java.util.Enumeration;
     13import java.util.HashMap;
     14import java.util.List;
     15import java.util.Map;
     16import java.util.Optional;
    1317
    1418import javax.swing.AbstractButton;
     
    2327import javax.swing.JSlider;
    2428
     29import org.apache.commons.jcs.access.exception.InvalidArgumentException;
    2530import org.openstreetmap.josm.actions.ExpertToggleAction;
    26 import org.openstreetmap.josm.data.PreferencesUtils;
    27 import org.openstreetmap.josm.data.preferences.NamedColorProperty;
     31import org.openstreetmap.josm.data.gpx.GpxData;
    2832import org.openstreetmap.josm.gui.MainApplication;
     33import org.openstreetmap.josm.gui.layer.GpxLayer;
    2934import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper;
    3035import org.openstreetmap.josm.gui.layer.markerlayer.Marker;
    31 import org.openstreetmap.josm.gui.layer.markerlayer.Marker.TemplateEntryProperty;
    3236import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
    3337import org.openstreetmap.josm.gui.widgets.JosmComboBox;
     
    6872    private final JRadioButton colorTypeTime = new JRadioButton(tr("Track date"));
    6973    private final JRadioButton colorTypeHeatMap = new JRadioButton(tr("Heat Map (dark = few, bright = many)"));
    70     private final JRadioButton colorTypeNone = new JRadioButton(tr("Single Color (can be customized for named layers)"));
     74    private final JRadioButton colorTypeNone = new JRadioButton(tr("Single Color (can be customized in the layer manager)"));
    7175    private final JRadioButton colorTypeGlobal = new JRadioButton(tr("Use global settings"));
    7276    private final JosmComboBox<String> colorTypeVelocityTune = new JosmComboBox<>(new String[] {tr("Car"), tr("Bicycle"), tr("Foot")});
     
    9498    private final JCheckBox drawLineWithAlpha = new JCheckBox(tr("Draw with Opacity (alpha blending) "));
    9599
    96     private String layerName;
    97     private final boolean local; // flag to display LocalOnly checkbox
    98     private final boolean nonlocal; // flag to display AllLines checkbox
    99 
    100     /**
    101      * Constructs a new {@code GPXSettingsPanel} for a given layer name.
    102      * @param layerName The GPX layer name
    103      * @param local flag to display LocalOnly checkbox
    104      * @param nonlocal flag to display AllLines checkbox
    105      */
    106     public GPXSettingsPanel(String layerName, boolean local, boolean nonlocal) {
     100    private final List<GpxLayer> layers;
     101    private final GpxLayer firstLayer;
     102    private final boolean global; // global settings vs. layer specific settings
     103    private final boolean hasLocalFile; // flag to display LocalOnly checkbooks
     104    private final boolean hasNonLocalFile; // flag to display AllLines checkbox
     105
     106    private final static Map<String, Object> DEFAULT_PREFS = getDefaultPrefs();
     107
     108    private static Map<String, Object> getDefaultPrefs() {
     109        HashMap<String, Object> m = new HashMap<>();
     110        m.put("colormode", -1);
     111        m.put("colormode.dynamic-range", false);
     112        m.put("colormode.heatmap.colormap", 0);
     113        m.put("colormode.heatmap.gain", 0);
     114        m.put("colormode.heatmap.line-extra", false); //Einstein only
     115        m.put("colormode.heatmap.lower-limit", 0);
     116        m.put("colormode.heatmap.use-points", false);
     117        m.put("colormode.time.min-distance", 60); //Einstein only
     118        m.put("colormode.velocity.tune", 45);
     119        m.put("lines", -1);
     120        m.put("lines.alpha-blend", false);
     121        m.put("lines.arrows", false);
     122        m.put("lines.arrows.fast", false);
     123        m.put("lines.arrows.min-distance", 40);
     124        m.put("lines.force", false);
     125        m.put("lines.max-length", 200);
     126        m.put("lines.max-length.local", -1);
     127        m.put("lines.width", 0);
     128        m.put("markers.color", "");
     129        m.put("markers.show-text", true);
     130        m.put("markers.pattern", Marker.LABEL_PATTERN_AUTO);
     131        m.put("markers.audio.pattern", "?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }");
     132        m.put("points.hdopcircle", false);
     133        m.put("points.large", false);
     134        m.put("points.large.alpha", -1); //Einstein only
     135        m.put("points.large.size", 3); //Einstein only
     136        return Collections.unmodifiableMap(m);
     137    }
     138
     139    /**
     140     * Constructs a new {@code GPXSettingsPanel} for the given layers.
     141     * @param layers the GPX layers
     142     */
     143    public GPXSettingsPanel(List<GpxLayer> layers) {
    107144        super(new GridBagLayout());
    108         this.local = local;
    109         this.nonlocal = nonlocal;
    110         this.layerName = "layer "+layerName;
     145        this.layers = layers;
     146        if (layers == null || layers.isEmpty()) {
     147            throw new InvalidArgumentException("At least one layer required");
     148        }
     149        firstLayer = layers.get(0);
     150        global = false;
     151        hasLocalFile = layers.stream().anyMatch(GpxLayer::isLocalFile);
     152        hasNonLocalFile = layers.stream().anyMatch(l -> !l.isLocalFile());
    111153        initComponents();
    112154        loadPreferences();
     
    118160    public GPXSettingsPanel() {
    119161        super(new GridBagLayout());
     162        layers = null;
     163        firstLayer = null;
     164        global = hasLocalFile = hasNonLocalFile = true;
    120165        initComponents();
    121         local = false;
    122         nonlocal = false;
    123166        loadPreferences(); // preferences -> controls
     167    }
     168
     169    /**
     170     * Reads the preference for the given layer or the default preference if not available
     171     * @param layer the GpxLayer. Can be <code>null</code>, default preference will be returned then
     172     * @param key the drawing key to be read, without "draw.rawgps."
     173     * @return the value
     174     */
     175    public static String getLayerPref(GpxLayer layer, String key) {
     176        Object d = DEFAULT_PREFS.get(key);
     177        String ds;
     178        if (d != null) {
     179            ds = d.toString();
     180        } else {
     181            Logging.warn("No default value found for layer preference \"" + key + "\".");
     182            ds = null;
     183        }
     184        return Optional.ofNullable(tryGetLayerPrefLocal(layer, key)).orElse(Config.getPref().get("draw.rawgps." + key, ds));
     185    }
     186
     187    /**
     188     * Reads the integer preference for the given layer or the default preference if not available
     189     * @param layer the GpxLayer. Can be <code>null</code>, default preference will be returned then
     190     * @param key the drawing key to be read, without "draw.rawgps."
     191     * @return the integer value
     192     */
     193    public static int getLayerPrefInt(GpxLayer layer, String key) {
     194        String s = getLayerPref(layer, key);
     195        if (s != null) {
     196            try {
     197                return Integer.parseInt(s);
     198            } catch (NumberFormatException ex) {
     199                Object d = DEFAULT_PREFS.get(key);
     200                if (d instanceof Integer) {
     201                    return (int) d;
     202                } else {
     203                    Logging.warn("No valid default value found for layer preference \"" + key + "\".");
     204                }
     205            }
     206        }
     207        return 0;
     208    }
     209
     210    /**
     211     * Try to read the preference for the given layer
     212     * @param layer the GpxLayer
     213     * @param key the drawing key to be read, without "draw.rawgps."
     214     * @return the value or <code>null</code> if not found
     215     */
     216    public static String tryGetLayerPrefLocal(GpxLayer layer, String key) {
     217        return layer != null ? tryGetLayerPrefLocal(layer.data, key) : null;
     218    }
     219
     220    /**
     221     * Try to read the preference for the given GpxData
     222     * @param data the GpxData
     223     * @param key the drawing key to be read, without "draw.rawgps."
     224     * @return the value or <code>null</code> if not found
     225     */
     226    public static String tryGetLayerPrefLocal(GpxData data, String key) {
     227        return data != null ? data.getLayerPrefs().get(key) : null;
     228    }
     229
     230    /**
     231     * Puts the preference for the given layers or the default preference if layers is <code>null</code>
     232     * @param layers List of <code>GpxLayer</code> to put the drawingOptions
     233     * @param key the drawing key to be written, without "draw.rawgps."
     234     * @param value (can be <code>null</code> to remove option)
     235     */
     236    public static void putLayerPref(List<GpxLayer> layers, String key, Object value) {
     237        String v = value == null ? null : value.toString();
     238        if (layers != null) {
     239            for (GpxLayer l : layers) {
     240                putLayerPrefLocal(l.data, key, v);
     241            }
     242        } else {
     243            Config.getPref().put("draw.rawgps." + key, v);
     244        }
     245    }
     246
     247    /**
     248     * Puts the preference for the given layer
     249     * @param layer <code>GpxLayer</code> to put the drawingOptions
     250     * @param key the drawing key to be written, without "draw.rawgps."
     251     * @param value the value or <code>null</code> to remove key
     252     */
     253    public static void putLayerPrefLocal(GpxLayer layer, String key, String value) {
     254        if (layer == null) return;
     255        putLayerPrefLocal(layer.data, key, value);
     256    }
     257
     258    /**
     259     * Puts the preference for the given layer
     260     * @param data <code>GpxData</code> to put the drawingOptions. Must not be <code>null</code>
     261     * @param key the drawing key to be written, without "draw.rawgps."
     262     * @param value the value or <code>null</code> to remove key
     263     */
     264    public static void putLayerPrefLocal(GpxData data, String key, String value) {
     265        if (value == null || value.trim().isEmpty() || (getLayerPref(null, key).equals(value) && DEFAULT_PREFS.get(key) != null && DEFAULT_PREFS.get(key).toString().equals(value))) {
     266            data.getLayerPrefs().remove(key);
     267        } else {
     268            data.getLayerPrefs().put(key, value);
     269        }
     270    }
     271
     272    private String pref(String key) {
     273        return getLayerPref(firstLayer, key);
     274    }
     275
     276    private boolean prefBool(String key) {
     277        return Boolean.parseBoolean(pref(key));
     278    }
     279
     280    private int prefInt(String key) {
     281        return getLayerPrefInt(firstLayer, key);
     282    }
     283
     284    private int prefIntLocal(String key) {
     285        try {
     286            return Integer.parseInt(tryGetLayerPrefLocal(firstLayer, key));
     287        } catch (NumberFormatException ex) {
     288            return -1;
     289        }
     290
     291    }
     292
     293    private void putPref(String key, Object value) {
     294        putLayerPref(layers, key, value);
    124295    }
    125296
     
    128299        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    129300
    130         // makeAutoMarkers
    131         makeAutoMarkers.setToolTipText(tr("Automatically make a marker layer from any waypoints when opening a GPX layer."));
    132         ExpertToggleAction.addVisibilitySwitcher(makeAutoMarkers);
    133         add(makeAutoMarkers, GBC.eol().insets(20, 0, 0, 5));
     301        if (global) {
     302            // makeAutoMarkers
     303            makeAutoMarkers.setToolTipText(tr("Automatically make a marker layer from any waypoints when opening a GPX layer."));
     304            ExpertToggleAction.addVisibilitySwitcher(makeAutoMarkers);
     305            add(makeAutoMarkers, GBC.eol().insets(20, 0, 0, 5));
     306        }
    134307
    135308        // drawRawGpsLines
    136309        ButtonGroup gpsLinesGroup = new ButtonGroup();
    137         if (layerName != null) {
     310        if (!global) {
    138311            gpsLinesGroup.add(drawRawGpsLinesGlobal);
    139312        }
     
    146319        JLabel label = new JLabel(tr("Draw lines between raw GPS points"));
    147320        add(label, GBC.eol().insets(20, 0, 0, 0));
    148         if (layerName != null) {
     321        if (!global) {
    149322            add(drawRawGpsLinesGlobal, GBC.eol().insets(40, 0, 0, 0));
    150323        }
    151324        add(drawRawGpsLinesNone, GBC.eol().insets(40, 0, 0, 0));
    152         if (layerName == null || local) {
     325        if (hasLocalFile) {
    153326            add(drawRawGpsLinesLocal, GBC.eol().insets(40, 0, 0, 0));
    154327        }
    155         if (layerName == null || nonlocal) {
     328        if (hasNonLocalFile) {
    156329            add(drawRawGpsLinesAll, GBC.eol().insets(40, 0, 0, 0));
    157330        }
     
    243416        // colorTracks
    244417        ButtonGroup colorGroup = new ButtonGroup();
    245         if (layerName != null) {
     418        if (!global) {
    246419            colorGroup.add(colorTypeGlobal);
    247420        }
     
    254427        colorGroup.add(colorTypeHeatMap);
    255428
    256         colorTypeNone.setToolTipText(tr("All points and track segments will have the same color. Can be customized in Layer Manager."));
     429        colorTypeNone.setToolTipText(tr("All points and track segments will have their own color. Can be customized in Layer Manager."));
    257430        colorTypeVelocity.setToolTipText(tr("Colors points and track segments by velocity."));
    258431        colorTypeDirection.setToolTipText(tr("Colors points and track segments by direction."));
     
    273446
    274447        add(new JLabel(tr("Track and Point Coloring")), GBC.eol().insets(20, 0, 0, 0));
    275         if (layerName != null) {
     448        if (!global) {
    276449            add(colorTypeGlobal, GBC.eol().insets(40, 0, 0, 0));
    277450        }
     
    332505                // get image size of environment
    333506                final int iconSize = (int) dim.getHeight();
    334                 final Color color;
    335                 // ask the GPX draw for the correct color of that layer ( if there is one )
    336                 if (null != layerName) {
    337                     color = GpxDrawHelper.DEFAULT_COLOR.getChildColor(
    338                             NamedColorProperty.COLOR_CATEGORY_LAYER, layerName, GpxDrawHelper.DEFAULT_COLOR.getName()).get();
    339                 } else {
    340                     color = GpxDrawHelper.DEFAULT_COLOR.getDefaultValue();
    341                 }
    342                 colorTypeHeatIconLabel.setIcon(GpxDrawHelper.getColorMapImageIcon(color, colorTypeHeatMapTune.getSelectedIndex(), iconSize));
     507                colorTypeHeatIconLabel.setIcon(GpxDrawHelper.getColorMapImageIcon(
     508                        GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get(),
     509                        colorTypeHeatMapTune.getSelectedIndex(),
     510                        iconSize));
    343511            }
    344512        });
     
    354522        ExpertToggleAction.addVisibilitySwitcher(colorDynamic);
    355523
    356         if (layerName == null) {
     524        if (global) {
    357525            // Setting waypoints for gpx layer doesn't make sense - waypoints are shown in marker layer that has different name - so show
    358526            // this only for global config
     
    364532            add(waypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
    365533            waypointLabel.addActionListener(e -> updateWaypointPattern(waypointLabel, waypointLabelPattern));
    366             updateWaypointLabelCombobox(waypointLabel, waypointLabelPattern, TemplateEntryProperty.forMarker(layerName));
    367534            add(waypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 5));
    368535            ExpertToggleAction.addVisibilitySwitcher(label);
     
    380547            add(audioWaypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
    381548            audioWaypointLabel.addActionListener(e -> updateWaypointPattern(audioWaypointLabel, audioWaypointLabelPattern));
    382             updateWaypointLabelCombobox(audioWaypointLabel, audioWaypointLabelPattern, TemplateEntryProperty.forAudioMarker(layerName));
    383549            add(audioWaypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 5));
    384550            ExpertToggleAction.addVisibilitySwitcher(label);
     
    396562    public final void loadPreferences() {
    397563        makeAutoMarkers.setSelected(Config.getPref().getBoolean("marker.makeautomarkers", true));
    398         if (layerName != null && Config.getPref().get("draw.rawgps.lines."+layerName).isEmpty()
    399                 && Config.getPref().get("draw.rawgps.lines.local."+layerName).isEmpty()) {
    400             // no line preferences for layer is found
     564        int lines = global ? prefInt("lines") : prefIntLocal("lines");
     565        if (lines == 2 && hasNonLocalFile) {
     566            drawRawGpsLinesAll.setSelected(true);
     567        } else if ((lines == 1 && hasLocalFile) || (lines == -1 && global)) {
     568            drawRawGpsLinesLocal.setSelected(true);
     569        } else if (lines == 0) {
     570            drawRawGpsLinesNone.setSelected(true);
     571        } else if (lines == -1) {
    401572            drawRawGpsLinesGlobal.setSelected(true);
    402573        } else {
    403             Boolean lf = PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.lines.local", layerName, true);
    404             if (PreferencesUtils.getBoolean(Config.getPref(), "draw.rawgps.lines", layerName, true)) {
    405                 drawRawGpsLinesAll.setSelected(true);
    406             } else if (lf) {
    407                 drawRawGpsLinesLocal.setSelected(true);
    408             } else {
    409                 drawRawGpsLinesNone.setSelected(true);
    410             }
    411         }
    412 
    413         drawRawGpsMaxLineLengthLocal.setText(Integer.toString(PreferencesUtils.getInteger(Config.getPref(),
    414                 "draw.rawgps.max-line-length.local", layerName, -1)));
    415         drawRawGpsMaxLineLength.setText(Integer.toString(PreferencesUtils.getInteger(Config.getPref(),
    416                 "draw.rawgps.max-line-length", layerName, 200)));
    417         drawLineWidth.setText(Integer.toString(PreferencesUtils.getInteger(Config.getPref(),
    418                 "draw.rawgps.linewidth", layerName, 0)));
    419         drawLineWithAlpha.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    420                 "draw.rawgps.lines.alpha-blend", layerName, false));
    421         forceRawGpsLines.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    422                 "draw.rawgps.lines.force", layerName, false));
    423         drawGpsArrows.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    424                 "draw.rawgps.direction", layerName, false));
    425         drawGpsArrowsFast.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    426                 "draw.rawgps.alternatedirection", layerName, false));
    427         drawGpsArrowsMinDist.setText(Integer.toString(PreferencesUtils.getInteger(Config.getPref(),
    428                 "draw.rawgps.min-arrow-distance", layerName, 40)));
    429         hdopCircleGpsPoints.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    430                 "draw.rawgps.hdopcircle", layerName, false));
    431         largeGpsPoints.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    432                 "draw.rawgps.large", layerName, false));
     574            Logging.warn("Unknown line type: " + lines);
     575        }
     576        drawRawGpsMaxLineLengthLocal.setText(pref("lines.max-length.local"));
     577        drawRawGpsMaxLineLength.setText(pref("lines.max-length"));
     578        drawLineWidth.setText(pref("lines.width"));
     579        drawLineWithAlpha.setSelected(prefBool("lines.alpha-blend"));
     580        forceRawGpsLines.setSelected(prefBool("lines.force"));
     581        drawGpsArrows.setSelected(prefBool("lines.arrows"));
     582        drawGpsArrowsFast.setSelected(prefBool("lines.arrows.fast"));
     583        drawGpsArrowsMinDist.setText(pref("lines.arrows.min-distance"));
     584        hdopCircleGpsPoints.setSelected(prefBool("points.hdopcircle"));
     585        largeGpsPoints.setSelected(prefBool("points.large"));
    433586        useGpsAntialiasing.setSelected(Config.getPref().getBoolean("mappaint.gpx.use-antialiasing", false));
    434587
    435588        drawRawGpsLinesActionListener.actionPerformed(null);
    436 
    437         if (layerName != null && Config.getPref().get("draw.rawgps.colors."+layerName).isEmpty()) {
     589        if (!global && prefIntLocal("colormode") == -1) {
    438590            colorTypeGlobal.setSelected(true);
    439591            colorDynamic.setSelected(false);
     
    443595            colorTypeHeatMapLowerLimit.setValue(0);
    444596        } else {
    445             int colorType = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.colors", layerName, 0);
     597            int colorType = prefInt("colormode");
    446598            switch (colorType) {
    447             case 0: colorTypeNone.setSelected(true); break;
     599            case -1: case 0: colorTypeNone.setSelected(true); break;
    448600            case 1: colorTypeVelocity.setSelected(true); break;
    449601            case 2: colorTypeDilution.setSelected(true); break;
     
    454606            default: Logging.warn("Unknown color type: " + colorType);
    455607            }
    456             int ccts = PreferencesUtils.getInteger(Config.getPref(), "draw.rawgps.colorTracksTune", layerName, 45);
     608            int ccts = prefInt("colormode.velocity.tune");
    457609            colorTypeVelocityTune.setSelectedIndex(ccts == 10 ? 2 : (ccts == 20 ? 1 : 0));
    458             colorTypeHeatMapTune.setSelectedIndex(PreferencesUtils.getInteger(Config.getPref(),
    459                     "draw.rawgps.heatmap.colormap", layerName, 0));
    460             colorDynamic.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    461                     "draw.rawgps.colors.dynamic", layerName, false));
    462             colorTypeHeatMapPoints.setSelected(PreferencesUtils.getBoolean(Config.getPref(),
    463                     "draw.rawgps.heatmap.use-points", layerName, false));
    464             colorTypeHeatMapGain.setValue(PreferencesUtils.getInteger(Config.getPref(),
    465                     "draw.rawgps.heatmap.gain", layerName, 0));
    466             colorTypeHeatMapLowerLimit.setValue(PreferencesUtils.getInteger(Config.getPref(),
    467                     "draw.rawgps.heatmap.lower-limit", layerName, 0));
    468         }
    469     }
    470 
    471     /**
    472      * Save preferences from UI controls, globally or for a specified layer.
    473      * @param layerName The GPX layer name. Can be {@code null}, in that case, global preferences are written
    474      * @param locLayer {@code true} if the GPX layer is a local one. Ignored if {@code layerName} is null
     610            colorTypeHeatMapTune.setSelectedIndex(prefInt("colormode.heatmap.colormap"));
     611            colorDynamic.setSelected(prefBool("colormode.dynamic-range"));
     612            colorTypeHeatMapPoints.setSelected(prefBool("colormode.heatmap.use-points"));
     613            colorTypeHeatMapGain.setValue(prefInt("colormode.heatmap.gain"));
     614            colorTypeHeatMapLowerLimit.setValue(prefInt("colormode.heatmap.lower-limit"));
     615        }
     616        updateWaypointLabelCombobox(waypointLabel, waypointLabelPattern, pref("markers.pattern"));
     617        updateWaypointLabelCombobox(audioWaypointLabel, audioWaypointLabelPattern, pref("markers.audio.pattern"));
     618
     619    }
     620
     621    /**
     622     * Save preferences from UI controls, globally or for the specified layers.
    475623     * @return {@code true} when restart is required, {@code false} otherwise
    476624     */
    477     public boolean savePreferences(String layerName, boolean locLayer) {
    478         String layerNameDot = ".layer "+layerName;
    479         if (layerName == null) {
    480             layerNameDot = "";
    481         }
    482         Config.getPref().putBoolean("marker.makeautomarkers"+layerNameDot, makeAutoMarkers.isSelected());
    483         if (drawRawGpsLinesGlobal.isSelected()) {
    484             Config.getPref().put("draw.rawgps.lines" + layerNameDot, null);
    485             Config.getPref().put("draw.rawgps.max-line-length" + layerNameDot, null);
    486             Config.getPref().put("draw.rawgps.lines.local" + layerNameDot, null);
    487             Config.getPref().put("draw.rawgps.max-line-length.local" + layerNameDot, null);
    488             Config.getPref().put("draw.rawgps.lines.force"+layerNameDot, null);
    489             Config.getPref().put("draw.rawgps.direction"+layerNameDot, null);
    490             Config.getPref().put("draw.rawgps.alternatedirection"+layerNameDot, null);
    491             Config.getPref().put("draw.rawgps.min-arrow-distance"+layerNameDot, null);
     625    public boolean savePreferences() {
     626        if (global) {
     627            Config.getPref().putBoolean("marker.makeautomarkers", makeAutoMarkers.isSelected());
     628            putPref("markers.pattern", waypointLabelPattern.getText());
     629            putPref("markers.audio.pattern", audioWaypointLabelPattern.getText());
     630        }
     631        boolean g;
     632        if (!global && ((g = drawRawGpsLinesGlobal.isSelected()) || drawRawGpsLinesNone.isSelected())) {
     633            if (g) {
     634                putPref("lines", null);
     635            } else {
     636                putPref("lines", 0);
     637            }
     638            putPref("lines.max-length", null);
     639            putPref("lines.max-length.local", null);
     640            putPref("lines.force", null);
     641            putPref("lines.arrows", null);
     642            putPref("lines.arrows.fast", null);
     643            putPref("lines.arrows.min-distance", null);
    492644        } else {
    493             if (layerName == null || !locLayer) {
    494                 Config.getPref().putBoolean("draw.rawgps.lines" + layerNameDot, drawRawGpsLinesAll.isSelected());
    495                 Config.getPref().put("draw.rawgps.max-line-length" + layerNameDot, drawRawGpsMaxLineLength.getText());
     645            if (drawRawGpsLinesLocal.isSelected()) {
     646                putPref("lines", 1);
     647            } else if (drawRawGpsLinesAll.isSelected()) {
     648                putPref("lines", 2);
    496649            }
    497             if (layerName == null || locLayer) {
    498                 Config.getPref().putBoolean("draw.rawgps.lines.local" + layerNameDot,
    499                         drawRawGpsLinesAll.isSelected() || drawRawGpsLinesLocal.isSelected());
    500                 Config.getPref().put("draw.rawgps.max-line-length.local" + layerNameDot,
    501                         drawRawGpsMaxLineLengthLocal.getText());
    502             }
    503             Config.getPref().putBoolean("draw.rawgps.lines.force"+layerNameDot, forceRawGpsLines.isSelected());
    504             Config.getPref().putBoolean("draw.rawgps.direction"+layerNameDot, drawGpsArrows.isSelected());
    505             Config.getPref().putBoolean("draw.rawgps.alternatedirection"+layerNameDot, drawGpsArrowsFast.isSelected());
    506             Config.getPref().put("draw.rawgps.min-arrow-distance"+layerNameDot, drawGpsArrowsMinDist.getText());
    507         }
    508 
    509         Config.getPref().putBoolean("draw.rawgps.hdopcircle"+layerNameDot, hdopCircleGpsPoints.isSelected());
    510         Config.getPref().putBoolean("draw.rawgps.large"+layerNameDot, largeGpsPoints.isSelected());
    511         Config.getPref().put("draw.rawgps.linewidth"+layerNameDot, drawLineWidth.getText());
    512         Config.getPref().putBoolean("draw.rawgps.lines.alpha-blend"+layerNameDot, drawLineWithAlpha.isSelected());
     650            putPref("lines.max-length", drawRawGpsMaxLineLength.getText());
     651            putPref("lines.max-length.local", drawRawGpsMaxLineLengthLocal.getText());
     652            putPref("lines.force", forceRawGpsLines.isSelected());
     653            putPref("lines.arrows", drawGpsArrows.isSelected());
     654            putPref("lines.arrows.fast", drawGpsArrowsFast.isSelected());
     655            putPref("lines.arrows.min-distance", drawGpsArrowsMinDist.getText());
     656        }
     657
     658        putPref("points.hdopcircle", hdopCircleGpsPoints.isSelected());
     659        putPref("points.large", largeGpsPoints.isSelected());
     660        putPref("lines.width", drawLineWidth.getText());
     661        putPref("lines.alpha-blend", drawLineWithAlpha.isSelected());
    513662
    514663        Config.getPref().putBoolean("mappaint.gpx.use-antialiasing", useGpsAntialiasing.isSelected());
    515664
    516         TemplateEntryProperty.forMarker(layerName).put(waypointLabelPattern.getText());
    517         TemplateEntryProperty.forAudioMarker(layerName).put(audioWaypointLabelPattern.getText());
    518 
    519665        if (colorTypeGlobal.isSelected()) {
    520             Config.getPref().put("draw.rawgps.colors"+layerNameDot, null);
    521             Config.getPref().put("draw.rawgps.colors.dynamic"+layerNameDot, null);
    522             Config.getPref().put("draw.rawgps.colorTracksTunec"+layerNameDot, null);
     666            putPref("colormode", null);
     667            putPref("colormode.dynamic-range", null);
     668            putPref("colormode.velocity.tune", null);
    523669            return false;
    524670        } else if (colorTypeVelocity.isSelected()) {
    525             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 1);
     671            putPref("colormode", 1);
    526672        } else if (colorTypeDilution.isSelected()) {
    527             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 2);
     673            putPref("colormode", 2);
    528674        } else if (colorTypeDirection.isSelected()) {
    529             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 3);
     675            putPref("colormode", 3);
    530676        } else if (colorTypeTime.isSelected()) {
    531             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 4);
     677            putPref("colormode", 4);
    532678        } else if (colorTypeHeatMap.isSelected()) {
    533             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 5);
     679            putPref("colormode", 5);
    534680        } else if (colorTypeQuality.isSelected()) {
    535             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 6);
     681            putPref("colormode", 6);
    536682        } else {
    537             Config.getPref().putInt("draw.rawgps.colors"+layerNameDot, 0);
    538         }
    539         Config.getPref().putBoolean("draw.rawgps.colors.dynamic"+layerNameDot, colorDynamic.isSelected());
     683            putPref("colormode", 0);
     684        }
     685        putPref("colormode.dynamic-range", colorDynamic.isSelected());
    540686        int ccti = colorTypeVelocityTune.getSelectedIndex();
    541         Config.getPref().putInt("draw.rawgps.colorTracksTune"+layerNameDot, ccti == 2 ? 10 : (ccti == 1 ? 20 : 45));
    542         Config.getPref().putInt("draw.rawgps.heatmap.colormap"+layerNameDot, colorTypeHeatMapTune.getSelectedIndex());
    543         Config.getPref().putBoolean("draw.rawgps.heatmap.use-points"+layerNameDot, colorTypeHeatMapPoints.isSelected());
    544         Config.getPref().putInt("draw.rawgps.heatmap.gain"+layerNameDot, colorTypeHeatMapGain.getValue());
    545         Config.getPref().putInt("draw.rawgps.heatmap.lower-limit"+layerNameDot, colorTypeHeatMapLowerLimit.getValue());
     687        putPref("colormode.velocity.tune", ccti == 2 ? 10 : (ccti == 1 ? 20 : 45));
     688        putPref("colormode.heatmap.colormap", colorTypeHeatMapTune.getSelectedIndex());
     689        putPref("colormode.heatmap.use-points", colorTypeHeatMapPoints.isSelected());
     690        putPref("colormode.heatmap.gain", colorTypeHeatMapGain.getValue());
     691        putPref("colormode.heatmap.lower-limit", colorTypeHeatMapLowerLimit.getValue());
     692
     693        if (!global && layers != null && !layers.isEmpty()) {
     694            layers.forEach(l -> l.data.invalidate());
     695        }
    546696
    547697        return false;
    548698    }
    549699
    550     /**
    551      * Save preferences from UI controls for initial layer or globally
    552      * @return {@code true} when restart is required, {@code false} otherwise
    553      */
    554     public boolean savePreferences() {
    555         return savePreferences(null, false);
    556     }
    557 
    558     private static void updateWaypointLabelCombobox(JosmComboBox<String> cb, JosmTextField tf, TemplateEntryProperty property) {
    559         String labelPattern = property.getAsString();
     700    private static void updateWaypointLabelCombobox(JosmComboBox<String> cb, JosmTextField tf, String labelPattern) {
    560701        boolean found = false;
    561702        for (int i = 0; i < LABEL_PATTERN_TEMPLATE.length; i++) {
  • trunk/src/org/openstreetmap/josm/io/GpxReader.java

    r14456 r15496  
    1919import org.openstreetmap.josm.data.Bounds;
    2020import org.openstreetmap.josm.data.coor.LatLon;
    21 import org.openstreetmap.josm.data.gpx.Extensions;
    2221import org.openstreetmap.josm.data.gpx.GpxConstants;
    2322import org.openstreetmap.josm.data.gpx.GpxData;
     23import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
     24import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
    2425import org.openstreetmap.josm.data.gpx.GpxLink;
    2526import org.openstreetmap.josm.data.gpx.GpxRoute;
    26 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
     27import org.openstreetmap.josm.data.gpx.GpxTrack;
     28import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     29import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    2730import org.openstreetmap.josm.data.gpx.WayPoint;
    2831import org.openstreetmap.josm.tools.Logging;
     
    6871
    6972        private GpxData data;
    70         private Collection<Collection<WayPoint>> currentTrack;
     73        private Collection<IGpxTrackSegment> currentTrack;
    7174        private Map<String, Object> currentTrackAttr;
    7275        private Collection<WayPoint> currentTrackSeg;
     
    7780
    7881        private GpxLink currentLink;
    79         private Extensions currentExtensions;
     82        private GpxExtensionCollection currentExtensionCollection;
     83        private GpxExtensionCollection currentTrackExtensionCollection;
    8084        private Stack<State> states;
    8185        private final Stack<String> elements = new Stack<>();
     
    8993            accumulator = new StringBuilder();
    9094            states = new Stack<>();
    91             data = new GpxData();
     95            data = new GpxData(true);
     96            currentExtensionCollection = new GpxExtensionCollection();
     97            currentTrackExtensionCollection = new GpxExtensionCollection();
     98        }
     99
     100        @Override
     101        public void startPrefixMapping(String prefix, String uri) throws SAXException {
     102            data.getNamespaces().add(new XMLNamespace(prefix, uri));
    92103        }
    93104
     
    134145                    version = "1.1";
    135146                }
     147                String schemaLocation = atts.getValue(GpxConstants.XML_URI_XSD, "schemaLocation");
     148                if (schemaLocation != null) {
     149                    String[] schemaLocations = schemaLocation.split(" ");
     150                    for (int i = 0; i < schemaLocations.length - 1; i += 2) {
     151                        final String schemaURI = schemaLocations[i];
     152                        final String schemaXSD = schemaLocations[i + 1];
     153                        data.getNamespaces().stream().filter(xml -> xml.getURI().equals(schemaURI)).forEach(xml -> {
     154                            xml.setLocation(schemaXSD);
     155                        });
     156                    }
     157                }
    136158                break;
    137159            case GPX:
     
    160182                    states.push(currentState);
    161183                    currentState = State.EXT;
    162                     currentExtensions = new Extensions();
    163184                    break;
    164185                case "gpx":
     
    179200                    states.push(currentState);
    180201                    currentState = State.EXT;
    181                     currentExtensions = new Extensions();
    182202                    break;
    183203                case "copyright":
     
    229249                    states.push(currentState);
    230250                    currentState = State.EXT;
    231                     currentExtensions = new Extensions();
    232251                    break;
    233252                default: // Do nothing
     
    235254                break;
    236255            case TRKSEG:
    237                 if ("trkpt".equals(localName)) {
     256                switch (localName) {
     257                case "trkpt":
    238258                    states.push(currentState);
    239259                    currentState = State.WPT;
    240260                    currentWayPoint = new WayPoint(parseLatLon(atts));
     261                    break;
     262                case "extensions":
     263                    states.push(currentState);
     264                    currentState = State.EXT;
     265                    break;
    241266                }
    242267                break;
     
    251276                    states.push(currentState);
    252277                    currentState = State.EXT;
    253                     currentExtensions = new Extensions();
    254278                    break;
    255279                default: // Do nothing
     
    271295                    states.push(currentState);
    272296                    currentState = State.EXT;
    273                     currentExtensions = new Extensions();
    274                     break;
    275                 default: // Do nothing
     297                    break;
     298                default: // Do nothing
     299                }
     300                break;
     301            case EXT:
     302                if (states.lastElement() == State.TRK) {
     303                    currentTrackExtensionCollection.openChild(namespaceURI, qName, atts);
     304                } else {
     305                    currentExtensionCollection.openChild(namespaceURI, qName, atts);
    276306                }
    277307                break;
     
    350380                        (currentState == State.GPX && "gpx".equals(localName))) {
    351381                        convertUrlToLink(data.attr);
    352                         if (currentExtensions != null && !currentExtensions.isEmpty()) {
    353                             data.put(META_EXTENSIONS, currentExtensions);
    354                         }
     382                        data.getExtensions().addAll(currentExtensionCollection);
     383                        currentExtensionCollection.clear();
    355384                        currentState = states.pop();
    356385                    }
     
    360389                    break;
    361390                default:
    362                     //TODO: parse extensions
    363391                }
    364392                break;
     
    465493                    currentState = states.pop();
    466494                    convertUrlToLink(currentWayPoint.attr);
    467                     if (currentExtensions != null && !currentExtensions.isEmpty()) {
    468                         currentWayPoint.put(META_EXTENSIONS, currentExtensions);
    469                     }
     495                    currentWayPoint.getExtensions().addAll(currentExtensionCollection);
    470496                    data.waypoints.add(currentWayPoint);
     497                    currentExtensionCollection.clear();
    471498                    break;
    472499                default: // Do nothing
     
    476503                if ("trkseg".equals(localName)) {
    477504                    currentState = states.pop();
    478                     currentTrack.add(currentTrackSeg);
     505                    if (!currentTrackSeg.isEmpty()) {
     506                        GpxTrackSegment seg = new GpxTrackSegment(currentTrackSeg);
     507                        seg.getExtensions().addAll(currentExtensionCollection);
     508                        currentTrack.add(seg);
     509                    }
     510                    currentExtensionCollection.clear();
    479511                }
    480512                break;
     
    484516                    currentState = states.pop();
    485517                    convertUrlToLink(currentTrackAttr);
    486                     data.addTrack(new ImmutableGpxTrack(currentTrack, currentTrackAttr));
     518                    GpxTrack trk = new GpxTrack(new ArrayList<>(currentTrack), currentTrackAttr);
     519                    trk.getExtensions().addAll(currentTrackExtensionCollection);
     520                    data.addTrack(trk);
     521                    currentTrackExtensionCollection.clear();
    487522                    break;
    488523                case "name":
     
    502537                if ("extensions".equals(localName)) {
    503538                    currentState = states.pop();
    504                 } else if (JOSM_EXTENSIONS_NAMESPACE_URI.equals(namespaceURI)) {
    505                     // only interested in extensions written by JOSM
    506                     currentExtensions.put(localName, accumulator.toString());
     539                } else if (currentExtensionCollection != null) {
     540                    String acc = accumulator.toString().trim();
     541                    if (states.lastElement() == State.TRK) {
     542                        currentTrackExtensionCollection.closeChild(qName, acc); //a segment inside the track can have an extension too
     543                    } else {
     544                        currentExtensionCollection.closeChild(qName, acc);
     545                    }
    507546                }
    508547                break;
     
    520559                }
    521560            }
     561            accumulator.setLength(0);
    522562        }
    523563
     
    526566            if (!states.empty())
    527567                throw new SAXException(tr("Parse error: invalid document structure for GPX document."));
    528             Extensions metaExt = (Extensions) data.get(META_EXTENSIONS);
    529             if (metaExt != null && "true".equals(metaExt.get("from-server"))) {
    530                 data.fromServer = true;
    531             }
     568
     569            data.getExtensions().stream("josm", "from-server").findAny().ifPresent(ext -> {
     570                data.fromServer = "true".equals(ext.getValue());
     571            });
     572
     573            data.getExtensions().stream("josm", "layerPreferences").forEach(prefs -> {
     574                prefs.getExtensions().stream("josm", "entry").forEach(prefEntry -> {
     575                    Object key = prefEntry.get("key");
     576                    Object val = prefEntry.get("value");
     577                    if (key != null && val != null) {
     578                        data.getLayerPrefs().put(key.toString(), val.toString());
     579                    }
     580                });
     581            });
     582            data.endUpdate();
    532583            gpxData = data;
    533584        }
  • trunk/src/org/openstreetmap/josm/io/GpxWriter.java

    r14459 r15496  
    99import java.io.PrintWriter;
    1010import java.nio.charset.StandardCharsets;
     11import java.util.ArrayList;
    1112import java.util.Collection;
    1213import java.util.Date;
    1314import java.util.List;
    1415import java.util.Map;
    15 import java.util.Map.Entry;
     16import java.util.Objects;
     17import java.util.stream.Collectors;
    1618
    1719import javax.xml.XMLConstants;
     
    1921import org.openstreetmap.josm.data.Bounds;
    2022import org.openstreetmap.josm.data.coor.LatLon;
    21 import org.openstreetmap.josm.data.gpx.Extensions;
    2223import org.openstreetmap.josm.data.gpx.GpxConstants;
    2324import org.openstreetmap.josm.data.gpx.GpxData;
     25import org.openstreetmap.josm.data.gpx.GpxData.XMLNamespace;
     26import org.openstreetmap.josm.data.gpx.GpxExtension;
     27import org.openstreetmap.josm.data.gpx.GpxExtensionCollection;
    2428import org.openstreetmap.josm.data.gpx.GpxLink;
    2529import org.openstreetmap.josm.data.gpx.GpxRoute;
    2630import org.openstreetmap.josm.data.gpx.GpxTrack;
    27 import org.openstreetmap.josm.data.gpx.GpxTrackSegment;
     31import org.openstreetmap.josm.data.gpx.IGpxTrackSegment;
    2832import org.openstreetmap.josm.data.gpx.IWithAttributes;
    2933import org.openstreetmap.josm.data.gpx.WayPoint;
     
    5559    private GpxData data;
    5660    private String indent = "";
     61    private List<String> validprefixes;
    5762
    5863    private static final int WAY_POINT = 0;
     
    6570     */
    6671    public void write(GpxData data) {
     72        write(data, ColorFormat.GPXD, true);
     73    }
     74
     75    /**
     76     * Writes the given GPX data.
     77     *
     78     * @param data The data to write
     79     * @param colorFormat determines if colors are saved and which extension is to be used
     80     * @param savePrefs whether layer specific preferences are saved
     81     */
     82    public void write(GpxData data, ColorFormat colorFormat, boolean savePrefs) {
    6783        this.data = data;
    68         // We write JOSM specific meta information into gpx 'extensions' elements.
    69         // In particular it is noted whether the gpx data is from the OSM server
    70         // (so the rendering of clouds of anonymous TrackPoints can be improved)
    71         // and some extra synchronization info for export of AudioMarkers.
    72         // It is checked in advance, if any extensions are used, so we know whether
    73         // a namespace declaration is necessary.
    74         boolean hasExtensions = data.fromServer;
    75         if (!hasExtensions) {
    76             for (WayPoint wpt : data.waypoints) {
    77                 Extensions extensions = (Extensions) wpt.get(META_EXTENSIONS);
    78                 if (extensions != null && !extensions.isEmpty()) {
    79                     hasExtensions = true;
    80                     break;
    81                 }
    82             }
    83         }
     84
     85        //Prepare extensions for writing
     86        data.beginUpdate();
     87        data.getTracks().forEach(trk -> trk.convertColor(colorFormat));
     88        data.getExtensions().removeAllWithPrefix("josm");
     89        if (data.fromServer) {
     90            data.getExtensions().add("josm", "from-server", "true");
     91        }
     92        if (savePrefs && !data.getLayerPrefs().isEmpty()) {
     93            GpxExtensionCollection layerExts = data.getExtensions().add("josm", "layerPreferences").getExtensions();
     94            data.getLayerPrefs().entrySet()
     95            .stream()
     96            .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))
     97            .forEach(entry -> {
     98                GpxExtension e = layerExts.add("josm", "entry");
     99                e.put("key", entry.getKey());
     100                e.put("value", entry.getValue());
     101            });
     102        }
     103        data.endUpdate();
     104
     105        Collection<IWithAttributes> all = new ArrayList<>();
     106
     107        all.add(data);
     108        all.addAll(data.getWaypoints());
     109        all.addAll(data.getRoutes());
     110        all.addAll(data.getTracks());
     111        all.addAll(data.getTrackSegmentsStream().collect(Collectors.toList()));
     112
     113        List<XMLNamespace> namespaces = all
     114                .stream()
     115                .flatMap(w -> w.getExtensions().getPrefixesStream())
     116                .distinct()
     117                .map(p -> data.getNamespaces()
     118                        .stream()
     119                        .filter(s -> s.getPrefix().equals(p))
     120                        .findAny()
     121                        .orElse(GpxExtension.findNamespace(p)))
     122                .filter(Objects::nonNull)
     123                .collect(Collectors.toList());
     124
     125        validprefixes = namespaces.stream().map(n -> n.getPrefix()).collect(Collectors.toList());
    84126
    85127        out.println("<?xml version='1.0' encoding='UTF-8'?>");
    86128        out.println("<gpx version=\"1.1\" creator=\"JOSM GPX export\" xmlns=\"http://www.topografix.com/GPX/1/1\"");
    87         out.println((hasExtensions ? String.format("    xmlns:josm=\"%s\"%n", JOSM_EXTENSIONS_NAMESPACE_URI) : "") +
    88                     "    xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\"");
    89         out.println("    xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">");
     129
     130        String schemaLocations = "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd";
     131
     132        for (XMLNamespace n : namespaces) {
     133            if (n.getURI() != null && n.getPrefix() != null && !n.getPrefix().isEmpty()) {
     134                out.println(String.format("    xmlns:%s=\"%s\"", n.getPrefix(), n.getURI()));
     135                if (n.getLocation() != null) {
     136                    schemaLocations += " " + n.getURI() + " " + n.getLocation();
     137                }
     138            }
     139        }
     140
     141        out.println("    xmlns:xsi=\""+XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI+"\"");
     142        out.println(String.format("    xsi:schemaLocation=\"%s\">", schemaLocations));
    90143        indent = "  ";
    91144        writeMetaData();
     
    105158                        gpxLink(link);
    106159                    }
    107                 }
    108             } else if (META_EXTENSIONS.equals(key)) {
    109                 Extensions extensions = (Extensions) obj.get(key);
    110                 if (extensions != null) {
    111                     gpxExtensions(extensions);
    112160                }
    113161            } else {
     
    148196                String[] tmp = data.getString(META_AUTHOR_EMAIL).split("@");
    149197                if (tmp.length == 2) {
    150                     inline("email", "id=\"" + tmp[0] + "\" domain=\""+tmp[1]+'\"');
     198                    inline("email", "id=\"" + encode(tmp[0]) + "\" domain=\"" + encode(tmp[1]) +'\"');
    151199                }
    152200            }
     
    159207        if (attr.containsKey(META_COPYRIGHT_LICENSE)
    160208                || attr.containsKey(META_COPYRIGHT_YEAR)) {
    161             openAtt("copyright", "author=\""+ data.get(META_COPYRIGHT_AUTHOR) +'\"');
     209            openln("copyright", "author=\""+ encode(data.get(META_COPYRIGHT_AUTHOR).toString()) +'\"');
    162210            if (attr.containsKey(META_COPYRIGHT_YEAR)) {
    163211                simpleTag("year", (String) data.get(META_COPYRIGHT_YEAR));
     
    188236        }
    189237
    190         if (data.fromServer) {
    191             openln("extensions");
    192             simpleTag("josm:from-server", "true");
    193             closeln("extensions");
    194         }
    195 
     238        gpxExtensions(data.getExtensions());
    196239        closeln("metadata");
    197240    }
     
    207250            openln("rte");
    208251            writeAttr(rte, RTE_TRK_KEYS);
     252            gpxExtensions(rte.getExtensions());
    209253            for (WayPoint pnt : rte.routePoints) {
    210254                wayPoint(pnt, ROUTE_POINT);
     
    218262            openln("trk");
    219263            writeAttr(trk, RTE_TRK_KEYS);
    220             for (GpxTrackSegment seg : trk.getSegments()) {
     264            gpxExtensions(trk.getExtensions());
     265            for (IGpxTrackSegment seg : trk.getSegments()) {
    221266                openln("trkseg");
     267                gpxExtensions(seg.getExtensions());
    222268                for (WayPoint pnt : seg.getWayPoints()) {
    223269                    wayPoint(pnt, TRACK_POINT);
     
    234280    }
    235281
     282    private void openln(String tag, String attributes) {
     283        open(tag, attributes);
     284        out.println();
     285    }
     286
    236287    private void open(String tag) {
    237288        out.print(indent + '<' + tag + '>');
     
    239290    }
    240291
    241     private void openAtt(String tag, String attributes) {
    242         out.println(indent + '<' + tag + ' ' + attributes + '>');
     292    private void open(String tag, String attributes) {
     293        out.print(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + '>');
    243294        indent += "  ";
    244295    }
    245296
    246297    private void inline(String tag, String attributes) {
    247         out.println(indent + '<' + tag + ' ' + attributes + "/>");
     298        out.println(indent + '<' + tag + (attributes.isEmpty() ? "" : ' ') + attributes + "/>");
    248299    }
    249300
     
    273324    }
    274325
     326    private void simpleTag(String tag, String content, String attributes) {
     327        if (content != null && !content.isEmpty()) {
     328            open(tag, attributes);
     329            out.print(encode(content));
     330            out.println("</" + tag + '>');
     331            indent = indent.substring(2);
     332        }
     333    }
     334
    275335    /**
    276336     * output link
     
    279339    private void gpxLink(GpxLink link) {
    280340        if (link != null) {
    281             openAtt("link", "href=\"" + link.uri + '\"');
     341            openln("link", "href=\"" + encode(link.uri) + '\"');
    282342            simpleTag("text", link.text);
    283343            simpleTag("type", link.type);
     
    309369            LatLon c = pnt.getCoor();
    310370            String coordAttr = "lat=\"" + c.lat() + "\" lon=\"" + c.lon() + '\"';
    311             if (pnt.attr.isEmpty()) {
     371            if (pnt.attr.isEmpty() && pnt.getExtensions().isEmpty()) {
    312372                inline(type, coordAttr);
    313373            } else {
    314                 openAtt(type, coordAttr);
     374                openln(type, coordAttr);
    315375                writeAttr(pnt, WPT_KEYS);
     376                gpxExtensions(pnt.getExtensions());
    316377                closeln(type);
    317378            }
     
    319380    }
    320381
    321     private void gpxExtensions(Extensions extensions) {
    322         if (extensions != null && !extensions.isEmpty()) {
     382    private void gpxExtensions(GpxExtensionCollection allExtensions) {
     383        if (allExtensions.isVisible()) {
    323384            openln("extensions");
    324             for (Entry<String, String> e : extensions.entrySet()) {
    325                 simpleTag("josm:" + e.getKey(), e.getValue());
    326             }
     385            writeExtension(allExtensions);
    327386            closeln("extensions");
    328387        }
    329388    }
     389
     390    private void writeExtension(List<GpxExtension> extensions) {
     391        for (GpxExtension e : extensions) {
     392            if (validprefixes.contains(e.getPrefix()) && e.isVisible()) {
     393                // this might lead to loss of an unknown extension *after* the file was saved as .osm,
     394                // but otherwise the file is invalid and can't even be parsed by SAX anymore
     395                String k = (e.getPrefix().isEmpty() ? "" : e.getPrefix() + ":") + e.getKey();
     396                String attr = String.join(" ", e.getAttributes().entrySet().stream().map(a -> encode(a.getKey()) + "=\"" + encode(a.getValue().toString()) + "\"").sorted().collect(Collectors.toList()));
     397                if (e.getValue() == null && e.getExtensions().isEmpty()) {
     398                    inline(k, attr);
     399                } else if (e.getExtensions().isEmpty()) {
     400                    simpleTag(k, e.getValue(), attr);
     401                } else {
     402                    openln(k, attr);
     403                    if (e.getValue() != null) {
     404                        out.print(encode(e.getValue()));
     405                    }
     406                    writeExtension(e.getExtensions());
     407                    closeln(k);
     408                }
     409            }
     410        }
     411    }
    330412}
  • trunk/src/org/openstreetmap/josm/io/nmea/NmeaReader.java

    r15247 r15496  
    2121import org.openstreetmap.josm.data.gpx.GpxConstants;
    2222import org.openstreetmap.josm.data.gpx.GpxData;
    23 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
     23import org.openstreetmap.josm.data.gpx.GpxTrack;
    2424import org.openstreetmap.josm.data.gpx.WayPoint;
    2525import org.openstreetmap.josm.io.IGpxReader;
     
    265265            }
    266266            currentTrack.add(ps.waypoints);
    267             data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
     267            data.tracks.add(new GpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
    268268
    269269        } catch (IllegalDataException e) {
  • trunk/src/org/openstreetmap/josm/io/rtklib/RtkLibPosReader.java

    r15343 r15496  
    1919import org.openstreetmap.josm.data.gpx.GpxConstants;
    2020import org.openstreetmap.josm.data.gpx.GpxData;
    21 import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
     21import org.openstreetmap.josm.data.gpx.GpxTrack;
    2222import org.openstreetmap.josm.data.gpx.WayPoint;
    2323import org.openstreetmap.josm.io.IGpxReader;
     
    115115        }
    116116        currentTrack.add(waypoints);
    117         data.tracks.add(new ImmutableGpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
     117        data.tracks.add(new GpxTrack(currentTrack, Collections.<String, Object>emptyMap()));
    118118        return true;
    119119    }
  • trunk/src/org/openstreetmap/josm/io/session/GenericSessionExporter.java

    r15404 r15496  
    8282        @Override
    8383        public void actionPerformed(ActionEvent e) {
    84             SaveAction.getInstance().doSave(layer);
     84            SaveAction.getInstance().doSave(layer, true);
    8585            updateEnabledState();
    8686        }
Note: See TracChangeset for help on using the changeset viewer.