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.jcs.access.exception.InvalidArgumentException;
030import org.openstreetmap.josm.actions.ExpertToggleAction;
031import org.openstreetmap.josm.data.gpx.GpxData;
032import org.openstreetmap.josm.gui.MainApplication;
033import org.openstreetmap.josm.gui.layer.GpxLayer;
034import org.openstreetmap.josm.gui.layer.gpx.GpxDrawHelper;
035import org.openstreetmap.josm.gui.layer.markerlayer.Marker;
036import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
037import org.openstreetmap.josm.gui.widgets.JosmComboBox;
038import org.openstreetmap.josm.gui.widgets.JosmTextField;
039import org.openstreetmap.josm.spi.preferences.Config;
040import org.openstreetmap.josm.tools.GBC;
041import org.openstreetmap.josm.tools.Logging;
042import org.openstreetmap.josm.tools.template_engine.ParseError;
043import org.openstreetmap.josm.tools.template_engine.TemplateParser;
044
045/**
046 * Panel for GPX settings.
047 */
048public class GPXSettingsPanel extends JPanel implements ValidationListener {
049
050    private static final int WAYPOINT_LABEL_CUSTOM = 6;
051    private static final String[] LABEL_PATTERN_TEMPLATE = new String[] {Marker.LABEL_PATTERN_AUTO, Marker.LABEL_PATTERN_NAME,
052        Marker.LABEL_PATTERN_DESC, "{special:everything}", "?{ '{name}' | '{desc}' | '{formattedWaypointOffset}' }", " "};
053    private static final String[] LABEL_PATTERN_DESC = new String[] {tr("Auto"), /* gpx data field name */ trc("gpx_field", "Name"),
054        /* gpx data field name */ trc("gpx_field", "Desc(ription)"), tr("Everything"), tr("Name or offset"), tr("None"), tr("Custom")};
055
056
057    private final JRadioButton drawRawGpsLinesGlobal = new JRadioButton(tr("Use global settings"));
058    private final JRadioButton drawRawGpsLinesAll = new JRadioButton(tr("All"));
059    private final JRadioButton drawRawGpsLinesLocal = new JRadioButton(tr("Local files"));
060    private final JRadioButton drawRawGpsLinesNone = new JRadioButton(tr("None"));
061    private transient ActionListener drawRawGpsLinesActionListener;
062    private final JosmTextField drawRawGpsMaxLineLength = new JosmTextField(8);
063    private final JosmTextField drawRawGpsMaxLineLengthLocal = new JosmTextField(8);
064    private final JosmTextField drawLineWidth = new JosmTextField(2);
065    private final JCheckBox forceRawGpsLines = new JCheckBox(tr("Force lines if no segments imported"));
066    private final JCheckBox largeGpsPoints = new JCheckBox(tr("Draw large GPS points"));
067    private final JCheckBox hdopCircleGpsPoints = new JCheckBox(tr("Draw a circle from HDOP value"));
068    private final JRadioButton colorTypeVelocity = new JRadioButton(tr("Velocity (red = slow, green = fast)"));
069    private final JRadioButton colorTypeDirection = new JRadioButton(tr("Direction (red = west, yellow = north, green = east, blue = south)"));
070    private final JRadioButton colorTypeDilution = new JRadioButton(tr("Dilution of Position (red = high, green = low, if available)"));
071    private final JRadioButton colorTypeQuality = new JRadioButton(tr("Quality (RTKLib only, if available)"));
072    private final JRadioButton colorTypeTime = new JRadioButton(tr("Track date"));
073    private final JRadioButton colorTypeHeatMap = new JRadioButton(tr("Heat Map (dark = few, bright = many)"));
074    private final JRadioButton colorTypeNone = new JRadioButton(tr("Single Color (can be customized in the layer manager)"));
075    private final JRadioButton colorTypeGlobal = new JRadioButton(tr("Use global settings"));
076    private final JosmComboBox<String> colorTypeVelocityTune = new JosmComboBox<>(new String[] {tr("Car"), tr("Bicycle"), tr("Foot")});
077    private final JosmComboBox<String> colorTypeHeatMapTune = new JosmComboBox<>(new String[] {
078        trc("Heat map", "User Normal"),
079        trc("Heat map", "User Light"),
080        trc("Heat map", "Traffic Lights"),
081        trc("Heat map", "Inferno"),
082        trc("Heat map", "Viridis"),
083        trc("Heat map", "Wood"),
084        trc("Heat map", "Heat")});
085    private final JCheckBox colorTypeHeatMapPoints = new JCheckBox(tr("Use points instead of lines for heat map"));
086    private final JSlider colorTypeHeatMapGain = new JSlider();
087    private final JSlider colorTypeHeatMapLowerLimit = new JSlider();
088    private final JCheckBox makeAutoMarkers = new JCheckBox(tr("Create markers when reading GPX"));
089    private final JCheckBox drawGpsArrows = new JCheckBox(tr("Draw Direction Arrows"));
090    private final JCheckBox drawGpsArrowsFast = new JCheckBox(tr("Fast drawing (looks uglier)"));
091    private final JosmTextField drawGpsArrowsMinDist = new JosmTextField(8);
092    private final JCheckBox colorDynamic = new JCheckBox(tr("Dynamic color range based on data limits"));
093    private final JosmComboBox<String> waypointLabel = new JosmComboBox<>(LABEL_PATTERN_DESC);
094    private final JosmTextField waypointLabelPattern = new JosmTextField();
095    private final JosmComboBox<String> audioWaypointLabel = new JosmComboBox<>(LABEL_PATTERN_DESC);
096    private final JosmTextField audioWaypointLabelPattern = new JosmTextField();
097    private final JCheckBox useGpsAntialiasing = new JCheckBox(tr("Smooth GPX graphics (antialiasing)"));
098    private final JCheckBox drawLineWithAlpha = new JCheckBox(tr("Draw with Opacity (alpha blending) "));
099
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 static final 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) {
144        super(new GridBagLayout());
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());
153        initComponents();
154        loadPreferences();
155    }
156
157    /**
158     * Constructs a new {@code GPXSettingsPanel}.
159     */
160    public GPXSettingsPanel() {
161        super(new GridBagLayout());
162        layers = null;
163        firstLayer = null;
164        global = hasLocalFile = hasNonLocalFile = true;
165        initComponents();
166        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() ||
266                (getLayerPref(null, key).equals(value) && DEFAULT_PREFS.get(key) != null && DEFAULT_PREFS.get(key).toString().equals(value))) {
267            data.getLayerPrefs().remove(key);
268        } else {
269            data.getLayerPrefs().put(key, value);
270        }
271    }
272
273    private String pref(String key) {
274        return getLayerPref(firstLayer, key);
275    }
276
277    private boolean prefBool(String key) {
278        return Boolean.parseBoolean(pref(key));
279    }
280
281    private int prefInt(String key) {
282        return getLayerPrefInt(firstLayer, key);
283    }
284
285    private int prefIntLocal(String key) {
286        try {
287            return Integer.parseInt(tryGetLayerPrefLocal(firstLayer, key));
288        } catch (NumberFormatException ex) {
289            return -1;
290        }
291
292    }
293
294    private void putPref(String key, Object value) {
295        putLayerPref(layers, key, value);
296    }
297
298    // CHECKSTYLE.OFF: ExecutableStatementCountCheck
299    private void initComponents() {
300        setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
301
302        if (global) {
303            // makeAutoMarkers
304            makeAutoMarkers.setToolTipText(tr("Automatically make a marker layer from any waypoints when opening a GPX layer."));
305            ExpertToggleAction.addVisibilitySwitcher(makeAutoMarkers);
306            add(makeAutoMarkers, GBC.eol().insets(20, 0, 0, 5));
307        }
308
309        // drawRawGpsLines
310        ButtonGroup gpsLinesGroup = new ButtonGroup();
311        if (!global) {
312            gpsLinesGroup.add(drawRawGpsLinesGlobal);
313        }
314        gpsLinesGroup.add(drawRawGpsLinesNone);
315        gpsLinesGroup.add(drawRawGpsLinesLocal);
316        gpsLinesGroup.add(drawRawGpsLinesAll);
317
318        /* ensure that default is in data base */
319
320        JLabel label = new JLabel(tr("Draw lines between raw GPS points"));
321        add(label, GBC.eol().insets(20, 0, 0, 0));
322        if (!global) {
323            add(drawRawGpsLinesGlobal, GBC.eol().insets(40, 0, 0, 0));
324        }
325        add(drawRawGpsLinesNone, GBC.eol().insets(40, 0, 0, 0));
326        if (hasLocalFile) {
327            add(drawRawGpsLinesLocal, GBC.eol().insets(40, 0, 0, 0));
328        }
329        if (hasNonLocalFile) {
330            add(drawRawGpsLinesAll, GBC.eol().insets(40, 0, 0, 0));
331        }
332        ExpertToggleAction.addVisibilitySwitcher(label);
333        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesGlobal);
334        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesNone);
335        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesLocal);
336        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsLinesAll);
337
338        drawRawGpsLinesActionListener = e -> {
339            boolean f = drawRawGpsLinesNone.isSelected() || drawRawGpsLinesGlobal.isSelected();
340            forceRawGpsLines.setEnabled(!f);
341            drawRawGpsMaxLineLength.setEnabled(!(f || drawRawGpsLinesLocal.isSelected()));
342            drawRawGpsMaxLineLengthLocal.setEnabled(!f);
343            drawGpsArrows.setEnabled(!f);
344            drawGpsArrowsFast.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
345            drawGpsArrowsMinDist.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
346        };
347
348        drawRawGpsLinesGlobal.addActionListener(drawRawGpsLinesActionListener);
349        drawRawGpsLinesNone.addActionListener(drawRawGpsLinesActionListener);
350        drawRawGpsLinesLocal.addActionListener(drawRawGpsLinesActionListener);
351        drawRawGpsLinesAll.addActionListener(drawRawGpsLinesActionListener);
352
353        // drawRawGpsMaxLineLengthLocal
354        drawRawGpsMaxLineLengthLocal.setToolTipText(
355                tr("Maximum length (in meters) to draw lines for local files. Set to ''-1'' to draw all lines."));
356        label = new JLabel(tr("Maximum length for local files (meters)"));
357        add(label, GBC.std().insets(40, 0, 0, 0));
358        add(drawRawGpsMaxLineLengthLocal, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
359        ExpertToggleAction.addVisibilitySwitcher(label);
360        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsMaxLineLengthLocal);
361
362        // drawRawGpsMaxLineLength
363        drawRawGpsMaxLineLength.setToolTipText(tr("Maximum length (in meters) to draw lines. Set to ''-1'' to draw all lines."));
364        label = new JLabel(tr("Maximum length (meters)"));
365        add(label, GBC.std().insets(40, 0, 0, 0));
366        add(drawRawGpsMaxLineLength, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
367        ExpertToggleAction.addVisibilitySwitcher(label);
368        ExpertToggleAction.addVisibilitySwitcher(drawRawGpsMaxLineLength);
369
370        // forceRawGpsLines
371        forceRawGpsLines.setToolTipText(tr("Force drawing of lines if the imported data contain no line information."));
372        add(forceRawGpsLines, GBC.eop().insets(40, 0, 0, 0));
373        ExpertToggleAction.addVisibilitySwitcher(forceRawGpsLines);
374
375        // drawGpsArrows
376        drawGpsArrows.addActionListener(e -> {
377            drawGpsArrowsFast.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
378            drawGpsArrowsMinDist.setEnabled(drawGpsArrows.isSelected() && drawGpsArrows.isEnabled());
379        });
380        drawGpsArrows.setToolTipText(tr("Draw direction arrows for lines, connecting GPS points."));
381        add(drawGpsArrows, GBC.eop().insets(20, 0, 0, 0));
382
383        // drawGpsArrowsFast
384        drawGpsArrowsFast.setToolTipText(tr("Draw the direction arrows using table lookups instead of complex math."));
385        add(drawGpsArrowsFast, GBC.eop().insets(40, 0, 0, 0));
386        ExpertToggleAction.addVisibilitySwitcher(drawGpsArrowsFast);
387
388        // drawGpsArrowsMinDist
389        drawGpsArrowsMinDist.setToolTipText(tr("Do not draw arrows if they are not at least this distance away from the last one."));
390        add(new JLabel(tr("Minimum distance (pixels)")), GBC.std().insets(40, 0, 0, 0));
391        add(drawGpsArrowsMinDist, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
392
393        // hdopCircleGpsPoints
394        hdopCircleGpsPoints.setToolTipText(tr("Draw a circle from HDOP value"));
395        add(hdopCircleGpsPoints, GBC.eop().insets(20, 0, 0, 0));
396        ExpertToggleAction.addVisibilitySwitcher(hdopCircleGpsPoints);
397
398        // largeGpsPoints
399        largeGpsPoints.setToolTipText(tr("Draw larger dots for the GPS points."));
400        add(largeGpsPoints, GBC.eop().insets(20, 0, 0, 0));
401
402        // drawLineWidth
403        drawLineWidth.setToolTipText(tr("Width of drawn GPX line (0 for default)"));
404        add(new JLabel(tr("Drawing width of GPX lines")), GBC.std().insets(20, 0, 0, 0));
405        add(drawLineWidth, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
406
407        // antialiasing
408        useGpsAntialiasing.setToolTipText(tr("Apply antialiasing to the GPX lines resulting in a smoother appearance."));
409        add(useGpsAntialiasing, GBC.eop().insets(20, 0, 0, 0));
410        ExpertToggleAction.addVisibilitySwitcher(useGpsAntialiasing);
411
412        // alpha blending
413        drawLineWithAlpha.setToolTipText(tr("Apply dynamic alpha-blending and adjust width based on zoom level for all GPX lines."));
414        add(drawLineWithAlpha, GBC.eop().insets(20, 0, 0, 0));
415        ExpertToggleAction.addVisibilitySwitcher(drawLineWithAlpha);
416
417        // colorTracks
418        ButtonGroup colorGroup = new ButtonGroup();
419        if (!global) {
420            colorGroup.add(colorTypeGlobal);
421        }
422        colorGroup.add(colorTypeNone);
423        colorGroup.add(colorTypeVelocity);
424        colorGroup.add(colorTypeDirection);
425        colorGroup.add(colorTypeDilution);
426        colorGroup.add(colorTypeQuality);
427        colorGroup.add(colorTypeTime);
428        colorGroup.add(colorTypeHeatMap);
429
430        colorTypeNone.setToolTipText(tr("All points and track segments will have their own color. Can be customized in Layer Manager."));
431        colorTypeVelocity.setToolTipText(tr("Colors points and track segments by velocity."));
432        colorTypeDirection.setToolTipText(tr("Colors points and track segments by direction."));
433        colorTypeDilution.setToolTipText(
434                tr("Colors points and track segments by dilution of position (HDOP). Your capture device needs to log that information."));
435        colorTypeQuality.setToolTipText(
436                tr("Colors points and track segments by RTKLib quality flag (Q). Your capture device needs to log that information."));
437        colorTypeTime.setToolTipText(tr("Colors points and track segments by its timestamp."));
438        colorTypeHeatMap.setToolTipText(tr("Collected points and track segments for a position and displayed as heat map."));
439
440        // color Tracks by Velocity Tune
441        colorTypeVelocityTune.setToolTipText(tr("Allows to tune the track coloring for different average speeds."));
442
443        colorTypeHeatMapTune.setToolTipText(tr("Selects the color schema for heat map."));
444        JLabel colorTypeHeatIconLabel = new JLabel();
445
446        add(Box.createVerticalGlue(), GBC.eol().insets(0, 20, 0, 0));
447
448        add(new JLabel(tr("Track and Point Coloring")), GBC.eol().insets(20, 0, 0, 0));
449        if (!global) {
450            add(colorTypeGlobal, GBC.eol().insets(40, 0, 0, 0));
451        }
452        add(colorTypeNone, GBC.eol().insets(40, 0, 0, 0));
453        add(colorTypeVelocity, GBC.std().insets(40, 0, 0, 0));
454        add(colorTypeVelocityTune, GBC.eop().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
455        add(colorTypeDirection, GBC.eol().insets(40, 0, 0, 0));
456        add(colorTypeDilution, GBC.eol().insets(40, 0, 0, 0));
457        add(colorTypeQuality, GBC.eol().insets(40, 0, 0, 0));
458        add(colorTypeTime, GBC.eol().insets(40, 0, 0, 0));
459        add(colorTypeHeatMap, GBC.std().insets(40, 0, 0, 0));
460        add(colorTypeHeatIconLabel, GBC.std().insets(5, 0, 0, 5));
461        add(colorTypeHeatMapTune, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
462
463        JLabel colorTypeHeatMapGainLabel = new JLabel(tr("Overlay gain adjustment"));
464        JLabel colorTypeHeatMapLowerLimitLabel = new JLabel(tr("Lower limit of visibility"));
465        add(colorTypeHeatMapGainLabel, GBC.std().insets(80, 0, 0, 0));
466        add(colorTypeHeatMapGain, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
467        add(colorTypeHeatMapLowerLimitLabel, GBC.std().insets(80, 0, 0, 0));
468        add(colorTypeHeatMapLowerLimit, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
469        add(colorTypeHeatMapPoints, GBC.eol().insets(60, 0, 0, 0));
470
471        colorTypeHeatMapGain.setToolTipText(tr("Adjust the gain of overlay blending."));
472        colorTypeHeatMapGain.setOrientation(JSlider.HORIZONTAL);
473        colorTypeHeatMapGain.setPaintLabels(true);
474        colorTypeHeatMapGain.setMinimum(-10);
475        colorTypeHeatMapGain.setMaximum(+10);
476        colorTypeHeatMapGain.setMinorTickSpacing(1);
477        colorTypeHeatMapGain.setMajorTickSpacing(5);
478
479        colorTypeHeatMapLowerLimit.setToolTipText(tr("Draw all GPX traces that exceed this threshold."));
480        colorTypeHeatMapLowerLimit.setOrientation(JSlider.HORIZONTAL);
481        colorTypeHeatMapLowerLimit.setMinimum(0);
482        colorTypeHeatMapLowerLimit.setMaximum(254);
483        colorTypeHeatMapLowerLimit.setPaintLabels(true);
484        colorTypeHeatMapLowerLimit.setMinorTickSpacing(10);
485        colorTypeHeatMapLowerLimit.setMajorTickSpacing(100);
486
487        colorTypeHeatMapPoints.setToolTipText(tr("Render engine uses points with simulated position error instead of lines. "));
488
489        // iterate over the buttons, add change listener to any change event
490        for (Enumeration<AbstractButton> button = colorGroup.getElements(); button.hasMoreElements();) {
491            (button.nextElement()).addChangeListener(e -> {
492                colorTypeVelocityTune.setEnabled(colorTypeVelocity.isSelected());
493                colorTypeHeatMapTune.setEnabled(colorTypeHeatMap.isSelected());
494                colorTypeHeatMapPoints.setEnabled(colorTypeHeatMap.isSelected());
495                colorTypeHeatMapGain.setEnabled(colorTypeHeatMap.isSelected());
496                colorTypeHeatMapLowerLimit.setEnabled(colorTypeHeatMap.isSelected());
497                colorTypeHeatMapGainLabel.setEnabled(colorTypeHeatMap.isSelected());
498                colorTypeHeatMapLowerLimitLabel.setEnabled(colorTypeHeatMap.isSelected());
499                colorDynamic.setEnabled(colorTypeVelocity.isSelected() || colorTypeDilution.isSelected());
500            });
501        }
502
503        colorTypeHeatMapTune.addActionListener(e -> {
504            final Dimension dim = colorTypeHeatMapTune.getPreferredSize();
505            if (null != dim) {
506                // get image size of environment
507                final int iconSize = (int) dim.getHeight();
508                colorTypeHeatIconLabel.setIcon(GpxDrawHelper.getColorMapImageIcon(
509                        GpxDrawHelper.DEFAULT_COLOR_PROPERTY.get(),
510                        colorTypeHeatMapTune.getSelectedIndex(),
511                        iconSize));
512            }
513        });
514
515        ExpertToggleAction.addVisibilitySwitcher(colorTypeDirection);
516        ExpertToggleAction.addVisibilitySwitcher(colorTypeDilution);
517        ExpertToggleAction.addVisibilitySwitcher(colorTypeQuality);
518        ExpertToggleAction.addVisibilitySwitcher(colorTypeHeatMapLowerLimit);
519        ExpertToggleAction.addVisibilitySwitcher(colorTypeHeatMapLowerLimitLabel);
520
521        colorDynamic.setToolTipText(tr("Colors points and track segments by data limits."));
522        add(colorDynamic, GBC.eop().insets(40, 0, 0, 0));
523        ExpertToggleAction.addVisibilitySwitcher(colorDynamic);
524
525        if (global) {
526            // Setting waypoints for gpx layer doesn't make sense - waypoints are shown in marker layer that has different name - so show
527            // this only for global config
528
529            // waypointLabel
530            label = new JLabel(tr("Waypoint labelling"));
531            add(label, GBC.std().insets(20, 0, 0, 0));
532            label.setLabelFor(waypointLabel);
533            add(waypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
534            waypointLabel.addActionListener(e -> updateWaypointPattern(waypointLabel, waypointLabelPattern));
535            add(waypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 5));
536            ExpertToggleAction.addVisibilitySwitcher(label);
537            ExpertToggleAction.addVisibilitySwitcher(waypointLabel);
538            ExpertToggleAction.addVisibilitySwitcher(waypointLabelPattern);
539
540            // audioWaypointLabel
541            Component glue = Box.createVerticalGlue();
542            add(glue, GBC.eol().insets(0, 20, 0, 0));
543            ExpertToggleAction.addVisibilitySwitcher(glue);
544
545            label = new JLabel(tr("Audio waypoint labelling"));
546            add(label, GBC.std().insets(20, 0, 0, 0));
547            label.setLabelFor(audioWaypointLabel);
548            add(audioWaypointLabel, GBC.eol().fill(GBC.HORIZONTAL).insets(5, 0, 0, 5));
549            audioWaypointLabel.addActionListener(e -> updateWaypointPattern(audioWaypointLabel, audioWaypointLabelPattern));
550            add(audioWaypointLabelPattern, GBC.eol().fill(GBC.HORIZONTAL).insets(20, 0, 0, 5));
551            ExpertToggleAction.addVisibilitySwitcher(label);
552            ExpertToggleAction.addVisibilitySwitcher(audioWaypointLabel);
553            ExpertToggleAction.addVisibilitySwitcher(audioWaypointLabelPattern);
554        }
555
556        add(Box.createVerticalGlue(), GBC.eol().fill(GBC.BOTH));
557    }
558    // CHECKSTYLE.ON: ExecutableStatementCountCheck
559
560    /**
561     * Loads preferences to UI controls
562     */
563    public final void loadPreferences() {
564        makeAutoMarkers.setSelected(Config.getPref().getBoolean("marker.makeautomarkers", true));
565        int lines = global ? prefInt("lines") : prefIntLocal("lines");
566        if (lines == 2 && hasNonLocalFile) {
567            drawRawGpsLinesAll.setSelected(true);
568        } else if ((lines == 1 && hasLocalFile) || (lines == -1 && global)) {
569            drawRawGpsLinesLocal.setSelected(true);
570        } else if (lines == 0) {
571            drawRawGpsLinesNone.setSelected(true);
572        } else if (lines == -1) {
573            drawRawGpsLinesGlobal.setSelected(true);
574        } else {
575            Logging.warn("Unknown line type: " + lines);
576        }
577        drawRawGpsMaxLineLengthLocal.setText(pref("lines.max-length.local"));
578        drawRawGpsMaxLineLength.setText(pref("lines.max-length"));
579        drawLineWidth.setText(pref("lines.width"));
580        drawLineWithAlpha.setSelected(prefBool("lines.alpha-blend"));
581        forceRawGpsLines.setSelected(prefBool("lines.force"));
582        drawGpsArrows.setSelected(prefBool("lines.arrows"));
583        drawGpsArrowsFast.setSelected(prefBool("lines.arrows.fast"));
584        drawGpsArrowsMinDist.setText(pref("lines.arrows.min-distance"));
585        hdopCircleGpsPoints.setSelected(prefBool("points.hdopcircle"));
586        largeGpsPoints.setSelected(prefBool("points.large"));
587        useGpsAntialiasing.setSelected(Config.getPref().getBoolean("mappaint.gpx.use-antialiasing", false));
588
589        drawRawGpsLinesActionListener.actionPerformed(null);
590        if (!global && prefIntLocal("colormode") == -1) {
591            colorTypeGlobal.setSelected(true);
592            colorDynamic.setSelected(false);
593            colorDynamic.setEnabled(false);
594            colorTypeHeatMapPoints.setSelected(false);
595            colorTypeHeatMapGain.setValue(0);
596            colorTypeHeatMapLowerLimit.setValue(0);
597        } else {
598            int colorType = prefInt("colormode");
599            switch (colorType) {
600            case -1: case 0: colorTypeNone.setSelected(true); break;
601            case 1: colorTypeVelocity.setSelected(true); break;
602            case 2: colorTypeDilution.setSelected(true); break;
603            case 3: colorTypeDirection.setSelected(true); break;
604            case 4: colorTypeTime.setSelected(true); break;
605            case 5: colorTypeHeatMap.setSelected(true); break;
606            case 6: colorTypeQuality.setSelected(true); break;
607            default: Logging.warn("Unknown color type: " + colorType);
608            }
609            int ccts = prefInt("colormode.velocity.tune");
610            colorTypeVelocityTune.setSelectedIndex(ccts == 10 ? 2 : (ccts == 20 ? 1 : 0));
611            colorTypeHeatMapTune.setSelectedIndex(prefInt("colormode.heatmap.colormap"));
612            colorDynamic.setSelected(prefBool("colormode.dynamic-range"));
613            colorTypeHeatMapPoints.setSelected(prefBool("colormode.heatmap.use-points"));
614            colorTypeHeatMapGain.setValue(prefInt("colormode.heatmap.gain"));
615            colorTypeHeatMapLowerLimit.setValue(prefInt("colormode.heatmap.lower-limit"));
616        }
617        updateWaypointLabelCombobox(waypointLabel, waypointLabelPattern, pref("markers.pattern"));
618        updateWaypointLabelCombobox(audioWaypointLabel, audioWaypointLabelPattern, pref("markers.audio.pattern"));
619
620    }
621
622    /**
623     * Save preferences from UI controls, globally or for the specified layers.
624     * @return {@code true} when restart is required, {@code false} otherwise
625     */
626    public boolean savePreferences() {
627        if (global) {
628            Config.getPref().putBoolean("marker.makeautomarkers", makeAutoMarkers.isSelected());
629            putPref("markers.pattern", waypointLabelPattern.getText());
630            putPref("markers.audio.pattern", audioWaypointLabelPattern.getText());
631        }
632        boolean g;
633        if (!global && ((g = drawRawGpsLinesGlobal.isSelected()) || drawRawGpsLinesNone.isSelected())) {
634            if (g) {
635                putPref("lines", null);
636            } else {
637                putPref("lines", 0);
638            }
639            putPref("lines.max-length", null);
640            putPref("lines.max-length.local", null);
641            putPref("lines.force", null);
642            putPref("lines.arrows", null);
643            putPref("lines.arrows.fast", null);
644            putPref("lines.arrows.min-distance", null);
645        } else {
646            if (drawRawGpsLinesLocal.isSelected()) {
647                putPref("lines", 1);
648            } else if (drawRawGpsLinesAll.isSelected()) {
649                putPref("lines", 2);
650            }
651            putPref("lines.max-length", drawRawGpsMaxLineLength.getText());
652            putPref("lines.max-length.local", drawRawGpsMaxLineLengthLocal.getText());
653            putPref("lines.force", forceRawGpsLines.isSelected());
654            putPref("lines.arrows", drawGpsArrows.isSelected());
655            putPref("lines.arrows.fast", drawGpsArrowsFast.isSelected());
656            putPref("lines.arrows.min-distance", drawGpsArrowsMinDist.getText());
657        }
658
659        putPref("points.hdopcircle", hdopCircleGpsPoints.isSelected());
660        putPref("points.large", largeGpsPoints.isSelected());
661        putPref("lines.width", drawLineWidth.getText());
662        putPref("lines.alpha-blend", drawLineWithAlpha.isSelected());
663
664        Config.getPref().putBoolean("mappaint.gpx.use-antialiasing", useGpsAntialiasing.isSelected());
665
666        if (colorTypeGlobal.isSelected()) {
667            putPref("colormode", null);
668            putPref("colormode.dynamic-range", null);
669            putPref("colormode.velocity.tune", null);
670            return false;
671        } else if (colorTypeVelocity.isSelected()) {
672            putPref("colormode", 1);
673        } else if (colorTypeDilution.isSelected()) {
674            putPref("colormode", 2);
675        } else if (colorTypeDirection.isSelected()) {
676            putPref("colormode", 3);
677        } else if (colorTypeTime.isSelected()) {
678            putPref("colormode", 4);
679        } else if (colorTypeHeatMap.isSelected()) {
680            putPref("colormode", 5);
681        } else if (colorTypeQuality.isSelected()) {
682            putPref("colormode", 6);
683        } else {
684            putPref("colormode", 0);
685        }
686        putPref("colormode.dynamic-range", colorDynamic.isSelected());
687        int ccti = colorTypeVelocityTune.getSelectedIndex();
688        putPref("colormode.velocity.tune", ccti == 2 ? 10 : (ccti == 1 ? 20 : 45));
689        putPref("colormode.heatmap.colormap", colorTypeHeatMapTune.getSelectedIndex());
690        putPref("colormode.heatmap.use-points", colorTypeHeatMapPoints.isSelected());
691        putPref("colormode.heatmap.gain", colorTypeHeatMapGain.getValue());
692        putPref("colormode.heatmap.lower-limit", colorTypeHeatMapLowerLimit.getValue());
693
694        if (!global && layers != null && !layers.isEmpty()) {
695            layers.forEach(l -> l.data.invalidate());
696        }
697
698        return false;
699    }
700
701    private static void updateWaypointLabelCombobox(JosmComboBox<String> cb, JosmTextField tf, String labelPattern) {
702        boolean found = false;
703        for (int i = 0; i < LABEL_PATTERN_TEMPLATE.length; i++) {
704            if (LABEL_PATTERN_TEMPLATE[i].equals(labelPattern)) {
705                cb.setSelectedIndex(i);
706                found = true;
707                break;
708            }
709        }
710        if (!found) {
711            cb.setSelectedIndex(WAYPOINT_LABEL_CUSTOM);
712            tf.setEnabled(true);
713            tf.setText(labelPattern);
714        }
715    }
716
717    private static void updateWaypointPattern(JosmComboBox<String> cb, JosmTextField tf) {
718        if (cb.getSelectedIndex() == WAYPOINT_LABEL_CUSTOM) {
719            tf.setEnabled(true);
720        } else {
721            tf.setEnabled(false);
722            tf.setText(LABEL_PATTERN_TEMPLATE[cb.getSelectedIndex()]);
723        }
724    }
725
726    @Override
727    public boolean validatePreferences() {
728        TemplateParser parser = new TemplateParser(waypointLabelPattern.getText());
729        try {
730            parser.parse();
731        } catch (ParseError e) {
732            Logging.warn(e);
733            JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
734                    tr("Incorrect waypoint label pattern: {0}", e.getMessage()), tr("Incorrect pattern"), JOptionPane.ERROR_MESSAGE);
735            waypointLabelPattern.requestFocus();
736            return false;
737        }
738        parser = new TemplateParser(audioWaypointLabelPattern.getText());
739        try {
740            parser.parse();
741        } catch (ParseError e) {
742            Logging.warn(e);
743            JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
744                    tr("Incorrect audio waypoint label pattern: {0}", e.getMessage()), tr("Incorrect pattern"), JOptionPane.ERROR_MESSAGE);
745            audioWaypointLabelPattern.requestFocus();
746            return false;
747        }
748        return true;
749    }
750}