001    // License: GPL. See LICENSE file for details.
002    package org.openstreetmap.josm.gui;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    
007    import java.awt.AWTEvent;
008    import java.awt.Color;
009    import java.awt.Component;
010    import java.awt.Cursor;
011    import java.awt.Dimension;
012    import java.awt.EventQueue;
013    import java.awt.Font;
014    import java.awt.GridBagLayout;
015    import java.awt.Point;
016    import java.awt.SystemColor;
017    import java.awt.Toolkit;
018    import java.awt.event.AWTEventListener;
019    import java.awt.event.InputEvent;
020    import java.awt.event.KeyAdapter;
021    import java.awt.event.KeyEvent;
022    import java.awt.event.MouseAdapter;
023    import java.awt.event.MouseEvent;
024    import java.awt.event.MouseListener;
025    import java.awt.event.MouseMotionListener;
026    import java.util.ArrayList;
027    import java.util.Collection;
028    import java.util.ConcurrentModificationException;
029    import java.util.List;
030    
031    import javax.swing.BorderFactory;
032    import javax.swing.JLabel;
033    import javax.swing.JPanel;
034    import javax.swing.JProgressBar;
035    import javax.swing.JScrollPane;
036    import javax.swing.JTextField;
037    import javax.swing.Popup;
038    import javax.swing.PopupFactory;
039    import javax.swing.UIManager;
040    
041    import org.openstreetmap.josm.Main;
042    import org.openstreetmap.josm.data.coor.CoordinateFormat;
043    import org.openstreetmap.josm.data.coor.LatLon;
044    import org.openstreetmap.josm.data.osm.DataSet;
045    import org.openstreetmap.josm.data.osm.OsmPrimitive;
046    import org.openstreetmap.josm.gui.help.Helpful;
047    import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
048    import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor.ProgressMonitorDialog;
049    import org.openstreetmap.josm.gui.util.GuiHelper;
050    import org.openstreetmap.josm.tools.GBC;
051    import org.openstreetmap.josm.tools.ImageProvider;
052    
053    /**
054     * A component that manages some status information display about the map.
055     * It keeps a status line below the map up to date and displays some tooltip
056     * information if the user hold the mouse long enough at some point.
057     *
058     * All this is done in background to not disturb other processes.
059     *
060     * The background thread does not alter any data of the map (read only thread).
061     * Also it is rather fail safe. In case of some error in the data, it just does
062     * nothing instead of whining and complaining.
063     *
064     * @author imi
065     */
066    public class MapStatus extends JPanel implements Helpful {
067    
068        /**
069         * The MapView this status belongs to.
070         */
071        final MapView mv;
072        final Collector collector;
073    
074        /**
075         * A small user interface component that consists of an image label and
076         * a fixed text content to the right of the image.
077         */
078        static class ImageLabel extends JPanel {
079            static Color backColor = Color.decode("#b8cfe5");
080            static Color backColorActive = Color.decode("#aaff5e");
081                
082            private JLabel tf;
083            private int chars;
084            public ImageLabel(String img, String tooltip, int chars) {
085                super();
086                setLayout(new GridBagLayout());
087                setBackground(backColor);
088                add(new JLabel(ImageProvider.get("statusline/"+img+".png")), GBC.std().anchor(GBC.WEST).insets(0,1,1,0));
089                add(tf = new JLabel(), GBC.std().fill(GBC.BOTH).anchor(GBC.WEST).insets(2,1,1,0));
090                setToolTipText(tooltip);
091                this.chars = chars;
092            }
093            public void setText(String t) {
094                tf.setText(t);
095            }
096            @Override public Dimension getPreferredSize() {
097                return new Dimension(25 + chars*tf.getFontMetrics(tf.getFont()).charWidth('0'), super.getPreferredSize().height);
098            }
099            @Override public Dimension getMinimumSize() {
100                return new Dimension(25 + chars*tf.getFontMetrics(tf.getFont()).charWidth('0'), super.getMinimumSize().height);
101            }
102        }
103    
104        public class BackgroundProgressMonitor implements ProgressMonitorDialog {
105    
106            private String title;
107            private String customText;
108    
109            private void updateText() {
110                if (customText != null && !customText.isEmpty()) {
111                    progressBar.setToolTipText(tr("{0} ({1})", title, customText));
112                } else {
113                    progressBar.setToolTipText(title);
114                }
115            }
116    
117            public void setVisible(boolean visible) {
118                progressBar.setVisible(visible);
119            }
120    
121            public void updateProgress(int progress) {
122                progressBar.setValue(progress);
123                MapStatus.this.doLayout();
124            }
125    
126            public void setCustomText(String text) {
127                this.customText = text;
128                updateText();
129            }
130    
131            public void setCurrentAction(String text) {
132                this.title = text;
133                updateText();
134            }
135    
136            public void setIndeterminate(boolean newValue) {
137                UIManager.put("ProgressBar.cycleTime", UIManager.getInt("ProgressBar.repaintInterval") * 100);
138                progressBar.setIndeterminate(newValue);
139            }
140    
141            @Override
142            public void appendLogMessage(String message) {
143                if (message != null && !message.isEmpty()) {
144                    System.out.println("appendLogMessage not implemented for background tasks. Message was: " + message);
145                }
146            }
147    
148        }
149    
150        final ImageLabel lonText = new ImageLabel("lon", tr("The geographic longitude at the mouse pointer."), 11);
151        final ImageLabel nameText = new ImageLabel("name", tr("The name of the object at the mouse pointer."), 20);
152        final JTextField helpText = new JTextField();
153        final ImageLabel latText = new ImageLabel("lat", tr("The geographic latitude at the mouse pointer."), 11);
154        final ImageLabel angleText = new ImageLabel("angle", tr("The angle between the previous and the current way segment."), 6);
155        final ImageLabel headingText = new ImageLabel("heading", tr("The (compass) heading of the line segment being drawn."), 6);
156        final ImageLabel distText = new ImageLabel("dist", tr("The length of the new way segment being drawn."), 10);
157        final JProgressBar progressBar = new JProgressBar();
158        public final BackgroundProgressMonitor progressMonitor = new BackgroundProgressMonitor();
159    
160        /**
161         * This is the thread that runs in the background and collects the information displayed.
162         * It gets destroyed by MapFrame.java/destroy() when the MapFrame itself is destroyed.
163         */
164        public Thread thread;
165    
166        private final List<StatusTextHistory> statusText = new ArrayList<StatusTextHistory>();
167    
168        private static class StatusTextHistory {
169            final Object id;
170            final String text;
171    
172            public StatusTextHistory(Object id, String text) {
173                this.id = id;
174                this.text = text;
175            }
176    
177            @Override
178            public boolean equals(Object obj) {
179                return obj instanceof StatusTextHistory && ((StatusTextHistory)obj).id == id;
180            }
181    
182            @Override
183            public int hashCode() {
184                return System.identityHashCode(id);
185            }
186        }
187    
188        /**
189         * The collector class that waits for notification and then update
190         * the display objects.
191         *
192         * @author imi
193         */
194        private final class Collector implements Runnable {
195            /**
196             * the mouse position of the previous iteration. This is used to show
197             * the popup until the cursor is moved.
198             */
199            private Point oldMousePos;
200            /**
201             * Contains the labels that are currently shown in the information
202             * popup
203             */
204            private List<JLabel> popupLabels = null;
205            /**
206             * The popup displayed to show additional information
207             */
208            private Popup popup;
209    
210            private MapFrame parent;
211    
212            public Collector(MapFrame parent) {
213                this.parent = parent;
214            }
215    
216            /**
217             * Execution function for the Collector.
218             */
219            public void run() {
220                registerListeners();
221                try {
222                    for (;;) {
223    
224                        final MouseState ms = new MouseState();
225                        synchronized (this) {
226                            // TODO Would be better if the timeout wasn't necessary
227                            try {wait(1000);} catch (InterruptedException e) {}
228                            ms.modifiers = mouseState.modifiers;
229                            ms.mousePos = mouseState.mousePos;
230                        }
231                        if (parent != Main.map)
232                            return; // exit, if new parent.
233    
234                        // Do nothing, if required data is missing
235                        if(ms.mousePos == null || mv.center == null) {
236                            continue;
237                        }
238    
239                        try {
240                            EventQueue.invokeAndWait(new Runnable() {
241    
242                                @Override
243                                public void run() {
244                                    // Freeze display when holding down CTRL
245                                    if ((ms.modifiers & MouseEvent.CTRL_DOWN_MASK) != 0) {
246                                        // update the information popup's labels though, because
247                                        // the selection might have changed from the outside
248                                        popupUpdateLabels();
249                                        return;
250                                    }
251    
252                                    // This try/catch is a hack to stop the flooding bug reports about this.
253                                    // The exception needed to handle with in the first place, means that this
254                                    // access to the data need to be restarted, if the main thread modifies
255                                    // the data.
256                                    DataSet ds = null;
257                                    // The popup != null check is required because a left-click
258                                    // produces several events as well, which would make this
259                                    // variable true. Of course we only want the popup to show
260                                    // if the middle mouse button has been pressed in the first
261                                    // place
262                                    boolean mouseNotMoved = oldMousePos != null
263                                            && oldMousePos.equals(ms.mousePos);
264                                    boolean isAtOldPosition = mouseNotMoved && popup != null;
265                                    boolean middleMouseDown = (ms.modifiers & MouseEvent.BUTTON2_DOWN_MASK) != 0;
266                                    try {
267                                        ds = mv.getCurrentDataSet();
268                                        if (ds != null) {
269                                            // This is not perfect, if current dataset was changed during execution, the lock would be useless
270                                            if(isAtOldPosition && middleMouseDown) {
271                                                // Write lock is necessary when selecting in popupCycleSelection
272                                                // locks can not be upgraded -> if do read lock here and write lock later (in OsmPrimitive.updateFlags)
273                                                // then always occurs deadlock (#5814)
274                                                ds.beginUpdate();
275                                            } else {
276                                                ds.getReadLock().lock();
277                                            }
278                                        }
279    
280                                        // Set the text label in the bottom status bar
281                                        // "if mouse moved only" was added to stop heap growing
282                                        if (!mouseNotMoved) statusBarElementUpdate(ms);
283    
284    
285                                        // Popup Information
286                                        // display them if the middle mouse button is pressed and
287                                        // keep them until the mouse is moved
288                                        if (middleMouseDown || isAtOldPosition)
289                                        {
290                                            Collection<OsmPrimitive> osms = mv.getAllNearest(ms.mousePos, OsmPrimitive.isUsablePredicate);
291    
292                                            if (osms == null)
293                                                return;
294    
295                                            final JPanel c = new JPanel(new GridBagLayout());
296                                            final JLabel lbl = new JLabel(
297                                                    "<html>"+tr("Middle click again to cycle through.<br>"+
298                                                            "Hold CTRL to select directly from this list with the mouse.<hr>")+"</html>",
299                                                            null,
300                                                            JLabel.HORIZONTAL
301                                                    );
302                                            lbl.setHorizontalAlignment(JLabel.LEFT);
303                                            c.add(lbl, GBC.eol().insets(2, 0, 2, 0));
304    
305                                            // Only cycle if the mouse has not been moved and the
306                                            // middle mouse button has been pressed at least twice
307                                            // (the reason for this is the popup != null check for
308                                            // isAtOldPosition, see above. This is a nice side
309                                            // effect though, because it does not change selection
310                                            // of the first middle click)
311                                            if(isAtOldPosition && middleMouseDown) {
312                                                // Hand down mouse modifiers so the SHIFT mod can be
313                                                // handled correctly (see funcion)
314                                                popupCycleSelection(osms, ms.modifiers);
315                                            }
316    
317                                            // These labels may need to be updated from the outside
318                                            // so collect them
319                                            List<JLabel> lbls = new ArrayList<JLabel>();
320                                            for (final OsmPrimitive osm : osms) {
321                                                JLabel l = popupBuildPrimitiveLabels(osm);
322                                                lbls.add(l);
323                                                c.add(l, GBC.eol().fill(GBC.HORIZONTAL).insets(2, 0, 2, 2));
324                                            }
325    
326                                            popupShowPopup(popupCreatePopup(c, ms), lbls);
327                                        } else {
328                                            popupHidePopup();
329                                        }
330    
331                                        oldMousePos = ms.mousePos;
332                                    } catch (ConcurrentModificationException x) {
333                                        //x.printStackTrace();
334                                    } catch (NullPointerException x) {
335                                        //x.printStackTrace();
336                                    } finally {
337                                        if (ds != null) {
338                                            if(isAtOldPosition && middleMouseDown) {
339                                                ds.endUpdate();
340                                            } else {
341                                                ds.getReadLock().unlock();
342                                            }
343                                        }
344                                    }
345                                }
346                            });
347                        } catch (Exception e) {
348    
349                        }
350                    }
351                } finally {
352                    unregisterListeners();
353                }
354            }
355    
356            /**
357             * Creates a popup for the given content next to the cursor. Tries to
358             * keep the popup on screen and shows a vertical scrollbar, if the
359             * screen is too small.
360             * @param content
361             * @param ms
362             * @return popup
363             */
364            private final Popup popupCreatePopup(Component content, MouseState ms) {
365                Point p = mv.getLocationOnScreen();
366                Dimension scrn = Toolkit.getDefaultToolkit().getScreenSize();
367    
368                // Create a JScrollPane around the content, in case there's not
369                // enough space
370                JScrollPane sp = new JScrollPane(content);
371                sp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
372                sp.setBorder(BorderFactory.createRaisedBevelBorder());
373                // Implement max-size content-independent
374                Dimension prefsize = sp.getPreferredSize();
375                int w = Math.min(prefsize.width, Math.min(800, (scrn.width/2) - 16));
376                int h = Math.min(prefsize.height, scrn.height - 10);
377                sp.setPreferredSize(new Dimension(w, h));
378    
379                int xPos = p.x + ms.mousePos.x + 16;
380                // Display the popup to the left of the cursor if it would be cut
381                // off on its right, but only if more space is available
382                if(xPos + w > scrn.width && xPos > scrn.width/2) {
383                    xPos = p.x + ms.mousePos.x - 4 - w;
384                }
385                int yPos = p.y + ms.mousePos.y + 16;
386                // Move the popup up if it would be cut off at its bottom but do not
387                // move it off screen on the top
388                if(yPos + h > scrn.height - 5) {
389                    yPos = Math.max(5, scrn.height - h - 5);
390                }
391    
392                PopupFactory pf = PopupFactory.getSharedInstance();
393                return pf.getPopup(mv, sp, xPos, yPos);
394            }
395    
396            /**
397             * Calls this to update the element that is shown in the statusbar
398             * @param ms
399             */
400            private final void statusBarElementUpdate(MouseState ms) {
401                final OsmPrimitive osmNearest = mv.getNearestNodeOrWay(ms.mousePos, OsmPrimitive.isUsablePredicate, false);
402                if (osmNearest != null) {
403                    nameText.setText(osmNearest.getDisplayName(DefaultNameFormatter.getInstance()));
404                } else {
405                    nameText.setText(tr("(no object)"));
406                }
407            }
408    
409            /**
410             * Call this with a set of primitives to cycle through them. Method
411             * will automatically select the next item and update the map
412             * @param osms
413             * @param mouse modifiers
414             */
415            private final void popupCycleSelection(Collection<OsmPrimitive> osms, int mods) {
416                DataSet ds = Main.main.getCurrentDataSet();
417                // Find some items that are required for cycling through
418                OsmPrimitive firstItem = null;
419                OsmPrimitive firstSelected = null;
420                OsmPrimitive nextSelected = null;
421                for (final OsmPrimitive osm : osms) {
422                    if(firstItem == null) {
423                        firstItem = osm;
424                    }
425                    if(firstSelected != null && nextSelected == null) {
426                        nextSelected = osm;
427                    }
428                    if(firstSelected == null && ds.isSelected(osm)) {
429                        firstSelected = osm;
430                    }
431                }
432    
433                // Clear previous selection if SHIFT (add to selection) is not
434                // pressed. Cannot use "setSelected()" because it will cause a
435                // fireSelectionChanged event which is unnecessary at this point.
436                if((mods & MouseEvent.SHIFT_DOWN_MASK) == 0) {
437                    ds.clearSelection();
438                }
439    
440                // This will cycle through the available items.
441                if(firstSelected == null) {
442                    ds.addSelected(firstItem);
443                } else {
444                    ds.clearSelection(firstSelected);
445                    if(nextSelected != null) {
446                        ds.addSelected(nextSelected);
447                    }
448                }
449            }
450    
451            /**
452             * Tries to hide the given popup
453             * @param popup
454             */
455            private final void popupHidePopup() {
456                popupLabels = null;
457                if(popup == null)
458                    return;
459                final Popup staticPopup = popup;
460                popup = null;
461                EventQueue.invokeLater(new Runnable(){
462                    public void run() { staticPopup.hide(); }});
463            }
464    
465            /**
466             * Tries to show the given popup, can be hidden using popupHideOldPopup
467             * If an old popup exists, it will be automatically hidden
468             * @param popup
469             */
470            private final void popupShowPopup(Popup newPopup, List<JLabel> lbls) {
471                final Popup staticPopup = newPopup;
472                if(this.popup != null) {
473                    // If an old popup exists, remove it when the new popup has been
474                    // drawn to keep flickering to a minimum
475                    final Popup staticOldPopup = this.popup;
476                    EventQueue.invokeLater(new Runnable(){
477                        public void run() {
478                            staticPopup.show();
479                            staticOldPopup.hide();
480                        }
481                    });
482                } else {
483                    // There is no old popup
484                    EventQueue.invokeLater(new Runnable(){
485                        public void run() { staticPopup.show(); }});
486                }
487                this.popupLabels = lbls;
488                this.popup = newPopup;
489            }
490    
491            /**
492             * This method should be called if the selection may have changed from
493             * outside of this class. This is the case when CTRL is pressed and the
494             * user clicks on the map instead of the popup.
495             */
496            private final void popupUpdateLabels() {
497                if(this.popup == null || this.popupLabels == null)
498                    return;
499                for(JLabel l : this.popupLabels) {
500                    l.validate();
501                }
502            }
503    
504            /**
505             * Sets the colors for the given label depending on the selected status of
506             * the given OsmPrimitive
507             *
508             * @param lbl The label to color
509             * @param osm The primitive to derive the colors from
510             */
511            private final void popupSetLabelColors(JLabel lbl, OsmPrimitive osm) {
512                DataSet ds = Main.main.getCurrentDataSet();
513                if(ds.isSelected(osm)) {
514                    lbl.setBackground(SystemColor.textHighlight);
515                    lbl.setForeground(SystemColor.textHighlightText);
516                } else {
517                    lbl.setBackground(SystemColor.control);
518                    lbl.setForeground(SystemColor.controlText);
519                }
520            }
521    
522            /**
523             * Builds the labels with all necessary listeners for the info popup for the
524             * given OsmPrimitive
525             * @param osm  The primitive to create the label for
526             * @return
527             */
528            private final JLabel popupBuildPrimitiveLabels(final OsmPrimitive osm) {
529                final StringBuilder text = new StringBuilder();
530                String name = osm.getDisplayName(DefaultNameFormatter.getInstance());
531                if (osm.isNewOrUndeleted() || osm.isModified()) {
532                    name = "<i><b>"+ name + "*</b></i>";
533                }
534                text.append(name);
535                
536                boolean idShown = Main.pref.getBoolean("osm-primitives.showid");
537                // fix #7557 - do not show ID twice
538                
539                if (!osm.isNew() && !idShown) {
540                    text.append(" [id="+osm.getId()+"]");
541                }
542    
543                if(osm.getUser() != null) {
544                    text.append(" [" + tr("User:") + " " + osm.getUser().getName() + "]");
545                }
546    
547                for (String key : osm.keySet()) {
548                    text.append("<br>" + key + "=" + osm.get(key));
549                }
550    
551                final JLabel l = new JLabel(
552                        "<html>" +text.toString() + "</html>",
553                        ImageProvider.get(osm.getDisplayType()),
554                        JLabel.HORIZONTAL
555                        ) {
556                    // This is necessary so the label updates its colors when the
557                    // selection is changed from the outside
558                    @Override public void validate() {
559                        super.validate();
560                        popupSetLabelColors(this, osm);
561                    }
562                };
563                l.setOpaque(true);
564                popupSetLabelColors(l, osm);
565                l.setFont(l.getFont().deriveFont(Font.PLAIN));
566                l.setVerticalTextPosition(JLabel.TOP);
567                l.setHorizontalAlignment(JLabel.LEFT);
568                l.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
569                l.addMouseListener(new MouseAdapter(){
570                    @Override public void mouseEntered(MouseEvent e) {
571                        l.setBackground(SystemColor.info);
572                        l.setForeground(SystemColor.infoText);
573                    }
574                    @Override public void mouseExited(MouseEvent e) {
575                        popupSetLabelColors(l, osm);
576                    }
577                    @Override public void mouseClicked(MouseEvent e) {
578                        DataSet ds = Main.main.getCurrentDataSet();
579                        // Let the user toggle the selection
580                        ds.toggleSelected(osm);
581                        l.validate();
582                    }
583                });
584                // Sometimes the mouseEntered event is not catched, thus the label
585                // will not be highlighted, making it confusing. The MotionListener
586                // can correct this defect.
587                l.addMouseMotionListener(new MouseMotionListener() {
588                    public void mouseMoved(MouseEvent e) {
589                        l.setBackground(SystemColor.info);
590                        l.setForeground(SystemColor.infoText);
591                    }
592                    public void mouseDragged(MouseEvent e) {
593                        l.setBackground(SystemColor.info);
594                        l.setForeground(SystemColor.infoText);
595                    }
596                });
597                return l;
598            }
599        }
600    
601        /**
602         * Everything, the collector is interested of. Access must be synchronized.
603         * @author imi
604         */
605        static class MouseState {
606            Point mousePos;
607            int modifiers;
608        }
609        /**
610         * The last sent mouse movement event.
611         */
612        MouseState mouseState = new MouseState();
613    
614        private AWTEventListener awtListener = new AWTEventListener() {
615            public void eventDispatched(AWTEvent event) {
616                if (event instanceof InputEvent &&
617                        ((InputEvent)event).getComponent() == mv) {
618                    synchronized (collector) {
619                        mouseState.modifiers = ((InputEvent)event).getModifiersEx();
620                        if (event instanceof MouseEvent) {
621                            mouseState.mousePos = ((MouseEvent)event).getPoint();
622                        }
623                        collector.notify();
624                    }
625                }
626            }
627        };
628    
629        private MouseMotionListener mouseMotionListener = new MouseMotionListener() {
630            public void mouseMoved(MouseEvent e) {
631                synchronized (collector) {
632                    mouseState.modifiers = e.getModifiersEx();
633                    mouseState.mousePos = e.getPoint();
634                    collector.notify();
635                }
636            }
637    
638            public void mouseDragged(MouseEvent e) {
639                mouseMoved(e);
640            }
641        };
642    
643        private KeyAdapter keyAdapter = new KeyAdapter() {
644            @Override public void keyPressed(KeyEvent e) {
645                synchronized (collector) {
646                    mouseState.modifiers = e.getModifiersEx();
647                    collector.notify();
648                }
649            }
650    
651            @Override public void keyReleased(KeyEvent e) {
652                keyPressed(e);
653            }
654        };
655    
656        private void registerListeners() {
657            // Listen to keyboard/mouse events for pressing/releasing alt key and
658            // inform the collector.
659            try {
660                Toolkit.getDefaultToolkit().addAWTEventListener(awtListener,
661                        AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK);
662            } catch (SecurityException ex) {
663                mv.addMouseMotionListener(mouseMotionListener);
664                mv.addKeyListener(keyAdapter);
665            }
666        }
667    
668        private void unregisterListeners() {
669            try {
670                Toolkit.getDefaultToolkit().removeAWTEventListener(awtListener);
671            } catch (SecurityException e) {
672                // Don't care, awtListener probably wasn't registered anyway
673            }
674            mv.removeMouseMotionListener(mouseMotionListener);
675            mv.removeKeyListener(keyAdapter);
676        }
677    
678    
679        /**
680         * Construct a new MapStatus and attach it to the map view.
681         * @param mapFrame The MapFrame the status line is part of.
682         */
683        public MapStatus(final MapFrame mapFrame) {
684            this.mv = mapFrame.mapView;
685            this.collector = new Collector(mapFrame);
686    
687            lonText.addMouseListener(Main.main.menu.jumpToAct);
688            latText.addMouseListener(Main.main.menu.jumpToAct);
689            
690            // Listen for mouse movements and set the position text field
691            mv.addMouseMotionListener(new MouseMotionListener(){
692                public void mouseDragged(MouseEvent e) {
693                    mouseMoved(e);
694                }
695                public void mouseMoved(MouseEvent e) {
696                    if (mv.center == null)
697                        return;
698                    // Do not update the view if ctrl is pressed.
699                    if ((e.getModifiersEx() & MouseEvent.CTRL_DOWN_MASK) == 0) {
700                        CoordinateFormat mCord = CoordinateFormat.getDefaultFormat();
701                        LatLon p = mv.getLatLon(e.getX(),e.getY());
702                        latText.setText(p.latToString(mCord));
703                        lonText.setText(p.lonToString(mCord));
704                    }
705                }
706            });
707    
708            setLayout(new GridBagLayout());
709            setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
710    
711            add(latText, GBC.std());
712            add(lonText, GBC.std().insets(3,0,0,0));
713            add(headingText, GBC.std().insets(3,0,0,0));
714            add(angleText, GBC.std().insets(3,0,0,0));
715            add(distText, GBC.std().insets(3,0,0,0));
716    
717            helpText.setEditable(false);
718            add(nameText, GBC.std().insets(3,0,0,0));
719            add(helpText, GBC.std().insets(3,0,0,0).fill(GBC.HORIZONTAL));
720    
721            progressBar.setMaximum(PleaseWaitProgressMonitor.PROGRESS_BAR_MAX);
722            progressBar.setVisible(false);
723            GBC gbc = GBC.eol();
724            gbc.ipadx = 100;
725            add(progressBar,gbc);
726            progressBar.addMouseListener(new MouseAdapter() {
727                @Override
728                public void mouseClicked(MouseEvent e) {
729                    PleaseWaitProgressMonitor monitor = Main.currentProgressMonitor;
730                    if (monitor != null) {
731                        monitor.showForegroundDialog();
732                    }
733                }
734            });
735    
736            // The background thread
737            thread = new Thread(collector, "Map Status Collector");
738            thread.setDaemon(true);
739            thread.start();
740        }
741        
742        public JPanel getAnglePanel() {
743            return angleText;
744        }
745    
746        public String helpTopic() {
747            return ht("/Statusline");
748        }
749    
750        @Override
751        public synchronized void addMouseListener(MouseListener ml) {
752            //super.addMouseListener(ml);
753            lonText.addMouseListener(ml);
754            latText.addMouseListener(ml);
755        }
756    
757        public void setHelpText(String t) {
758            setHelpText(null, t);
759        }
760        public void setHelpText(Object id, final String text)  {
761    
762            StatusTextHistory entry = new StatusTextHistory(id, text);
763    
764            statusText.remove(entry);
765            statusText.add(entry);
766    
767            GuiHelper.runInEDT(new Runnable() {
768                @Override
769                public void run() {
770                    helpText.setText(text);
771                    helpText.setToolTipText(text);
772                }
773            });
774        }
775        public void resetHelpText(Object id) {
776            if (statusText.isEmpty())
777                return;
778    
779            StatusTextHistory entry = new StatusTextHistory(id, null);
780            if (statusText.get(statusText.size() - 1).equals(entry)) {
781                if (statusText.size() == 1) {
782                    setHelpText("");
783                } else {
784                    StatusTextHistory history = statusText.get(statusText.size() - 2);
785                    setHelpText(history.id, history.text);
786                }
787            }
788            statusText.remove(entry);
789        }
790        public void setAngle(double a) {
791            angleText.setText(a < 0 ? "--" : Math.round(a*10)/10.0 + " \u00B0");
792        }
793        public void setHeading(double h) {
794            headingText.setText(h < 0 ? "--" : Math.round(h*10)/10.0 + " \u00B0");
795        }
796        public void setDist(double dist) {
797            distText.setText(dist < 0 ? "--" : NavigatableComponent.getDistText(dist));
798        }
799        public void activateAnglePanel(boolean activeFlag) {
800            angleText.setBackground(activeFlag ? ImageLabel.backColorActive : ImageLabel.backColor);
801        }
802    }