001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.marktr;
006import static org.openstreetmap.josm.tools.I18n.tr;
007
008import java.awt.AWTEvent;
009import java.awt.Color;
010import java.awt.Component;
011import java.awt.Cursor;
012import java.awt.Dimension;
013import java.awt.EventQueue;
014import java.awt.Font;
015import java.awt.GraphicsEnvironment;
016import java.awt.GridBagLayout;
017import java.awt.MouseInfo;
018import java.awt.Point;
019import java.awt.PointerInfo;
020import java.awt.SystemColor;
021import java.awt.Toolkit;
022import java.awt.event.AWTEventListener;
023import java.awt.event.ActionEvent;
024import java.awt.event.ComponentAdapter;
025import java.awt.event.ComponentEvent;
026import java.awt.event.InputEvent;
027import java.awt.event.KeyAdapter;
028import java.awt.event.KeyEvent;
029import java.awt.event.MouseAdapter;
030import java.awt.event.MouseEvent;
031import java.awt.event.MouseListener;
032import java.awt.event.MouseMotionListener;
033import java.lang.reflect.InvocationTargetException;
034import java.text.DecimalFormat;
035import java.util.ArrayList;
036import java.util.Collection;
037import java.util.Comparator;
038import java.util.ConcurrentModificationException;
039import java.util.Iterator;
040import java.util.List;
041import java.util.Objects;
042import java.util.concurrent.BlockingQueue;
043import java.util.concurrent.LinkedBlockingQueue;
044import java.util.stream.Collectors;
045
046import javax.swing.AbstractAction;
047import javax.swing.BorderFactory;
048import javax.swing.JCheckBoxMenuItem;
049import javax.swing.JLabel;
050import javax.swing.JMenuItem;
051import javax.swing.JPanel;
052import javax.swing.JPopupMenu;
053import javax.swing.JProgressBar;
054import javax.swing.JScrollPane;
055import javax.swing.JSeparator;
056import javax.swing.Popup;
057import javax.swing.PopupFactory;
058import javax.swing.UIManager;
059import javax.swing.event.PopupMenuEvent;
060import javax.swing.event.PopupMenuListener;
061
062import org.openstreetmap.josm.data.SystemOfMeasurement;
063import org.openstreetmap.josm.data.SystemOfMeasurement.SoMChangeListener;
064import org.openstreetmap.josm.data.coor.LatLon;
065import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager;
066import org.openstreetmap.josm.data.coor.conversion.DMSCoordinateFormat;
067import org.openstreetmap.josm.data.coor.conversion.ICoordinateFormat;
068import org.openstreetmap.josm.data.coor.conversion.ProjectedCoordinateFormat;
069import org.openstreetmap.josm.data.osm.DataSelectionListener;
070import org.openstreetmap.josm.data.osm.DataSet;
071import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
072import org.openstreetmap.josm.data.osm.IPrimitive;
073import org.openstreetmap.josm.data.osm.Node;
074import org.openstreetmap.josm.data.osm.OsmPrimitive;
075import org.openstreetmap.josm.data.osm.Way;
076import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
077import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
078import org.openstreetmap.josm.data.osm.event.DataSetListener;
079import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
080import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
081import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
082import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
083import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
084import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
085import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
086import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
087import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
088import org.openstreetmap.josm.data.preferences.AbstractProperty;
089import org.openstreetmap.josm.data.preferences.BooleanProperty;
090import org.openstreetmap.josm.data.preferences.DoubleProperty;
091import org.openstreetmap.josm.data.preferences.NamedColorProperty;
092import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
093import org.openstreetmap.josm.gui.help.Helpful;
094import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor;
095import org.openstreetmap.josm.gui.progress.swing.PleaseWaitProgressMonitor.ProgressMonitorDialog;
096import org.openstreetmap.josm.gui.util.GuiHelper;
097import org.openstreetmap.josm.gui.widgets.ImageLabel;
098import org.openstreetmap.josm.gui.widgets.JosmTextField;
099import org.openstreetmap.josm.spi.preferences.Config;
100import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
101import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
102import org.openstreetmap.josm.tools.ColorHelper;
103import org.openstreetmap.josm.tools.Destroyable;
104import org.openstreetmap.josm.tools.GBC;
105import org.openstreetmap.josm.tools.ImageProvider;
106import org.openstreetmap.josm.tools.Logging;
107import org.openstreetmap.josm.tools.SubclassFilteredCollection;
108import org.openstreetmap.josm.tools.Utils;
109
110/**
111 * A component that manages some status information display about the map.
112 * It keeps a status line below the map up to date and displays some tooltip
113 * information if the user hold the mouse long enough at some point.
114 *
115 * All this is done in background to not disturb other processes.
116 *
117 * The background thread does not alter any data of the map (read only thread).
118 * Also it is rather fail safe. In case of some error in the data, it just does
119 * nothing instead of whining and complaining.
120 *
121 * @author imi
122 */
123public final class MapStatus extends JPanel implements
124    Helpful, Destroyable, PreferenceChangedListener, SoMChangeListener, DataSelectionListener, DataSetListener, ZoomChangeListener {
125
126    private final DecimalFormat DECIMAL_FORMAT = new DecimalFormat(Config.getPref().get("statusbar.decimal-format", "0.00"));
127    private static final AbstractProperty<Double> DISTANCE_THRESHOLD = new DoubleProperty("statusbar.distance-threshold", 0.01).cached();
128
129    private static final AbstractProperty<Boolean> SHOW_ID = new BooleanProperty("osm-primitives.showid", false);
130
131    private static final List<SystemOfMeasurement> SORTED_SYSTEM_OF_MEASUREMENTS = SystemOfMeasurement.ALL_SYSTEMS.values().stream()
132            .sorted(Comparator.comparing(SystemOfMeasurement::toString))
133            .collect(Collectors.toList());
134
135    /**
136     * Property for map status background color.
137     * @since 6789
138     */
139    public static final NamedColorProperty PROP_BACKGROUND_COLOR = new NamedColorProperty(
140            marktr("Status bar background"), ColorHelper.html2color("#b8cfe5"));
141
142    /**
143     * Property for map status background color (active state).
144     * @since 6789
145     */
146    public static final NamedColorProperty PROP_ACTIVE_BACKGROUND_COLOR = new NamedColorProperty(
147            marktr("Status bar background: active"), ColorHelper.html2color("#aaff5e"));
148
149    /**
150     * Property for map status foreground color.
151     * @since 6789
152     */
153    public static final NamedColorProperty PROP_FOREGROUND_COLOR = new NamedColorProperty(
154            marktr("Status bar foreground"), Color.black);
155
156    /**
157     * Property for map status foreground color (active state).
158     * @since 6789
159     */
160    public static final NamedColorProperty PROP_ACTIVE_FOREGROUND_COLOR = new NamedColorProperty(
161            marktr("Status bar foreground: active"), Color.black);
162
163    /**
164     * The MapView this status belongs to.
165     */
166    private final MapView mv;
167    private final transient Collector collector;
168
169    static final class ShowMonitorDialogMouseAdapter extends MouseAdapter {
170        @Override
171        public void mouseClicked(MouseEvent e) {
172            PleaseWaitProgressMonitor monitor = PleaseWaitProgressMonitor.getCurrent();
173            if (monitor != null) {
174                monitor.showForegroundDialog();
175            }
176        }
177    }
178
179    static final class JumpToOnLeftClickMouseAdapter extends MouseAdapter {
180        @Override
181        public void mouseClicked(MouseEvent e) {
182            if (e.getButton() != MouseEvent.BUTTON3) {
183                MainApplication.getMenu().jumpToAct.showJumpToDialog();
184            }
185        }
186    }
187
188    /**
189     * The progress monitor that is used to display the progress if the user selects to run in background
190     */
191    public class BackgroundProgressMonitor implements ProgressMonitorDialog {
192
193        private String title;
194        private String customText;
195
196        private void updateText() {
197            if (!Utils.isEmpty(customText)) {
198                progressBar.setToolTipText(tr("{0} ({1})", title, customText));
199            } else {
200                progressBar.setToolTipText(title);
201            }
202        }
203
204        @Override
205        public void setVisible(boolean visible) {
206            progressBar.setVisible(visible);
207        }
208
209        @Override
210        public void updateProgress(int progress) {
211            progressBar.setValue(progress);
212            progressBar.repaint();
213            MapStatus.this.doLayout();
214        }
215
216        @Override
217        public void setCustomText(String text) {
218            this.customText = text;
219            updateText();
220        }
221
222        @Override
223        public void setCurrentAction(String text) {
224            this.title = text;
225            updateText();
226        }
227
228        @Override
229        public void setIndeterminate(boolean newValue) {
230            UIManager.put("ProgressBar.cycleTime", UIManager.getInt("ProgressBar.repaintInterval") * 100);
231            progressBar.setIndeterminate(newValue);
232        }
233
234        @Override
235        public void appendLogMessage(String message) {
236            if (!Utils.isEmpty(message)) {
237                Logging.info("appendLogMessage not implemented for background tasks. Message was: " + message);
238            }
239        }
240
241    }
242
243    /** The {@link ICoordinateFormat} set in the previous update */
244    private transient ICoordinateFormat previousCoordinateFormat;
245    private final ImageLabel latText = new ImageLabel("lat",
246            null, DMSCoordinateFormat.INSTANCE.latToString(LatLon.SOUTH_POLE).length(), PROP_BACKGROUND_COLOR.get());
247    private final ImageLabel lonText = new ImageLabel("lon",
248            null, DMSCoordinateFormat.INSTANCE.lonToString(new LatLon(0, 180)).length(), PROP_BACKGROUND_COLOR.get());
249    private final ImageLabel headingText = new ImageLabel("heading",
250            tr("The (compass) heading of the line segment being drawn."),
251            DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get());
252    private final ImageLabel angleText = new ImageLabel("angle",
253            tr("The angle between the previous and the current way segment."),
254            DECIMAL_FORMAT.format(360).length() + 1, PROP_BACKGROUND_COLOR.get());
255    private final ImageLabel distText = new ImageLabel("dist",
256            tr("The length of the new way segment being drawn."), 10, PROP_BACKGROUND_COLOR.get());
257    private final ImageLabel nameText = new ImageLabel("name",
258            tr("The name of the object at the mouse pointer."),
259            getNameLabelCharacterCount(MainApplication.getMainFrame()), PROP_BACKGROUND_COLOR.get());
260    private final JosmTextField helpText = new JosmTextField(null, null, 0, false);
261    private final JProgressBar progressBar = new JProgressBar();
262    private final transient ComponentAdapter mvComponentAdapter;
263    /**
264     * The progress monitor for displaying a background progress
265     */
266    public final transient BackgroundProgressMonitor progressMonitor = new BackgroundProgressMonitor();
267
268    // Distance value displayed in distText, stored if refresh needed after a change of system of measurement
269    private double distValue;
270
271    // Determines if angle panel is enabled or not
272    private boolean angleEnabled;
273
274    /**
275     * This is the thread that runs in the background and collects the information displayed.
276     * It gets destroyed by destroy() when the MapFrame itself is destroyed.
277     */
278    private final transient Thread thread;
279
280    private final transient List<StatusTextHistory> statusText = new ArrayList<>();
281
282    protected static final class StatusTextHistory {
283        private final Object id;
284        private final String text;
285
286        StatusTextHistory(Object id, String text) {
287            this.id = id;
288            this.text = text;
289        }
290
291        @Override
292        public boolean equals(Object obj) {
293            return obj instanceof StatusTextHistory && ((StatusTextHistory) obj).id == id;
294        }
295
296        @Override
297        public int hashCode() {
298            return System.identityHashCode(id);
299        }
300    }
301
302    /**
303     * The collector class that waits for notification and then update the display objects.
304     *
305     * @author imi
306     */
307    private final class Collector implements Runnable {
308        private final class CollectorWorker implements Runnable {
309            private final MouseState ms;
310
311            private CollectorWorker(MouseState ms) {
312                this.ms = ms;
313            }
314
315            @Override
316            public void run() {
317                // Freeze display when holding down CTRL
318                if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
319                    // update the information popup's labels though, because the selection might have changed from the outside
320                    popupUpdateLabels();
321                    return;
322                }
323
324                // The popup != null check is required because a left-click produces several events as well,
325                // which would make this variable true. Of course we only want the popup to show
326                // if the middle mouse button has been pressed in the first place
327                boolean mouseNotMoved = oldMousePos != null && oldMousePos.equals(ms.mousePos);
328                boolean isAtOldPosition = mouseNotMoved && popup != null;
329                boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0;
330
331                DataSet ds = mv.getLayerManager().getActiveDataSet();
332                if (ds != null) {
333                    // This is not perfect, if current dataset was changed during execution, the lock would be useless
334                    if (isAtOldPosition && middleMouseDown) {
335                        // Write lock is necessary when selecting in popupCycleSelection
336                        // locks can not be upgraded -> if do read lock here and write lock later
337                        // (in OsmPrimitive.updateFlags) then always occurs deadlock (#5814)
338                        ds.beginUpdate();
339                    } else {
340                        ds.getReadLock().lock();
341                    }
342                }
343                // This try/catch is a hack to stop the flooding bug reports about this.
344                // The exception needed to handle with in the first place, means that this
345                // access to the data need to be restarted, if the main thread modifies the data.
346                try {
347                    // Set the text label in the bottom status bar
348                    // "if mouse moved only" was added to stop heap growing
349                    if (!mouseNotMoved) {
350                        statusBarElementUpdate(ms);
351                    }
352
353                    // Popup Information
354                    // display them if the middle mouse button is pressed and keep them until the mouse is moved
355                    if (middleMouseDown || isAtOldPosition) {
356                        Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos, OsmPrimitive::isSelectable);
357
358                        final JPanel c = new JPanel(new GridBagLayout());
359                        final JLabel lbl = new JLabel(
360                                "<html>"+tr("Middle click again to cycle through.<br>"+
361                                        "Hold CTRL to select directly from this list with the mouse.<hr>")+"</html>",
362                                        null,
363                                        JLabel.HORIZONTAL
364                                );
365                        lbl.setHorizontalAlignment(JLabel.LEADING);
366                        c.add(lbl, GBC.eol().insets(2, 0, 2, 0));
367
368                        // Only cycle if the mouse has not been moved and the middle mouse button has been pressed at least
369                        // twice (the reason for this is the popup != null check for isAtOldPosition, see above.
370                        // This is a nice side effect though, because it does not change selection of the first middle click)
371                        if (isAtOldPosition && middleMouseDown) {
372                            // Hand down mouse modifiers so the SHIFT mod can be handled correctly (see function)
373                            popupCycleSelection(osms, ms.modifiers);
374                        }
375
376                        // These labels may need to be updated from the outside so collect them
377                        List<JLabel> lbls = new ArrayList<>(osms.size());
378                        for (final OsmPrimitive osm : osms) {
379                            JLabel l = popupBuildPrimitiveLabels(osm);
380                            lbls.add(l);
381                            c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2));
382                        }
383
384                        popupShowPopup(popupCreatePopup(c, ms), lbls);
385                    } else {
386                        popupHidePopup();
387                    }
388
389                    oldMousePos = ms.mousePos;
390                } catch (ConcurrentModificationException ex) {
391                    Logging.warn(ex);
392                } finally {
393                    if (ds != null) {
394                        if (isAtOldPosition && middleMouseDown) {
395                            ds.endUpdate();
396                        } else {
397                            ds.getReadLock().unlock();
398                        }
399                    }
400                }
401            }
402        }
403
404        /**
405         * the mouse position of the previous iteration. This is used to show
406         * the popup until the cursor is moved.
407         */
408        private Point oldMousePos;
409        /**
410         * Contains the labels that are currently shown in the information
411         * popup
412         */
413        private List<JLabel> popupLabels;
414        /**
415         * The popup displayed to show additional information
416         */
417        private Popup popup;
418
419        private final MapFrame parent;
420
421        private final BlockingQueue<MouseState> incomingMouseState = new LinkedBlockingQueue<>();
422
423        private Point lastMousePos;
424
425        Collector(MapFrame parent) {
426            this.parent = parent;
427        }
428
429        /**
430         * Execution function for the Collector.
431         */
432        @Override
433        public void run() {
434            registerListeners();
435            try {
436                for (;;) {
437                    try {
438                        final MouseState ms = incomingMouseState.take();
439                        if (parent != MainApplication.getMap())
440                            return; // exit, if new parent.
441
442                        // Do nothing, if required data is missing
443                        if (ms.mousePos == null || mv.getCenter() == null) {
444                            continue;
445                        }
446
447                        EventQueue.invokeAndWait(new CollectorWorker(ms));
448                    } catch (InvocationTargetException e) {
449                        Logging.warn(e);
450                    }
451                }
452            } catch (InterruptedException e) {
453                // Occurs frequently during JOSM shutdown, log set to trace only
454                Logging.trace("InterruptedException in "+MapStatus.class.getSimpleName());
455                Thread.currentThread().interrupt();
456            } finally {
457                unregisterListeners();
458            }
459        }
460
461        /**
462         * Creates a popup for the given content next to the cursor. Tries to
463         * keep the popup on screen and shows a vertical scrollbar, if the
464         * screen is too small.
465         * @param content popup content
466         * @param ms mouse state
467         * @return popup
468         */
469        private Popup popupCreatePopup(Component content, MouseState ms) {
470            Point p = mv.getLocationOnScreen();
471            Dimension scrn = GuiHelper.getScreenSize();
472
473            // Create a JScrollPane around the content, in case there's not enough space
474            JScrollPane sp = GuiHelper.embedInVerticalScrollPane(content);
475            sp.setBorder(BorderFactory.createRaisedBevelBorder());
476            // Implement max-size content-independent
477            Dimension prefsize = sp.getPreferredSize();
478            int w = Math.min(prefsize.width, Math.min(800, (scrn.width/2) - 16));
479            int h = Math.min(prefsize.height, scrn.height - 10);
480            sp.setPreferredSize(new Dimension(w, h));
481
482            int xPos = p.x + ms.mousePos.x + 16;
483            // Display the popup to the left of the cursor if it would be cut
484            // off on its right, but only if more space is available
485            if (xPos + w > scrn.width && xPos > scrn.width/2) {
486                xPos = p.x + ms.mousePos.x - 4 - w;
487            }
488            int yPos = p.y + ms.mousePos.y + 16;
489            // Move the popup up if it would be cut off at its bottom but do not
490            // move it off screen on the top
491            if (yPos + h > scrn.height - 5) {
492                yPos = Math.max(5, scrn.height - h - 5);
493            }
494
495            PopupFactory pf = PopupFactory.getSharedInstance();
496            return pf.getPopup(mv, sp, xPos, yPos);
497        }
498
499        /**
500         * Calls this to update the element that is shown in the statusbar
501         * @param ms mouse state
502         */
503        private void statusBarElementUpdate(MouseState ms) {
504            final OsmPrimitive osmNearest = mv.getNearestNodeOrWay(ms.mousePos, OsmPrimitive::isUsable, false);
505            if (osmNearest != null) {
506                nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance()));
507            } else {
508                nameText.setText(tr("(no object)"));
509            }
510        }
511
512        /**
513         * Call this with a set of primitives to cycle through them. Method
514         * will automatically select the next item and update the map
515         * @param osms primitives to cycle through
516         * @param mods modifiers (i.e. control keys)
517         */
518        private void popupCycleSelection(Collection<OsmPrimitive> osms, int mods) {
519            DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
520            // Find some items that are required for cycling through
521            OsmPrimitive firstItem = null;
522            OsmPrimitive firstSelected = null;
523            OsmPrimitive nextSelected = null;
524            for (final OsmPrimitive osm : osms) {
525                if (firstItem == null) {
526                    firstItem = osm;
527                }
528                if (firstSelected != null && nextSelected == null) {
529                    nextSelected = osm;
530                }
531                if (firstSelected == null && ds.isSelected(osm)) {
532                    firstSelected = osm;
533                }
534            }
535
536            // Clear previous selection if SHIFT (add to selection) is not
537            // pressed. Cannot use "setSelected()" because it will cause a
538            // fireSelectionChanged event which is unnecessary at this point.
539            if ((mods & MouseEvent.SHIFT_DOWN_MASK) == 0) {
540                ds.clearSelection();
541            }
542
543            // This will cycle through the available items.
544            if (firstSelected != null) {
545                ds.clearSelection(firstSelected);
546                if (nextSelected != null) {
547                    ds.addSelected(nextSelected);
548                }
549            } else if (firstItem != null) {
550                ds.addSelected(firstItem);
551            }
552        }
553
554        /**
555         * Tries to hide the given popup
556         */
557        private void popupHidePopup() {
558            popupLabels = null;
559            if (popup == null)
560                return;
561            final Popup staticPopup = popup;
562            popup = null;
563            EventQueue.invokeLater(staticPopup::hide);
564        }
565
566        /**
567         * Tries to show the given popup, can be hidden using {@link #popupHidePopup}
568         * If an old popup exists, it will be automatically hidden
569         * @param newPopup popup to show
570         * @param lbls labels to show (see {@link #popupLabels})
571         */
572        private void popupShowPopup(Popup newPopup, List<JLabel> lbls) {
573            final Popup staticPopup = newPopup;
574            if (this.popup != null) {
575                // If an old popup exists, remove it when the new popup has been drawn to keep flickering to a minimum
576                final Popup staticOldPopup = this.popup;
577                EventQueue.invokeLater(() -> {
578                    staticPopup.show();
579                    staticOldPopup.hide();
580                });
581            } else {
582                // There is no old popup
583                EventQueue.invokeLater(staticPopup::show);
584            }
585            this.popupLabels = lbls;
586            this.popup = newPopup;
587        }
588
589        /**
590         * This method should be called if the selection may have changed from
591         * outside of this class. This is the case when CTRL is pressed and the
592         * user clicks on the map instead of the popup.
593         */
594        private void popupUpdateLabels() {
595            if (this.popup == null || this.popupLabels == null)
596                return;
597            for (JLabel l : this.popupLabels) {
598                l.validate();
599            }
600        }
601
602        /**
603         * Sets the colors for the given label depending on the selected status of
604         * the given OsmPrimitive
605         *
606         * @param lbl The label to color
607         * @param osm The primitive to derive the colors from
608         */
609        private void popupSetLabelColors(JLabel lbl, IPrimitive osm) {
610            if (osm.isSelected()) {
611                lbl.setBackground(SystemColor.textHighlight);
612                lbl.setForeground(SystemColor.textHighlightText);
613            } else {
614                lbl.setBackground(SystemColor.control);
615                lbl.setForeground(SystemColor.controlText);
616            }
617        }
618
619        /**
620         * Builds the labels with all necessary listeners for the info popup for the
621         * given OsmPrimitive
622         * @param osm  The primitive to create the label for
623         * @return labels for info popup
624         */
625        private JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) {
626            final StringBuilder text = new StringBuilder(32);
627            String name = Utils.escapeReservedCharactersHTML(osm.getDisplayName(DefaultNameFormatter.getInstance()));
628            if (osm.isNewOrUndeleted() || osm.isModified()) {
629                name = "<i><b>"+ name + "*</b></i>";
630            }
631            text.append(name);
632
633            boolean idShown = SHOW_ID.get();
634            // fix #7557 - do not show ID twice
635
636            if (!osm.isNew() && !idShown) {
637                text.append(" [id=").append(osm.getId()).append(']');
638            }
639
640            if (osm.getUser() != null) {
641                text.append(" [").append(tr("User:")).append(' ')
642                    .append(Utils.escapeReservedCharactersHTML(osm.getUser().getName())).append(']');
643            }
644
645            osm.visitKeys((primitive, key, value) -> text.append("<br>").append(key).append('=').append(value));
646
647            final JLabel l = new JLabel(
648                    "<html>" + text.toString() + "</html>",
649                    ImageProvider.get(osm.getDisplayType()),
650                    JLabel.HORIZONTAL
651                    ) {
652                // This is necessary so the label updates its colors when the
653                // selection is changed from the outside
654                @Override
655                public void validate() {
656                    super.validate();
657                    popupSetLabelColors(this, osm);
658                }
659            };
660            l.setOpaque(true);
661            popupSetLabelColors(l, osm);
662            l.setFont(l.getFont().deriveFont(Font.PLAIN));
663            l.setVerticalTextPosition(JLabel.TOP);
664            l.setHorizontalAlignment(JLabel.LEADING);
665            l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
666            l.addMouseListener(new MouseAdapter() {
667                @Override
668                public void mouseEntered(MouseEvent e) {
669                    l.setBackground(SystemColor.info);
670                    l.setForeground(SystemColor.infoText);
671                }
672
673                @Override
674                public void mouseExited(MouseEvent e) {
675                    popupSetLabelColors(l, osm);
676                }
677
678                @Override
679                public void mouseClicked(MouseEvent e) {
680                    DataSet ds = MainApplication.getLayerManager().getActiveDataSet();
681                    // Let the user toggle the selection
682                    ds.toggleSelected(osm);
683                    l.validate();
684                }
685            });
686            // Sometimes the mouseEntered event is not catched, thus the label
687            // will not be highlighted, making it confusing. The MotionListener can correct this defect.
688            l.addMouseMotionListener(new MouseMotionListener() {
689                 @Override
690                 public void mouseMoved(MouseEvent e) {
691                    l.setBackground(SystemColor.info);
692                    l.setForeground(SystemColor.infoText);
693                 }
694
695                 @Override
696                 public void mouseDragged(MouseEvent e) {
697                     mouseMoved(e);
698                 }
699            });
700            return l;
701        }
702
703        /**
704         * Called whenever the mouse position or modifiers changed.
705         * @param mousePos The new mouse position. <code>null</code> if it did not change.
706         * @param modifiers The new modifiers.
707         */
708        public synchronized void updateMousePosition(Point mousePos, int modifiers) {
709            if (mousePos != null) {
710                lastMousePos = mousePos;
711            }
712            MouseState ms = new MouseState(lastMousePos, modifiers);
713            // remove mouse states that are in the queue. Our mouse state is newer.
714            incomingMouseState.clear();
715            if (!incomingMouseState.offer(ms)) {
716                Logging.warn("Unable to handle new MouseState: " + ms);
717            }
718        }
719    }
720
721    /**
722     * Everything, the collector is interested of. Access must be synchronized.
723     * @author imi
724     */
725    private static class MouseState {
726        private final Point mousePos;
727        private final int modifiers;
728
729        MouseState(Point mousePos, int modifiers) {
730            this.mousePos = mousePos;
731            this.modifiers = modifiers;
732        }
733    }
734
735    private final transient AWTEventListener awtListener;
736
737    private final transient MouseMotionListener mouseMotionListener = new MouseMotionListener() {
738        @Override
739        public void mouseMoved(MouseEvent e) {
740            synchronized (collector) {
741                collector.updateMousePosition(e.getPoint(), e.getModifiersEx());
742            }
743        }
744
745        @Override
746        public void mouseDragged(MouseEvent e) {
747            mouseMoved(e);
748        }
749    };
750
751    private final transient KeyAdapter keyAdapter = new KeyAdapter() {
752        @Override public void keyPressed(KeyEvent e) {
753            synchronized (collector) {
754                collector.updateMousePosition(null, e.getModifiersEx());
755            }
756        }
757
758        @Override public void keyReleased(KeyEvent e) {
759            keyPressed(e);
760        }
761    };
762
763    /** see #19887: determine if the {@code distValue} field should be filled with length of selected object */
764    private boolean autoLength = true;
765
766    private void registerListeners() {
767        // Listen to keyboard/mouse events for pressing/releasing alt key and inform the collector.
768        try {
769            Toolkit.getDefaultToolkit().addAWTEventListener(awtListener,
770                    AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
771        } catch (SecurityException ex) {
772            Logging.trace(ex);
773            mv.addMouseMotionListener(mouseMotionListener);
774            mv.addKeyListener(keyAdapter);
775        }
776    }
777
778    private void unregisterListeners() {
779        try {
780            Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener);
781        } catch (SecurityException e) {
782            // Don't care, awtListener probably wasn't registered anyway
783            Logging.trace(e);
784        }
785        mv.removeMouseMotionListener(mouseMotionListener);
786        mv.removeKeyListener(keyAdapter);
787    }
788
789    private class MapStatusPopupMenu extends JPopupMenu {
790
791        private final JMenuItem jumpButton = add(MainApplication.getMenu().jumpToAct);
792
793        /** Icons for selecting {@link SystemOfMeasurement} */
794        private final Collection<JCheckBoxMenuItem> somItems = new ArrayList<>();
795        /** Icons for selecting {@link ICoordinateFormat}  */
796        private final Collection<JCheckBoxMenuItem> coordinateFormatItems = new ArrayList<>();
797
798        private final JSeparator separator = new JSeparator();
799
800        private final JMenuItem doNotHide = new JCheckBoxMenuItem(new AbstractAction(tr("Do not hide status bar")) {
801            @Override
802            public void actionPerformed(ActionEvent e) {
803                boolean sel = ((JCheckBoxMenuItem) e.getSource()).getState();
804                Config.getPref().putBoolean("statusbar.always-visible", sel);
805            }
806        });
807
808        MapStatusPopupMenu() {
809            for (final SystemOfMeasurement som : SORTED_SYSTEM_OF_MEASUREMENTS) {
810                JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(som.toString()) {
811                    @Override
812                    public void actionPerformed(ActionEvent e) {
813                        updateSystemOfMeasurement(som);
814                    }
815                });
816                somItems.add(item);
817                add(item);
818            }
819            for (final ICoordinateFormat format : CoordinateFormatManager.getCoordinateFormats()) {
820                JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(format.getDisplayName()) {
821                    @Override
822                    public void actionPerformed(ActionEvent e) {
823                        CoordinateFormatManager.setCoordinateFormat(format);
824                    }
825                });
826                coordinateFormatItems.add(item);
827                add(item);
828            }
829
830            add(separator);
831            add(doNotHide);
832
833            addPopupMenuListener(new PopupMenuListener() {
834                @Override
835                public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
836                    Component invoker = ((JPopupMenu) e.getSource()).getInvoker();
837                    jumpButton.setVisible(latText.equals(invoker) || lonText.equals(invoker));
838                    String currentSOM = SystemOfMeasurement.getSystemOfMeasurement().toString();
839                    for (JMenuItem item : somItems) {
840                        item.setSelected(item.getText().equals(currentSOM));
841                        item.setVisible(distText.equals(invoker));
842                    }
843                    final String currentCorrdinateFormat = CoordinateFormatManager.getDefaultFormat().getDisplayName();
844                    for (JMenuItem item : coordinateFormatItems) {
845                        item.setSelected(currentCorrdinateFormat.equals(item.getText()));
846                        item.setVisible(latText.equals(invoker) || lonText.equals(invoker));
847                    }
848                    separator.setVisible(distText.equals(invoker) || latText.equals(invoker) || lonText.equals(invoker));
849                    doNotHide.setSelected(Config.getPref().getBoolean("statusbar.always-visible", true));
850                }
851
852                @Override
853                public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
854                    // Do nothing
855                }
856
857                @Override
858                public void popupMenuCanceled(PopupMenuEvent e) {
859                    // Do nothing
860                }
861            });
862        }
863    }
864
865    /**
866     * Construct a new MapStatus and attach it to the map view.
867     * @param mapFrame The MapFrame the status line is part of.
868     */
869    public MapStatus(final MapFrame mapFrame) {
870        this.mv = mapFrame.mapView;
871        this.collector = new Collector(mapFrame);
872        this.awtListener = event -> {
873            if (event instanceof InputEvent &&
874                    ((InputEvent) event).getComponent() == mv) {
875                synchronized (collector) {
876                    int modifiers = ((InputEvent) event).getModifiersEx();
877                    Point mousePos = null;
878                    if (event instanceof MouseEvent) {
879                        mousePos = ((MouseEvent) event).getPoint();
880                    }
881                    collector.updateMousePosition(mousePos, modifiers);
882                }
883            }
884        };
885
886        // Context menu of status bar
887        setComponentPopupMenu(new MapStatusPopupMenu());
888
889        // also show Jump To dialog on mouse click (except context menu)
890        MouseListener jumpToOnLeftClick = new JumpToOnLeftClickMouseAdapter();
891
892        // Listen for mouse movements and set the position text field
893        mv.addMouseMotionListener(new MouseMotionListener() {
894            @Override
895            public void mouseDragged(MouseEvent e) {
896                mouseMoved(e);
897            }
898
899            @Override
900            public void mouseMoved(MouseEvent e) {
901                if (mv.getCenter() == null)
902                    return;
903                // Do not update the view if ctrl or right button is pressed.
904                if ((e.getModifiersEx() & (MouseEvent.CTRL_DOWN_MASK | MouseEvent.BUTTON3_DOWN_MASK)) == 0) {
905                    updateLatLonText(e.getX(), e.getY());
906                }
907            }
908        });
909
910        setLayout(new GridBagLayout());
911        setBorder(BorderFactory.createEmptyBorder(1, 2, 1, 2));
912
913        latText.setForeground(PROP_FOREGROUND_COLOR.get());
914        lonText.setForeground(PROP_FOREGROUND_COLOR.get());
915        headingText.setForeground(PROP_FOREGROUND_COLOR.get());
916        distText.setForeground(PROP_FOREGROUND_COLOR.get());
917        nameText.setForeground(PROP_FOREGROUND_COLOR.get());
918
919        latText.setInheritsPopupMenu(true);
920        lonText.setInheritsPopupMenu(true);
921        headingText.setInheritsPopupMenu(true);
922        distText.setInheritsPopupMenu(true);
923        nameText.setInheritsPopupMenu(true);
924
925        add(latText, GBC.std());
926        add(lonText, GBC.std().insets(3, 0, 0, 0));
927        add(headingText, GBC.std().insets(3, 0, 0, 0));
928        add(angleText, GBC.std().insets(3, 0, 0, 0));
929        add(distText, GBC.std().insets(3, 0, 0, 0));
930
931        if (Config.getPref().getBoolean("statusbar.change-system-of-measurement-on-click", true)) {
932            distText.addMouseListener(new MouseAdapter() {
933                @Override
934                public void mouseClicked(MouseEvent e) {
935                    if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
936                        SystemOfMeasurement som = SystemOfMeasurement.getSystemOfMeasurement();
937                        int i = (SORTED_SYSTEM_OF_MEASUREMENTS.indexOf(som) + 1) % SORTED_SYSTEM_OF_MEASUREMENTS.size();
938                        SystemOfMeasurement newsom = SORTED_SYSTEM_OF_MEASUREMENTS.get(i);
939                        updateSystemOfMeasurement(newsom);
940                    }
941                }
942            });
943        }
944
945        SystemOfMeasurement.addSoMChangeListener(this);
946        NavigatableComponent.addZoomChangeListener(this);
947
948        latText.addMouseListener(jumpToOnLeftClick);
949        lonText.addMouseListener(jumpToOnLeftClick);
950
951        helpText.setEditable(false);
952        add(nameText, GBC.std().insets(3, 0, 0, 0));
953        add(helpText, GBC.std().insets(3, 0, 0, 0).fill(GBC.HORIZONTAL));
954
955        progressBar.setMaximum(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX);
956        progressBar.setVisible(false);
957        GBC gbc = GBC.eol();
958        gbc.ipadx = 100;
959        add(progressBar, gbc);
960        progressBar.addMouseListener(new ShowMonitorDialogMouseAdapter());
961
962        Config.getPref().addPreferenceChangeListener(this);
963        DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT);
964        SelectionEventManager.getInstance().addSelectionListenerForEdt(this);
965
966        mvComponentAdapter = new ComponentAdapter() {
967            @Override
968            public void componentResized(ComponentEvent e) {
969                nameText.setCharCount(getNameLabelCharacterCount(MainApplication.getMainFrame()));
970                revalidate();
971            }
972        };
973        mv.addComponentListener(mvComponentAdapter);
974
975        // The background thread
976        thread = new Thread(collector, "Map Status Collector");
977        thread.setDaemon(true);
978        thread.start();
979    }
980
981    private void updateLatLonText(int x, int y) {
982        LatLon p = mv.getLatLon(x, y);
983        ICoordinateFormat mCord = CoordinateFormatManager.getDefaultFormat();
984        latText.setText(mCord.latToString(p));
985        lonText.setText(mCord.lonToString(p));
986        if (Objects.equals(previousCoordinateFormat, mCord)) {
987            // do nothing
988        } else if (ProjectedCoordinateFormat.INSTANCE.equals(mCord)) {
989            latText.setIcon("northing");
990            lonText.setIcon("easting");
991            latText.setToolTipText(tr("The northing at the mouse pointer."));
992            lonText.setToolTipText(tr("The easting at the mouse pointer."));
993            previousCoordinateFormat = mCord;
994        } else {
995            latText.setIcon("lat");
996            lonText.setIcon("lon");
997            latText.setToolTipText(tr("The geographic latitude at the mouse pointer."));
998            lonText.setToolTipText(tr("The geographic longitude at the mouse pointer."));
999            previousCoordinateFormat = mCord;
1000        }
1001    }
1002
1003    @Override
1004    public void systemOfMeasurementChanged(String oldSoM, String newSoM) {
1005        setDist(distValue);
1006    }
1007
1008    /**
1009     * Updates the system of measurement and displays a notification.
1010     * @param som The new system of measurement to set
1011     * @since 6960
1012     */
1013    public void updateSystemOfMeasurement(SystemOfMeasurement som) {
1014        SystemOfMeasurement.setSystemOfMeasurement(som);
1015        if (Config.getPref().getBoolean("statusbar.notify.change-system-of-measurement", true)) {
1016            new Notification(tr("System of measurement changed to {0}", som.toString()))
1017                .setDuration(Notification.TIME_SHORT)
1018                .show();
1019        }
1020    }
1021
1022    /**
1023     * Gets the panel that displays the angle
1024     * @return The angle panel
1025     */
1026    public JPanel getAnglePanel() {
1027        return angleText;
1028    }
1029
1030    @Override
1031    public String helpTopic() {
1032        return ht("/StatusBar");
1033    }
1034
1035    @Override
1036    public synchronized void addMouseListener(MouseListener ml) {
1037        lonText.addMouseListener(ml);
1038        latText.addMouseListener(ml);
1039    }
1040
1041    /**
1042     * Sets the help text in the status panel
1043     * @param text The text
1044     */
1045    public void setHelpText(String text) {
1046        setHelpText(null, text);
1047    }
1048
1049    /**
1050     * Sets the help status text to display
1051     * @param id The object that caused the status update (or a id object it selects). May be <code>null</code>
1052     * @param text The text
1053     */
1054    public synchronized void setHelpText(Object id, final String text) {
1055        StatusTextHistory entry = new StatusTextHistory(id, text);
1056
1057        statusText.remove(entry);
1058        statusText.add(entry);
1059
1060        GuiHelper.runInEDT(() -> {
1061            helpText.setText(text);
1062            helpText.setToolTipText(text);
1063        });
1064    }
1065
1066    /**
1067     * Removes a help text and restores the previous one
1068     * @param id The id passed to {@link #setHelpText(Object, String)}
1069     */
1070    public synchronized void resetHelpText(Object id) {
1071        if (statusText.isEmpty())
1072            return;
1073
1074        StatusTextHistory entry = new StatusTextHistory(id, null);
1075        if (statusText.get(statusText.size() - 1).equals(entry)) {
1076            if (statusText.size() == 1) {
1077                setHelpText("");
1078            } else {
1079                StatusTextHistory history = statusText.get(statusText.size() - 2);
1080                setHelpText(history.id, history.text);
1081            }
1082        }
1083        statusText.remove(entry);
1084    }
1085
1086    /**
1087     * Sets the angle to display in the angle panel. Values less than 0 yield "--".
1088     * @param a The angle
1089     * @see #setAngleNaN
1090     * @see #setAngleText
1091     */
1092    public void setAngle(double a) {
1093        angleText.setText(a < 0 ? "--" : DECIMAL_FORMAT.format(a) + " \u00B0");
1094    }
1095
1096    /**
1097     * Sets the angle to display in the angle panel. NaN yields "--".
1098     * @param a The angle
1099     * @see #setAngle
1100     * @see #setAngleText
1101     */
1102    public void setAngleNaN(double a) {
1103        angleText.setText(!Double.isFinite(a) ? "--" : DECIMAL_FORMAT.format(a) + " \u00B0");
1104    }
1105
1106    /**
1107     * Sets the angle to display in the angle panel
1108     * @param text The angle text
1109     */
1110    public void setAngleText(String text) {
1111        angleText.setText(text);
1112    }
1113
1114    /**
1115     * Sets the heading to display in the heading panel
1116     * @param h The heading
1117     */
1118    public void setHeading(double h) {
1119        headingText.setText(h < 0 ? "--" : DECIMAL_FORMAT.format(h) + " \u00B0");
1120    }
1121
1122    /**
1123     * Sets the distance text to the given value
1124     * @param dist The distance value to display, in meters
1125     */
1126    public void setDist(double dist) {
1127        distValue = dist;
1128        distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist, DECIMAL_FORMAT, DISTANCE_THRESHOLD.get()));
1129    }
1130
1131    /**
1132     * Sets the distance text to the total sum of given ways length
1133     * @param ways The ways to consider for the total distance
1134     * @since 5991
1135     */
1136    public void setDist(Collection<Way> ways) {
1137        double dist = -1;
1138        // Compute total length of selected way(s) until an arbitrary limit set to 250 ways
1139        // in order to prevent performance issue if a large number of ways are selected (old behaviour kept in that case, see #8403)
1140        int maxWays = Math.max(1, Config.getPref().getInt("selection.max-ways-for-statusline", 250));
1141        if (!ways.isEmpty() && ways.size() <= maxWays) {
1142            dist = ways.stream().mapToDouble(Way::getLength).sum();
1143        }
1144        setDist(dist);
1145    }
1146
1147    /**
1148     * Activates the angle panel.
1149     * @param activeFlag {@code true} to activate it, {@code false} to deactivate it
1150     */
1151    public void activateAnglePanel(boolean activeFlag) {
1152        angleEnabled = activeFlag;
1153        refreshAnglePanel();
1154    }
1155
1156    private void refreshAnglePanel() {
1157        angleText.setBackground(angleEnabled ? PROP_ACTIVE_BACKGROUND_COLOR.get() : PROP_BACKGROUND_COLOR.get());
1158        angleText.setForeground(angleEnabled ? PROP_ACTIVE_FOREGROUND_COLOR.get() : PROP_FOREGROUND_COLOR.get());
1159    }
1160
1161    @Override
1162    public void destroy() {
1163        SystemOfMeasurement.removeSoMChangeListener(this);
1164        NavigatableComponent.removeZoomChangeListener(this);
1165        Config.getPref().removePreferenceChangeListener(this);
1166        DatasetEventManager.getInstance().removeDatasetListener(this);
1167        SelectionEventManager.getInstance().removeSelectionListener(this);
1168        mv.removeComponentListener(mvComponentAdapter);
1169
1170        // MapFrame gets destroyed when the last layer is removed, but the status line background
1171        // thread that collects the information doesn't get destroyed automatically.
1172        if (thread != null) {
1173            try {
1174                thread.interrupt();
1175            } catch (SecurityException e) {
1176                Logging.error(e);
1177            }
1178        }
1179    }
1180
1181    @Override
1182    public void preferenceChanged(PreferenceChangeEvent e) {
1183        String key = e.getKey();
1184        if (key.startsWith(NamedColorProperty.NAMED_COLOR_PREFIX)) {
1185            if (PROP_BACKGROUND_COLOR.getKey().equals(key) || PROP_FOREGROUND_COLOR.getKey().equals(key)) {
1186                for (ImageLabel il : new ImageLabel[]{latText, lonText, headingText, distText, nameText}) {
1187                    il.setBackground(PROP_BACKGROUND_COLOR.get());
1188                    il.setForeground(PROP_FOREGROUND_COLOR.get());
1189                }
1190                refreshAnglePanel();
1191            } else if (PROP_ACTIVE_BACKGROUND_COLOR.getKey().equals(key) || PROP_ACTIVE_FOREGROUND_COLOR.getKey().equals(key)) {
1192                refreshAnglePanel();
1193            }
1194        }
1195    }
1196
1197    /**
1198     * Loads all colors from preferences.
1199     * @since 6789
1200     */
1201    public static void getColors() {
1202        PROP_BACKGROUND_COLOR.get();
1203        PROP_FOREGROUND_COLOR.get();
1204        PROP_ACTIVE_BACKGROUND_COLOR.get();
1205        PROP_ACTIVE_FOREGROUND_COLOR.get();
1206    }
1207
1208    private static int getNameLabelCharacterCount(Component parent) {
1209        int w = parent != null ? parent.getWidth() : 800;
1210        return Math.min(80, 20 + Math.max(0, w-1280) * 60 / (1920-1280));
1211    }
1212
1213    private void refreshDistText(Collection<? extends OsmPrimitive> newSelection) {
1214        if (!autoLength) {
1215            return;
1216        }
1217
1218        if (newSelection.size() == 2) {
1219            Iterator<? extends OsmPrimitive> it = newSelection.iterator();
1220            OsmPrimitive n1 = it.next();
1221            OsmPrimitive n2 = it.next();
1222            // show distance between two selected nodes with coordinates
1223            if (n1 instanceof Node && n2 instanceof Node) {
1224                LatLon c1 = ((Node) n1).getCoor();
1225                LatLon c2 = ((Node) n2).getCoor();
1226                if (c1 != null && c2 != null) {
1227                    setDist(c1.greatCircleDistance(c2));
1228                    return;
1229                }
1230            }
1231        }
1232        setDist(new SubclassFilteredCollection<OsmPrimitive, Way>(newSelection, Way.class::isInstance));
1233    }
1234
1235    @Override
1236    public void selectionChanged(SelectionChangeEvent event) {
1237        refreshDistText(event.getSelection());
1238    }
1239
1240    @Override
1241    public void zoomChanged() {
1242        if (!GraphicsEnvironment.isHeadless()) {
1243            try {
1244                PointerInfo pointerInfo = MouseInfo.getPointerInfo();
1245                if (pointerInfo != null) {
1246                    Point mp = pointerInfo.getLocation();
1247                    updateLatLonText(mp.x, mp.y);
1248                }
1249            } catch (SecurityException ex) {
1250                Logging.log(Logging.LEVEL_ERROR, "Unable to get mouse pointer info", ex);
1251            }
1252        }
1253    }
1254
1255    @Override
1256    public void wayNodesChanged(WayNodesChangedEvent event) {
1257        refreshDistText(event.getDataset().getSelected());
1258    }
1259
1260    @Override
1261    public void nodeMoved(NodeMovedEvent event) {
1262        refreshDistText(event.getDataset().getSelected());
1263    }
1264
1265    @Override
1266    public void primitivesAdded(PrimitivesAddedEvent event) {
1267        // Do nothing
1268    }
1269
1270    @Override
1271    public void primitivesRemoved(PrimitivesRemovedEvent event) {
1272        // Do nothing
1273    }
1274
1275    @Override
1276    public void tagsChanged(TagsChangedEvent event) {
1277        // Do nothing
1278    }
1279
1280    @Override
1281    public void relationMembersChanged(RelationMembersChangedEvent event) {
1282        // Do nothing
1283    }
1284
1285    @Override
1286    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
1287        // Do nothing
1288    }
1289
1290    @Override
1291    public void dataChanged(DataChangedEvent event) {
1292        if (event.getDataset() != null) {
1293            refreshDistText(event.getDataset().getSelected());
1294        }
1295    }
1296
1297    /**
1298     * Enable or disable the automatic refresh of the length field.
1299     * @param b if {@code true} the automatic refresh is enabled, else disabled
1300     * @since 17108
1301     */
1302    public void setAutoLength(boolean b) {
1303        autoLength = b;
1304    }
1305
1306}