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