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