001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.display;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trc;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.GridBagLayout;
010import java.awt.event.ActionListener;
011import java.util.Collections;
012import java.util.Enumeration;
013import java.util.HashMap;
014import java.util.List;
015import java.util.Map;
016import java.util.Optional;
017
018import javax.swing.AbstractButton;
019import javax.swing.BorderFactory;
020import javax.swing.Box;
021import javax.swing.ButtonGroup;
022import javax.swing.JCheckBox;
023import javax.swing.JLabel;
024import javax.swing.JOptionPane;
025import javax.swing.JPanel;
026import javax.swing.JRadioButton;
027import javax.swing.JSlider;
028
029import org.apache.commons.jcs3.access.exception.InvalidArgumentException;
030import org.openstreetmap.josm.actions.ExpertToggleAction;
031import org.openstreetmap.josm.data.gpx.GpxData;
032import org.openstreetmap.josm.data.gpx.IGpxLayerPrefs;
033import org.openstreetmap.josm.gui.MainApplication;
034import org.openstreetmap.josm.gui.layer.GpxLayer;
035import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper;
036import org.openstreetmap.josm.gui.layer.markerlayer.Marker;
037import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
038import org.openstreetmap.josm.gui.widgets.JosmComboBox;
039import org.openstreetmap.josm.gui.widgets.JosmTextField;
040import org.openstreetmap.josm.spi.preferences.Config;
041import org.openstreetmap.josm.tools.GBC;
042import org.openstreetmap.josm.tools.Logging;
043import org.openstreetmap.josm.tools.Utils;
044import org.openstreetmap.josm.tools.template_engine.ParseError;
045import org.openstreetmap.josm.tools.template_engine.TemplateParser;
046
047/**
048 * Panel for GPX settings.
049 */
050public class GPXSettingsPanel extends JPanel implements ValidationListener {
051
052    private static final int WAYPOINT_LABEL_CUSTOM = 6;
053    private static final String[] LABEL_PATTERN_TEMPLATE = {Marker.LABEL_PATTERN_AUTO, Marker.LABEL_PATTERN_NAME,
054        Marker.LABEL_PATTERN_DESC, "{special:everything}", "?{ '{name}' | '{desc}' | '{formattedWaypointOffset}' }", " "};
055    private static final String[] LABEL_PATTERN_DESC = {tr("Auto"), /* gpx data field name */ trc("gpx_field", "Name"),
056        /* gpx data field name */ trc("gpx_field", "Desc(ription)"), tr("Everything"), tr("Name or offset"), tr("None"), tr("Custom")};
057
058    private final JRadioButton drawRawGpsLinesGlobal = new JRadioButton(tr("Use global settings"));
059    private final JRadioButton drawRawGpsLinesAll = new JRadioButton(tr("All"));
060    private final JRadioButton drawRawGpsLinesLocal = new JRadioButton(tr("Local files"));
061    private final JRadioButton drawRawGpsLinesNone = new JRadioButton(tr("None"));
062    private transient ActionListener drawRawGpsLinesActionListener;
063    private final JosmTextField drawRawGpsMaxLineLength = new JosmTextField(8);
064    private final JosmTextField drawRawGpsMaxLineLengthLocal = new JosmTextField(8);
065    private final JosmTextField drawLineWidth = new JosmTextField(2);
066    private final JCheckBox forceRawGpsLines = new JCheckBox(tr("Force lines if no segments imported"));
067    private final JCheckBox largeGpsPoints = new JCheckBox(tr("Draw large GPS points"));
068    private final JCheckBox hdopCircleGpsPoints = new JCheckBox(tr("Draw a circle from HDOP value"));
069    private final JRadioButton colorTypeVelocity = new JRadioButton(tr("Velocity (red = slow, green = fast)"));
070    private final JRadioButton colorTypeDirection = new JRadioButton(tr("Direction (red = west, yellow = north, green = east, blue = south)"));
071    private final JRadioButton colorTypeDilution = new JRadioButton(tr("Dilution of Position (red = high, green = low, if available)"));
072    private final JRadioButton colorTypeQuality = new JRadioButton(tr("Quality (RTKLib only, if available)"));
073    private final JRadioButton colorTypeTime = new JRadioButton(tr("Track date"));
074    private final JRadioButton colorTypeHeatMap = new JRadioButton(tr("Heat Map (dark = few, bright = many)"));
075    private final JRadioButton colorTypeNone = new JRadioButton(tr("Single Color (can be customized in the layer manager)"));
076    private final JRadioButton colorTypeGlobal = new JRadioButton(tr("Use global settings"));
077    private final JosmComboBox<String> colorTypeVelocityTune = new JosmComboBox<>(new String[] {tr("Car"), tr("Bicycle"), tr("Foot")});
078    private final JosmComboBox<String> colorTypeHeatMapTune = new JosmComboBox<>(new String[] {
079        trc("Heat map", "User Normal"),
080        trc("Heat map", "User Light"),
081        trc("Heat map", "Traffic Lights"),
082        trc("Heat map", "Inferno"),
083        trc("Heat map", "Viridis"),
084        trc("Heat map", "Wood"),
085        trc("Heat map", "Heat")});
086    private final JCheckBox colorTypeHeatMapPoints = new JCheckBox(tr("Use points instead of lines for heat map"));
087    private final JSlider colorTypeHeatMapGain = new JSlider();
088    private final JSlider colorTypeHeatMapLowerLimit = new JSlider();
089    private final JCheckBox makeAutoMarkers = new JCheckBox(tr("Create markers when reading GPX"));
090    private final JCheckBox drawGpsArrows = new JCheckBox(tr("Draw Direction Arrows"));
091    private final JCheckBox drawGpsArrowsFast = new JCheckBox(tr("Fast drawing (looks uglier)"));
092    private final JosmTextField drawGpsArrowsMinDist = new JosmTextField(8);
093    private final JCheckBox colorDynamic = new JCheckBox(tr("Dynamic color range based on data limits"));
094    private final JosmComboBox<String> waypointLabel = new JosmComboBox<>(LABEL_PATTERN_DESC);
095    private final JosmTextField waypointLabelPattern = new JosmTextField();
096    private final JosmComboBox<String> audioWaypointLabel = new JosmComboBox<>(LABEL_PATTERN_DESC);
097    private final JosmTextField audioWaypointLabelPattern = new JosmTextField();
098    private final JCheckBox useGpsAntialiasing = new JCheckBox(tr("Smooth GPX graphics (antialiasing)"));
099    private final JCheckBox drawLineWithAlpha = new JCheckBox(tr("Draw with Opacity (alpha blending) "));
100
101    private final List<GpxLayer> layers;
102    private final GpxLayer firstLayer;
103    private final boolean global; // global settings vs. layer specific settings
104    private final boolean hasLocalFile; // flag to display LocalOnly checkbooks
105    private final boolean hasNonLocalFile; // flag to display AllLines checkbox
106
107    private static final Map<String, Object> DEFAULT_PREFS = getDefaultPrefs();
108
109    private static Map<String, Object> getDefaultPrefs() {
110        HashMap<String, Object> m = new HashMap<>();
111        m.put("colormode", -1);
112        m.put("colormode.dynamic-range", false);
113        m.put("colormode.heatmap.colormap", 0);
114        m.put("colormode.heatmap.gain", 0);
115        m.put("colormode.heatmap.line-extra", false); //Expert mode only
116        m.put("colormode.heatmap.lower-limit", 0);
117        m.put("colormode.heatmap.use-points", false);
118        m.put("colormode.time.min-distance", 60); //Expert mode only
119        m.put("colormode.velocity.tune", 45);
120        m.put("lines", -1);
121        m.put("lines.alpha-blend", false);
122        m.put("lines.arrows", false);
123        m.put("lines.arrows.fast", false);
124        m.put("lines.arrows.min-distance", 40);
125        m.put("lines.force", false);
126        m.put("lines.max-length", 200);
127        m.put("lines.max-length.local", -1);
128        m.put("lines.width", 0);
129        m.put("markers.color", "");
130        m.put("markers.show-text", true);
131        m.put("markers.pattern", Marker.LABEL_PATTERN_AUTO);
132        m.put("markers.audio.pattern", "?{ '{name}' | '{desc}' | '{" + Marker.MARKER_FORMATTED_OFFSET + "}' }");
133        m.put("points.hdopcircle", false);
134        m.put("points.large", false);
135        m.put("points.large.alpha", -1); //Expert mode only
136        m.put("points.large.size", 3); //Expert mode only
137        return Collections.unmodifiableMap(m);
138    }
139
140    /**
141     * Constructs a new {@code GPXSettingsPanel} for the given layers.
142     * @param layers the GPX layers
143     */
144    public GPXSettingsPanel(List<GpxLayer> layers) {
145        super(new GridBagLayout());
146        this.layers = layers;
147        if (Utils.isEmpty(layers)) {
148            throw new InvalidArgumentException("At least one layer required");
149        }
150        firstLayer = layers.get(0);
151        global = false;
152        hasLocalFile = layers.stream().anyMatch(l -> !l.data.fromServer);
153        hasNonLocalFile = layers.stream().anyMatch(l -> l.data.fromServer);
154        initComponents();
155        loadPreferences();
156    }
157
158    /**
159     * Constructs a new {@code GPXSettingsPanel}.
160     */
161    public GPXSettingsPanel() {
162        super(new GridBagLayout());
163        layers = null;
164        firstLayer = null;
165        global = hasLocalFile = hasNonLocalFile = true;
166        initComponents();
167        loadPreferences(); // preferences -> controls
168    }
169
170    /**
171     * Reads the preference for the given layer or the default preference if not available
172     * @param layer the GpxLayer. Can be <code>null</code>, default preference will be returned then
173     * @param key the drawing key to be read, without "draw.rawgps."
174     * @return the value
175     */
176    public static String getLayerPref(GpxLayer layer, String key) {
177        GpxData data = layer != null ? layer.data : null;
178        return getDataPref(data, key);
179    }
180
181    /**
182     * Reads the preference for the given layer or the default preference if not available
183     * @param data the data. Can be <code>null</code>, default preference will be returned then
184     * @param key the drawing key to be read, without "draw.rawgps."
185     * @return the value
186     * @since 18287
187     */
188    public static String getDataPref(IGpxLayerPrefs data, String key) {
189        Object d = DEFAULT_PREFS.get(key);
190        String ds;
191        if (d != null) {
192            ds = d.toString();
193        } else {
194            Logging.warn("No default value found for layer preference \"" + key + "\".");
195            ds = null;
196        }
197        return Optional.ofNullable(tryGetDataPrefLocal(data, key)).orElse(Config.getPref().get("draw.rawgps." + key, ds));
198    }
199
200    /**
201     * Reads the integer preference for the given layer or the default preference if not available
202     * @param layer the GpxLayer. Can be <code>null</code>, default preference will be returned then
203     * @param key the drawing key to be read, without "draw.rawgps."
204     * @return the integer value
205     */
206    public static int getLayerPrefInt(GpxLayer layer, String key) {
207        GpxData data = layer != null ? layer.data : null;
208        return getDataPrefInt(data, key);
209    }
210
211    /**
212     * Reads the integer preference for the given data or the default preference if not available
213     * @param data the data. Can be <code>null</code>, default preference will be returned then
214     * @param key the drawing key to be read, without "draw.rawgps."
215     * @return the integer value
216     * @since 18287
217     */
218    public static int getDataPrefInt(IGpxLayerPrefs data, String key) {
219        String s = getDataPref(data, key);
220        if (s != null) {
221            try {
222                return Integer.parseInt(s);
223            } catch (NumberFormatException ex) {
224                Object d = DEFAULT_PREFS.get(key);
225                if (d instanceof Integer) {
226                    return (int) d;
227                } else {
228                    Logging.warn("No valid default value found for layer preference \"" + key + "\".");
229                }
230            }
231        }
232        return 0;
233    }
234
235    /**
236     * Try to read the preference for the given layer
237     * @param layer the GpxLayer
238     * @param key the drawing key to be read, without "draw.rawgps."
239     * @return the value or <code>null</code> if not found
240     */
241    public static String tryGetLayerPrefLocal(GpxLayer layer, String key) {
242        return layer != null ? tryGetDataPrefLocal(layer.data, key) : null;
243    }
244
245    /**
246     * Try to read the preference for the given GpxData
247     * @param data the GpxData
248     * @param key the drawing key to be read, without "draw.rawgps."
249     * @return the value or <code>null</code> if not found
250     */
251    public static String tryGetDataPrefLocal(IGpxLayerPrefs data, String key) {
252        return data != null ? data.getLayerPrefs().get(key) : null;
253    }
254
255    /**
256     * Puts the preference for the given layers or the default preference if layers is <code>null</code>
257     * @param layers List of <code>GpxLayer</code> to put the drawingOptions
258     * @param key the drawing key to be written, without "draw.rawgps."
259     * @param value (can be <code>null</code> to remove option)
260     */
261    public static void putLayerPref(List<GpxLayer> layers, String key, Object value) {
262        String v = value == null ? null : value.toString();
263        if (layers != null) {
264            for (GpxLayer l : layers) {
265                putDataPrefLocal(l.data, key, v);
266            }
267        } else {
268            Config.getPref().put("draw.rawgps." + key, v);
269        }
270    }
271
272    /**
273     * Puts the preference for the given layer
274     * @param layer <code>GpxLayer</code> to put the drawingOptions
275     * @param key the drawing key to be written, without "draw.rawgps."
276     * @param value the value or <code>null</code> to remove key
277     */
278    public static void putLayerPrefLocal(GpxLayer layer, String key, String value) {
279        if (layer == null || layer.data == null) return;
280        putDataPrefLocal(layer.data, key, value);
281    }
282
283    /**
284     * Puts the preference for the given layer
285     * @param data <code>GpxData</code> to put the drawingOptions. Must not be <code>null</code>
286     * @param key the drawing key to be written, without "draw.rawgps."
287     * @param value the value or <code>null</code> to remove key
288     * @since 18287
289     */
290    public static void putDataPrefLocal(IGpxLayerPrefs data, String key, String value) {
291        if (data == null) return;
292        data.setModified(true);
293        if (Utils.isBlank(value) ||
294                (getLayerPref(null, key).equals(value) && DEFAULT_PREFS.get(key) != null && DEFAULT_PREFS.get(key).toString().equals(value))) {
295            data.getLayerPrefs().remove(key);
296        } else {
297            data.getLayerPrefs().put(key, value);
298        }
299    }
300
301    private String pref(String key) {
302        return getLayerPref(firstLayer, key);
303    }
304
305    private boolean prefBool(String key) {
306        return Boolean.parseBoolean(pref(key));
307    }
308
309    private int prefInt(String key) {
310        return getLayerPrefInt(firstLayer, key);
311    }
312
313    private int prefIntLocal(String key) {
314        try {
315            return Integer.parseInt(tryGetLayerPrefLocal(firstLayer, key));
316        } catch (NumberFormatException ex) {
317            return -1;
318        }
319
320    }
321
322    private void putPref(String key, Object value) {
323        putLayerPref(layers, key, value);
324    }
325
326    // CHECKSTYLE.OFF: ExecutableStatementCountCheck
327    private void initComponents() {
328        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
329
330        if (global) {
331            // makeAutoMarkers
332            makeAutoMarkers.setToolTipText(tr("Automatically make a marker layer from any waypoints when opening a GPX layer."));
333            ExpertToggleAction.addVisibilitySwitcher(makeAutoMarkers);
334            add(makeAutoMarkers, GBC.eol().insets(20, 0, 0, 5));
335        }
336
337        // drawRawGpsLines
338        ButtonGroup gpsLinesGroup = new ButtonGroup();
339        if (!global) {
340            gpsLinesGroup.add(drawRawGpsLinesGlobal);
341        }
342        gpsLinesGroup.add(drawRawGpsLinesNone);
343        gpsLinesGroup.add(drawRawGpsLinesLocal);
344        gpsLinesGroup.add(drawRawGpsLinesAll);
345
346        /* ensure that default is in data base */
347
348        JLabel label = new JLabel(tr("Draw lines between raw GPS points"));
349        add(label, GBC.eol().insets(20, 0, 0, 0));
350        if (!global) {
351            add(drawRawGpsLinesGlobal, GBC.eol().insets(40, 0, 0, 0));
352        }
353        add(drawRawGpsLinesNone, GBC.eol().insets(40, 0, 0, 0));
354        if (hasLocalFile) {
355            add(drawRawGpsLinesLocal, GBC.eol().insets(40, 0, 0, 0));
356        }
357        if (hasNonLocalFile) {
358            add(drawRawGpsLinesAll, GBC.eol().insets(40, 0, 0, 0));
359        }
360        ExpertToggleAction.addVisibilitySwitcher(label);
361        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesGlobal);
362        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesNone);
363        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesLocal);
364        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesAll);
365
366        drawRawGpsLinesActionListener = e -> {
367            boolean f = drawRawGpsLinesNone.isSelected() || drawRawGpsLinesGlobal.isSelected();
368            forceRawGpsLines.setEnabled(!f);
369            drawRawGpsMaxLineLength.setEnabled(!(f || drawRawGpsLinesLocal.isSelected()));
370            drawRawGpsMaxLineLengthLocal.setEnabled(!f);
371            drawGpsArrows.setEnabled(!f);
372            drawGpsArrowsFast.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
373            drawGpsArrowsMinDist.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
374        };
375
376        drawRawGpsLinesGlobal.addActionListener(drawRawGpsLinesActionListener);
377        drawRawGpsLinesNone.addActionListener(drawRawGpsLinesActionListener);
378        drawRawGpsLinesLocal.addActionListener(drawRawGpsLinesActionListener);
379        drawRawGpsLinesAll.addActionListener(drawRawGpsLinesActionListener);
380
381        // drawRawGpsMaxLineLengthLocal
382        drawRawGpsMaxLineLengthLocal.setToolTipText(
383                tr("Maximum length (in meters) to draw lines for local files. Set to ''-1'' to draw all lines."));
384        label = new JLabel(tr("Maximum length for local files (meters)"));
385        add(label, GBC.std().insets(40, 0, 0, 0));
386        add(drawRawGpsMaxLineLengthLocal, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
387        ExpertToggleAction.addVisibilitySwitcher(label);
388        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsMaxLineLengthLocal);
389
390        // drawRawGpsMaxLineLength
391        drawRawGpsMaxLineLength.setToolTipText(tr("Maximum length (in meters) to draw lines. Set to ''-1'' to draw all lines."));
392        label = new JLabel(tr("Maximum length (meters)"));
393        add(label, GBC.std().insets(40, 0, 0, 0));
394        add(drawRawGpsMaxLineLength, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
395        ExpertToggleAction.addVisibilitySwitcher(label);
396        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsMaxLineLength);
397
398        // forceRawGpsLines
399        forceRawGpsLines.setToolTipText(tr("Force drawing of lines if the imported data contain no line information."));
400        add(forceRawGpsLines, GBC.eop().insets(40, 0, 0, 0));
401        ExpertToggleAction.addVisibilitySwitcher(forceRawGpsLines);
402
403        // drawGpsArrows
404        drawGpsArrows.addActionListener(e -> {
405            drawGpsArrowsFast.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
406            drawGpsArrowsMinDist.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
407        });
408        drawGpsArrows.setToolTipText(tr("Draw direction arrows for lines, connecting GPS points."));
409        add(drawGpsArrows, GBC.eop().insets(20, 0, 0, 0));
410
411        // drawGpsArrowsFast
412        drawGpsArrowsFast.setToolTipText(tr("Draw the direction arrows using table lookups instead of complex math."));
413        add(drawGpsArrowsFast, GBC.eop().insets(40, 0, 0, 0));
414        ExpertToggleAction.addVisibilitySwitcher(drawGpsArrowsFast);
415
416        // drawGpsArrowsMinDist
417        drawGpsArrowsMinDist.setToolTipText(tr("Do not draw arrows if they are not at least this distance away from the last one."));
418        add(new JLabel(tr("Minimum distance (pixels)")), GBC.std().insets(40, 0, 0, 0));
419        add(drawGpsArrowsMinDist, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
420
421        // hdopCircleGpsPoints
422        hdopCircleGpsPoints.setToolTipText(tr("Draw a circle from HDOP value"));
423        add(hdopCircleGpsPoints, GBC.eop().insets(20, 0, 0, 0));
424        ExpertToggleAction.addVisibilitySwitcher(hdopCircleGpsPoints);
425
426        // largeGpsPoints
427        largeGpsPoints.setToolTipText(tr("Draw larger dots for the GPS points."));
428        add(largeGpsPoints, GBC.eop().insets(20, 0, 0, 0));
429
430        // drawLineWidth
431        drawLineWidth.setToolTipText(tr("Width of drawn GPX line (0 for default)"));
432        add(new JLabel(tr("Drawing width of GPX lines")), GBC.std().insets(20, 0, 0, 0));
433        add(drawLineWidth, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
434
435        // antialiasing
436        useGpsAntialiasing.setToolTipText(tr("Apply antialiasing to the GPX lines resulting in a smoother appearance."));
437        add(useGpsAntialiasing, GBC.eop().insets(20, 0, 0, 0));
438        ExpertToggleAction.addVisibilitySwitcher(useGpsAntialiasing);
439
440        // alpha blending
441        drawLineWithAlpha.setToolTipText(tr("Apply dynamic alpha-blending and adjust width based on zoom level for all GPX lines."));
442        add(drawLineWithAlpha, GBC.eop().insets(20, 0, 0, 0));
443        ExpertToggleAction.addVisibilitySwitcher(drawLineWithAlpha);
444
445        // colorTracks
446        ButtonGroup colorGroup = new ButtonGroup();
447        if (!global) {
448            colorGroup.add(colorTypeGlobal);
449        }
450        colorGroup.add(colorTypeNone);
451        colorGroup.add(colorTypeVelocity);
452        colorGroup.add(colorTypeDirection);
453        colorGroup.add(colorTypeDilution);
454        colorGroup.add(colorTypeQuality);
455        colorGroup.add(colorTypeTime);
456        colorGroup.add(colorTypeHeatMap);
457
458        colorTypeNone.setToolTipText(tr("All points and track segments will have their own color. Can be customized in Layer Manager."));
459        colorTypeVelocity.setToolTipText(tr("Colors points and track segments by velocity."));
460        colorTypeDirection.setToolTipText(tr("Colors points and track segments by direction."));
461        colorTypeDilution.setToolTipText(
462                tr("Colors points and track segments by dilution of position (HDOP). Your capture device needs to log that information."));
463        colorTypeQuality.setToolTipText(
464                tr("Colors points and track segments by RTKLib quality flag (Q). Your capture device needs to log that information."));
465        colorTypeTime.setToolTipText(tr("Colors points and track segments by its timestamp."));
466        colorTypeHeatMap.setToolTipText(tr("Collected points and track segments for a position and displayed as heat map."));
467
468        // color Tracks by Velocity Tune
469        colorTypeVelocityTune.setToolTipText(tr("Allows to tune the track coloring for different average speeds."));
470
471        colorTypeHeatMapTune.setToolTipText(tr("Selects the color schema for heat map."));
472        JLabel colorTypeHeatIconLabel = new JLabel();
473
474        add(Box.createVerticalGlue(), GBC.eol().insets(0, 20, 0, 0));
475
476        add(new JLabel(tr("Track and Point Coloring")), GBC.eol().insets(20, 0, 0, 0));
477        if (!global) {
478            add(colorTypeGlobal, GBC.eol().insets(40, 0, 0, 0));
479        }
480        add(colorTypeNone, GBC.eol().insets(40, 0, 0, 0));
481        add(colorTypeVelocity, GBC.std().insets(40, 0, 0, 0));
482        add(colorTypeVelocityTune, GBC.eop().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
483        add(colorTypeDirection, GBC.eol().insets(40, 0, 0, 0));
484        add(colorTypeDilution, GBC.eol().insets(40, 0, 0, 0));
485        add(colorTypeQuality, GBC.eol().insets(40, 0, 0, 0));
486        add(colorTypeTime, GBC.eol().insets(40, 0, 0, 0));
487        add(colorTypeHeatMap, GBC.std().insets(40, 0, 0, 0));
488        add(colorTypeHeatIconLabel, GBC.std().insets(5, 0, 0, 5));
489        add(colorTypeHeatMapTune, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
490
491        JLabel colorTypeHeatMapGainLabel = new JLabel(tr("Overlay gain adjustment"));
492        JLabel colorTypeHeatMapLowerLimitLabel = new JLabel(tr("Lower limit of visibility"));
493        add(colorTypeHeatMapGainLabel, GBC.std().insets(80, 0, 0, 0));
494        add(colorTypeHeatMapGain, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
495        add(colorTypeHeatMapLowerLimitLabel, GBC.std().insets(80, 0, 0, 0));
496        add(colorTypeHeatMapLowerLimit, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
497        add(colorTypeHeatMapPoints, GBC.eol().insets(60, 0, 0, 0));
498
499        colorTypeHeatMapGain.setToolTipText(tr("Adjust the gain of overlay blending."));
500        colorTypeHeatMapGain.setOrientation(JSlider.HORIZONTAL);
501        colorTypeHeatMapGain.setPaintLabels(true);
502        colorTypeHeatMapGain.setMinimum(-10);
503        colorTypeHeatMapGain.setMaximum(+10);
504        colorTypeHeatMapGain.setMinorTickSpacing(1);
505        colorTypeHeatMapGain.setMajorTickSpacing(5);
506
507        colorTypeHeatMapLowerLimit.setToolTipText(tr("Draw all GPX traces that exceed this threshold."));
508        colorTypeHeatMapLowerLimit.setOrientation(JSlider.HORIZONTAL);
509        colorTypeHeatMapLowerLimit.setMinimum(0);
510        colorTypeHeatMapLowerLimit.setMaximum(254);
511        colorTypeHeatMapLowerLimit.setPaintLabels(true);
512        colorTypeHeatMapLowerLimit.setMinorTickSpacing(10);
513        colorTypeHeatMapLowerLimit.setMajorTickSpacing(100);
514
515        colorTypeHeatMapPoints.setToolTipText(tr("Render engine uses points with simulated position error instead of lines. "));
516
517        // iterate over the buttons, add change listener to any change event
518        for (Enumeration<AbstractButton> button = colorGroup.getElements(); button.hasMoreElements();) {
519            button.nextElement().addChangeListener(e -> {
520                colorTypeVelocityTune.setEnabled(colorTypeVelocity.isSelected());
521                colorTypeHeatMapTune.setEnabled(colorTypeHeatMap.isSelected());
522                colorTypeHeatMapPoints.setEnabled(colorTypeHeatMap.isSelected());
523                colorTypeHeatMapGain.setEnabled(colorTypeHeatMap.isSelected());
524                colorTypeHeatMapLowerLimit.setEnabled(colorTypeHeatMap.isSelected());
525                colorTypeHeatMapGainLabel.setEnabled(colorTypeHeatMap.isSelected());
526                colorTypeHeatMapLowerLimitLabel.setEnabled(colorTypeHeatMap.isSelected());
527                colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected());
528            });
529        }
530
531        colorTypeHeatMapTune.addActionListener(e -> {
532            final Dimension dim = colorTypeHeatMapTune.getPreferredSize();
533            if (null != dim) {
534                // get image size of environment
535                final int iconSize = (int) dim.getHeight();
536                colorTypeHeatIconLabel.setIcon(GpxDrawHelper.getColorMapImageIcon(
537                        GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get(),
538                        colorTypeHeatMapTune.getSelectedIndex(),
539                        iconSize));
540            }
541        });
542
543        ExpertToggleAction.addVisibilitySwitcher(colorTypeDirection);
544        ExpertToggleAction.addVisibilitySwitcher(colorTypeDilution);
545        ExpertToggleAction.addVisibilitySwitcher(colorTypeQuality);
546        ExpertToggleAction.addVisibilitySwitcher(colorTypeHeatMapLowerLimit);
547        ExpertToggleAction.addVisibilitySwitcher(colorTypeHeatMapLowerLimitLabel);
548
549        colorDynamic.setToolTipText(tr("Colors points and track segments by data limits."));
550        add(colorDynamic, GBC.eop().insets(40, 0, 0, 0));
551        ExpertToggleAction.addVisibilitySwitcher(colorDynamic);
552
553        if (global) {
554            // Setting waypoints for gpx layer doesn't make sense - waypoints are shown in marker layer that has different name - so show
555            // this only for global config
556
557            // waypointLabel
558            label = new JLabel(tr("Waypoint labelling"));
559            add(label, GBC.std().insets(20, 0, 0, 0));
560            label.setLabelFor(waypointLabel);
561            add(waypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
562            waypointLabel.addActionListener(e -> updateWaypointPattern(waypointLabel, waypointLabelPattern));
563            add(waypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 5));
564            ExpertToggleAction.addVisibilitySwitcher(label);
565            ExpertToggleAction.addVisibilitySwitcher(waypointLabel);
566            ExpertToggleAction.addVisibilitySwitcher(waypointLabelPattern);
567
568            // audioWaypointLabel
569            Component glue = Box.createVerticalGlue();
570            add(glue, GBC.eol().insets(0, 20, 0, 0));
571            ExpertToggleAction.addVisibilitySwitcher(glue);
572
573            label = new JLabel(tr("Audio waypoint labelling"));
574            add(label, GBC.std().insets(20, 0, 0, 0));
575            label.setLabelFor(audioWaypointLabel);
576            add(audioWaypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
577            audioWaypointLabel.addActionListener(e -> updateWaypointPattern(audioWaypointLabel, audioWaypointLabelPattern));
578            add(audioWaypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 5));
579            ExpertToggleAction.addVisibilitySwitcher(label);
580            ExpertToggleAction.addVisibilitySwitcher(audioWaypointLabel);
581            ExpertToggleAction.addVisibilitySwitcher(audioWaypointLabelPattern);
582        }
583
584        add(Box.createVerticalGlue(), GBC.eol().fill(GBC.BOTH));
585    }
586    // CHECKSTYLE.ON: ExecutableStatementCountCheck
587
588    /**
589     * Loads preferences to UI controls
590     */
591    public final void loadPreferences() {
592        makeAutoMarkers.setSelected(Config.getPref().getBoolean("marker.makeautomarkers", true));
593        int lines = global ? prefInt("lines") : prefIntLocal("lines");
594        // -1 = global (default: all)
595        //  0 = none
596        //  1 = local
597        //  2 = all
598        if ((lines == 2 && hasNonLocalFile) || (lines == -1 && global)) {
599            drawRawGpsLinesAll.setSelected(true);
600        } else if (lines == 1 && hasLocalFile) {
601            drawRawGpsLinesLocal.setSelected(true);
602        } else if (lines == 0) {
603            drawRawGpsLinesNone.setSelected(true);
604        } else if (lines == -1) {
605            drawRawGpsLinesGlobal.setSelected(true);
606        } else {
607            Logging.warn("Unknown line type: " + lines);
608        }
609        drawRawGpsMaxLineLengthLocal.setText(pref("lines.max-length.local"));
610        drawRawGpsMaxLineLength.setText(pref("lines.max-length"));
611        drawLineWidth.setText(pref("lines.width"));
612        drawLineWithAlpha.setSelected(prefBool("lines.alpha-blend"));
613        forceRawGpsLines.setSelected(prefBool("lines.force"));
614        drawGpsArrows.setSelected(prefBool("lines.arrows"));
615        drawGpsArrowsFast.setSelected(prefBool("lines.arrows.fast"));
616        drawGpsArrowsMinDist.setText(pref("lines.arrows.min-distance"));
617        hdopCircleGpsPoints.setSelected(prefBool("points.hdopcircle"));
618        largeGpsPoints.setSelected(prefBool("points.large"));
619        useGpsAntialiasing.setSelected(Config.getPref().getBoolean("mappaint.gpx.use-antialiasing", false));
620
621        drawRawGpsLinesActionListener.actionPerformed(null);
622        if (!global && prefIntLocal("colormode") == -1) {
623            colorTypeGlobal.setSelected(true);
624            colorDynamic.setSelected(false);
625            colorDynamic.setEnabled(false);
626            colorTypeHeatMapPoints.setSelected(false);
627            colorTypeHeatMapGain.setValue(0);
628            colorTypeHeatMapLowerLimit.setValue(0);
629        } else {
630            int colorType = prefInt("colormode");
631            switch (colorType) {
632            case -1: case 0: colorTypeNone.setSelected(true); break;
633            case 1: colorTypeVelocity.setSelected(true); break;
634            case 2: colorTypeDilution.setSelected(true); break;
635            case 3: colorTypeDirection.setSelected(true); break;
636            case 4: colorTypeTime.setSelected(true); break;
637            case 5: colorTypeHeatMap.setSelected(true); break;
638            case 6: colorTypeQuality.setSelected(true); break;
639            default: Logging.warn("Unknown color type: " + colorType);
640            }
641            int ccts = prefInt("colormode.velocity.tune");
642            colorTypeVelocityTune.setSelectedIndex(ccts == 10 ? 2 : (ccts == 20 ? 1 : 0));
643            colorTypeHeatMapTune.setSelectedIndex(prefInt("colormode.heatmap.colormap"));
644            colorDynamic.setSelected(prefBool("colormode.dynamic-range"));
645            colorTypeHeatMapPoints.setSelected(prefBool("colormode.heatmap.use-points"));
646            colorTypeHeatMapGain.setValue(prefInt("colormode.heatmap.gain"));
647            colorTypeHeatMapLowerLimit.setValue(prefInt("colormode.heatmap.lower-limit"));
648        }
649        updateWaypointLabelCombobox(waypointLabel, waypointLabelPattern, pref("markers.pattern"));
650        updateWaypointLabelCombobox(audioWaypointLabel, audioWaypointLabelPattern, pref("markers.audio.pattern"));
651
652    }
653
654    /**
655     * Save preferences from UI controls, globally or for the specified layers.
656     * @return {@code true} when restart is required, {@code false} otherwise
657     */
658    public boolean savePreferences() {
659        if (global) {
660            Config.getPref().putBoolean("marker.makeautomarkers", makeAutoMarkers.isSelected());
661            putPref("markers.pattern", waypointLabelPattern.getText());
662            putPref("markers.audio.pattern", audioWaypointLabelPattern.getText());
663        }
664        boolean g;
665        if (!global && ((g = drawRawGpsLinesGlobal.isSelected()) || drawRawGpsLinesNone.isSelected())) {
666            if (g) {
667                putPref("lines", null);
668            } else {
669                putPref("lines", 0);
670            }
671            putPref("lines.max-length", null);
672            putPref("lines.max-length.local", null);
673            putPref("lines.force", null);
674            putPref("lines.arrows", null);
675            putPref("lines.arrows.fast", null);
676            putPref("lines.arrows.min-distance", null);
677        } else {
678            if (drawRawGpsLinesLocal.isSelected()) {
679                putPref("lines", 1);
680            } else if (drawRawGpsLinesAll.isSelected()) {
681                putPref("lines", 2);
682            }
683            putPref("lines.max-length", drawRawGpsMaxLineLength.getText());
684            putPref("lines.max-length.local", drawRawGpsMaxLineLengthLocal.getText());
685            putPref("lines.force", forceRawGpsLines.isSelected());
686            putPref("lines.arrows", drawGpsArrows.isSelected());
687            putPref("lines.arrows.fast", drawGpsArrowsFast.isSelected());
688            putPref("lines.arrows.min-distance", drawGpsArrowsMinDist.getText());
689        }
690
691        putPref("points.hdopcircle", hdopCircleGpsPoints.isSelected());
692        putPref("points.large", largeGpsPoints.isSelected());
693        putPref("lines.width", drawLineWidth.getText());
694        putPref("lines.alpha-blend", drawLineWithAlpha.isSelected());
695
696        Config.getPref().putBoolean("mappaint.gpx.use-antialiasing", useGpsAntialiasing.isSelected());
697
698        if (colorTypeGlobal.isSelected()) {
699            putPref("colormode", null);
700            putPref("colormode.dynamic-range", null);
701            putPref("colormode.velocity.tune", null);
702            return false;
703        } else if (colorTypeVelocity.isSelected()) {
704            putPref("colormode", 1);
705        } else if (colorTypeDilution.isSelected()) {
706            putPref("colormode", 2);
707        } else if (colorTypeDirection.isSelected()) {
708            putPref("colormode", 3);
709        } else if (colorTypeTime.isSelected()) {
710            putPref("colormode", 4);
711        } else if (colorTypeHeatMap.isSelected()) {
712            putPref("colormode", 5);
713        } else if (colorTypeQuality.isSelected()) {
714            putPref("colormode", 6);
715        } else {
716            putPref("colormode", 0);
717        }
718        putPref("colormode.dynamic-range", colorDynamic.isSelected());
719        int ccti = colorTypeVelocityTune.getSelectedIndex();
720        putPref("colormode.velocity.tune", ccti == 2 ? 10 : (ccti == 1 ? 20 : 45));
721        putPref("colormode.heatmap.colormap", colorTypeHeatMapTune.getSelectedIndex());
722        putPref("colormode.heatmap.use-points", colorTypeHeatMapPoints.isSelected());
723        putPref("colormode.heatmap.gain", colorTypeHeatMapGain.getValue());
724        putPref("colormode.heatmap.lower-limit", colorTypeHeatMapLowerLimit.getValue());
725
726        if (!global && !Utils.isEmpty(layers)) {
727            layers.forEach(l -> l.data.invalidate());
728        }
729
730        return false;
731    }
732
733    private static void updateWaypointLabelCombobox(JosmComboBox<String> cb, JosmTextField tf, String labelPattern) {
734        boolean found = false;
735        for (int i = 0; i < LABEL_PATTERN_TEMPLATE.length; i++) {
736            if (LABEL_PATTERN_TEMPLATE[i].equals(labelPattern)) {
737                cb.setSelectedIndex(i);
738                found = true;
739                break;
740            }
741        }
742        if (!found) {
743            cb.setSelectedIndex(WAYPOINT_LABEL_CUSTOM);
744            tf.setEnabled(true);
745            tf.setText(labelPattern);
746        }
747    }
748
749    private static void updateWaypointPattern(JosmComboBox<String> cb, JosmTextField tf) {
750        if (cb.getSelectedIndex() == WAYPOINT_LABEL_CUSTOM) {
751            tf.setEnabled(true);
752        } else {
753            tf.setEnabled(false);
754            tf.setText(LABEL_PATTERN_TEMPLATE[cb.getSelectedIndex()]);
755        }
756    }
757
758    @Override
759    public boolean validatePreferences() {
760        TemplateParser parser = new TemplateParser(waypointLabelPattern.getText());
761        try {
762            parser.parse();
763        } catch (ParseError e) {
764            Logging.warn(e);
765            JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
766                    tr("Incorrect waypoint label pattern: {0}", e.getMessage()), tr("Incorrect pattern"), JOptionPane.ERROR_MESSAGE);
767            waypointLabelPattern.requestFocus();
768            return false;
769        }
770        parser = new TemplateParser(audioWaypointLabelPattern.getText());
771        try {
772            parser.parse();
773        } catch (ParseError e) {
774            Logging.warn(e);
775            JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
776                    tr("Incorrect audio waypoint label pattern: {0}", e.getMessage()), tr("Incorrect pattern"), JOptionPane.ERROR_MESSAGE);
777            audioWaypointLabelPattern.requestFocus();
778            return false;
779        }
780        return true;
781    }
782}