001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Component;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Window;
011import java.awt.event.ComponentEvent;
012import java.awt.event.ComponentListener;
013import java.awt.event.KeyEvent;
014import java.awt.event.WindowAdapter;
015import java.awt.event.WindowEvent;
016import java.io.File;
017import java.lang.ref.WeakReference;
018import java.net.URI;
019import java.net.URISyntaxException;
020import java.net.URL;
021import java.text.MessageFormat;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.StringTokenizer;
031import java.util.concurrent.Callable;
032import java.util.concurrent.ExecutorService;
033import java.util.concurrent.Executors;
034import java.util.concurrent.Future;
035
036import javax.swing.Action;
037import javax.swing.InputMap;
038import javax.swing.JComponent;
039import javax.swing.JFrame;
040import javax.swing.JOptionPane;
041import javax.swing.JPanel;
042import javax.swing.JTextArea;
043import javax.swing.KeyStroke;
044import javax.swing.UIManager;
045import javax.swing.UnsupportedLookAndFeelException;
046
047import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
048import org.openstreetmap.josm.actions.JosmAction;
049import org.openstreetmap.josm.actions.OpenFileAction;
050import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask;
051import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
052import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
053import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
054import org.openstreetmap.josm.actions.mapmode.DrawAction;
055import org.openstreetmap.josm.actions.mapmode.MapMode;
056import org.openstreetmap.josm.actions.search.SearchAction;
057import org.openstreetmap.josm.data.Bounds;
058import org.openstreetmap.josm.data.Preferences;
059import org.openstreetmap.josm.data.UndoRedoHandler;
060import org.openstreetmap.josm.data.ViewportData;
061import org.openstreetmap.josm.data.coor.CoordinateFormat;
062import org.openstreetmap.josm.data.coor.LatLon;
063import org.openstreetmap.josm.data.osm.DataSet;
064import org.openstreetmap.josm.data.osm.OsmPrimitive;
065import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
066import org.openstreetmap.josm.data.projection.Projection;
067import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
068import org.openstreetmap.josm.data.validation.OsmValidator;
069import org.openstreetmap.josm.gui.GettingStarted;
070import org.openstreetmap.josm.gui.MainApplication.Option;
071import org.openstreetmap.josm.gui.MainMenu;
072import org.openstreetmap.josm.gui.MapFrame;
073import org.openstreetmap.josm.gui.MapFrameListener;
074import org.openstreetmap.josm.gui.MapView;
075import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
076import org.openstreetmap.josm.gui.help.HelpUtil;
077import org.openstreetmap.josm.gui.io.SaveLayersDialog;
078import org.openstreetmap.josm.gui.layer.Layer;
079import org.openstreetmap.josm.gui.layer.OsmDataLayer;
080import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
081import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
082import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
083import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
084import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
085import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
086import org.openstreetmap.josm.gui.progress.ProgressMonitorExecutor;
087import org.openstreetmap.josm.gui.tagging.TaggingPresets;
088import org.openstreetmap.josm.gui.util.RedirectInputMap;
089import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
090import org.openstreetmap.josm.io.FileWatcher;
091import org.openstreetmap.josm.io.OsmApi;
092import org.openstreetmap.josm.tools.CheckParameterUtil;
093import org.openstreetmap.josm.tools.I18n;
094import org.openstreetmap.josm.tools.ImageProvider;
095import org.openstreetmap.josm.tools.OpenBrowser;
096import org.openstreetmap.josm.tools.OsmUrlToBounds;
097import org.openstreetmap.josm.tools.PlatformHook;
098import org.openstreetmap.josm.tools.PlatformHookOsx;
099import org.openstreetmap.josm.tools.PlatformHookUnixoid;
100import org.openstreetmap.josm.tools.PlatformHookWindows;
101import org.openstreetmap.josm.tools.Shortcut;
102import org.openstreetmap.josm.tools.Utils;
103import org.openstreetmap.josm.tools.WindowGeometry;
104
105/**
106 * Abstract class holding various static global variables and methods used in large parts of JOSM application.
107 * @since 98
108 */
109public abstract class Main {
110
111    /**
112     * The JOSM website URL.
113     * @since 6897 (was public from 6143 to 6896)
114     */
115    private static final String JOSM_WEBSITE = "https://josm.openstreetmap.de";
116
117    /**
118     * The OSM website URL.
119     * @since 6897 (was public from 6453 to 6896)
120     */
121    private static final String OSM_WEBSITE = "https://www.openstreetmap.org";
122
123    /**
124     * Replies true if JOSM currently displays a map view. False, if it doesn't, i.e. if
125     * it only shows the MOTD panel.
126     *
127     * @return <code>true</code> if JOSM currently displays a map view
128     */
129    public static boolean isDisplayingMapView() {
130        if (map == null) return false;
131        if (map.mapView == null) return false;
132        return true;
133    }
134
135    /**
136     * Global parent component for all dialogs and message boxes
137     */
138    public static Component parent;
139
140    /**
141     * Global application.
142     */
143    public static Main main;
144
145    /**
146     * Command-line arguments used to run the application.
147     */
148    public static String[] commandLineArgs;
149
150    /**
151     * The worker thread slave. This is for executing all long and intensive
152     * calculations. The executed runnables are guaranteed to be executed separately
153     * and sequential.
154     */
155    public static final ExecutorService worker = new ProgressMonitorExecutor();
156
157    /**
158     * Global application preferences
159     */
160    public static Preferences pref;
161
162    /**
163     * The global paste buffer.
164     */
165    public static final PrimitiveDeepCopy pasteBuffer = new PrimitiveDeepCopy();
166
167    /**
168     * The layer source from which {@link Main#pasteBuffer} data comes from.
169     */
170    public static Layer pasteSource;
171
172    /**
173     * The MapFrame. Use {@link Main#setMapFrame} to set or clear it.
174     */
175    public static MapFrame map;
176
177    /**
178     * The toolbar preference control to register new actions.
179     */
180    public static ToolbarPreferences toolbar;
181
182    /**
183     * The commands undo/redo handler.
184     */
185    public final UndoRedoHandler undoRedo = new UndoRedoHandler();
186
187    /**
188     * The progress monitor being currently displayed.
189     */
190    public static PleaseWaitProgressMonitor currentProgressMonitor;
191
192    /**
193     * The main menu bar at top of screen.
194     */
195    public MainMenu menu;
196
197    /**
198     * The data validation handler.
199     */
200    public OsmValidator validator;
201
202    /**
203     * The file watcher service.
204     */
205    public static final FileWatcher fileWatcher = new FileWatcher();
206
207    /**
208     * The MOTD Layer.
209     */
210    private GettingStarted gettingStarted = new GettingStarted();
211
212    private static final Collection<MapFrameListener> mapFrameListeners = new ArrayList<>();
213
214    protected static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>();
215
216    /**
217     * Logging level (5 = trace, 4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none).
218     * @since 6248
219     */
220    public static int logLevel = 3;
221
222    /**
223     * Prints an error message if logging is on.
224     * @param msg The message to print.
225     * @since 6248
226     */
227    public static void error(String msg) {
228        if (logLevel < 1)
229            return;
230        if (msg != null && !msg.isEmpty()) {
231            System.err.println(tr("ERROR: {0}", msg));
232        }
233    }
234
235    /**
236     * Prints a warning message if logging is on.
237     * @param msg The message to print.
238     */
239    public static void warn(String msg) {
240        if (logLevel < 2)
241            return;
242        if (msg != null && !msg.isEmpty()) {
243            System.err.println(tr("WARNING: {0}", msg));
244        }
245    }
246
247    /**
248     * Prints an informational message if logging is on.
249     * @param msg The message to print.
250     */
251    public static void info(String msg) {
252        if (logLevel < 3)
253            return;
254        if (msg != null && !msg.isEmpty()) {
255            System.out.println(tr("INFO: {0}", msg));
256        }
257    }
258
259    /**
260     * Prints a debug message if logging is on.
261     * @param msg The message to print.
262     */
263    public static void debug(String msg) {
264        if (logLevel < 4)
265            return;
266        if (msg != null && !msg.isEmpty()) {
267            System.out.println(tr("DEBUG: {0}", msg));
268        }
269    }
270
271    /**
272     * Prints a trace message if logging is on.
273     * @param msg The message to print.
274     */
275    public static void trace(String msg) {
276        if (logLevel < 5)
277            return;
278        if (msg != null && !msg.isEmpty()) {
279            System.out.print("TRACE: ");
280            System.out.println(msg);
281        }
282    }
283
284    /**
285     * Determines if debug log level is enabled.
286     * Useful to avoid costly construction of debug messages when not enabled.
287     * @return {@code true} if log level is at least debug, {@code false} otherwise
288     * @since 6852
289     */
290    public static boolean isDebugEnabled() {
291        return logLevel >= 4;
292    }
293
294    /**
295     * Determines if trace log level is enabled.
296     * Useful to avoid costly construction of trace messages when not enabled.
297     * @return {@code true} if log level is at least trace, {@code false} otherwise
298     * @since 6852
299     */
300    public static boolean isTraceEnabled() {
301        return logLevel >= 5;
302    }
303
304    /**
305     * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format}
306     * function to format text.
307     * @param msg The formatted message to print.
308     * @param objects The objects to insert into format string.
309     * @since 6248
310     */
311    public static void error(String msg, Object... objects) {
312        error(MessageFormat.format(msg, objects));
313    }
314
315    /**
316     * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format}
317     * function to format text.
318     * @param msg The formatted message to print.
319     * @param objects The objects to insert into format string.
320     */
321    public static void warn(String msg, Object... objects) {
322        warn(MessageFormat.format(msg, objects));
323    }
324
325    /**
326     * Prints a formatted informational message if logging is on. Calls {@link MessageFormat#format}
327     * function to format text.
328     * @param msg The formatted message to print.
329     * @param objects The objects to insert into format string.
330     */
331    public static void info(String msg, Object... objects) {
332        info(MessageFormat.format(msg, objects));
333    }
334
335    /**
336     * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format}
337     * function to format text.
338     * @param msg The formatted message to print.
339     * @param objects The objects to insert into format string.
340     */
341    public static void debug(String msg, Object... objects) {
342        debug(MessageFormat.format(msg, objects));
343    }
344
345    /**
346     * Prints an error message for the given Throwable.
347     * @param t The throwable object causing the error
348     * @since 6248
349     */
350    public static void error(Throwable t) {
351        error(t, true);
352    }
353
354    /**
355     * Prints a warning message for the given Throwable.
356     * @param t The throwable object causing the error
357     * @since 6248
358     */
359    public static void warn(Throwable t) {
360        warn(t, true);
361    }
362
363    /**
364     * Prints an error message for the given Throwable.
365     * @param t The throwable object causing the error
366     * @param stackTrace {@code true}, if the stacktrace should be displayed
367     * @since 6642
368     */
369    public static void error(Throwable t, boolean stackTrace) {
370        error(getErrorMessage(t));
371        if (stackTrace) {
372            t.printStackTrace();
373        }
374    }
375
376    /**
377     * Prints a warning message for the given Throwable.
378     * @param t The throwable object causing the error
379     * @param stackTrace {@code true}, if the stacktrace should be displayed
380     * @since 6642
381     */
382    public static void warn(Throwable t, boolean stackTrace) {
383        warn(getErrorMessage(t));
384        if (stackTrace) {
385            t.printStackTrace();
386        }
387    }
388
389    /**
390     * Returns a human-readable message of error, also usable for developers.
391     * @param t The error
392     * @return The human-readable error message
393     * @since 6642
394     */
395    public static String getErrorMessage(Throwable t) {
396        if (t == null) {
397            return null;
398        }
399        StringBuilder sb = new StringBuilder(t.getClass().getName());
400        String msg = t.getMessage();
401        if (msg != null) {
402            sb.append(": ").append(msg.trim());
403        }
404        Throwable cause = t.getCause();
405        if (cause != null && !cause.equals(t)) {
406            sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
407        }
408        return sb.toString();
409    }
410
411    /**
412     * Platform specific code goes in here.
413     * Plugins may replace it, however, some hooks will be called before any plugins have been loeaded.
414     * So if you need to hook into those early ones, split your class and send the one with the early hooks
415     * to the JOSM team for inclusion.
416     */
417    public static PlatformHook platform;
418
419    /**
420     * Whether or not the java vm is openjdk
421     * We use this to work around openjdk bugs
422     */
423    public static boolean isOpenjdk;
424
425    /**
426     * Initializes {@code Main.pref} in normal application context.
427     * @since 6471
428     */
429    public static void initApplicationPreferences() {
430        Main.pref = new Preferences();
431    }
432
433    /**
434     * Set or clear (if passed <code>null</code>) the map.
435     * @param map The map to set {@link Main#map} to. Can be null.
436     */
437    public final void setMapFrame(final MapFrame map) {
438        MapFrame old = Main.map;
439        panel.setVisible(false);
440        panel.removeAll();
441        if (map != null) {
442            map.fillPanel(panel);
443        } else {
444            old.destroy();
445            panel.add(gettingStarted, BorderLayout.CENTER);
446        }
447        panel.setVisible(true);
448        redoUndoListener.commandChanged(0,0);
449
450        Main.map = map;
451
452        for (MapFrameListener listener : mapFrameListeners ) {
453            listener.mapFrameInitialized(old, map);
454        }
455        if (map == null && currentProgressMonitor != null) {
456            currentProgressMonitor.showForegroundDialog();
457        }
458    }
459
460    /**
461     * Remove the specified layer from the map. If it is the last layer,
462     * remove the map as well.
463     * @param layer The layer to remove
464     */
465    public final synchronized void removeLayer(final Layer layer) {
466        if (map != null) {
467            map.mapView.removeLayer(layer);
468            if (isDisplayingMapView() && map.mapView.getAllLayers().isEmpty()) {
469                setMapFrame(null);
470            }
471        }
472    }
473
474    private static InitStatusListener initListener = null;
475
476    public static interface InitStatusListener {
477
478        void updateStatus(String event);
479    }
480
481    public static void setInitStatusListener(InitStatusListener listener) {
482        initListener = listener;
483    }
484
485    /**
486     * Constructs new {@code Main} object. A lot of global variables are initialized here.
487     */
488    public Main() {
489        main = this;
490        isOpenjdk = System.getProperty("java.vm.name").toUpperCase().indexOf("OPENJDK") != -1;
491
492        if (initListener != null) {
493            initListener.updateStatus(tr("Executing platform startup hook"));
494        }
495        platform.startupHook();
496
497        if (initListener != null) {
498            initListener.updateStatus(tr("Building main menu"));
499        }
500        contentPanePrivate.add(panel, BorderLayout.CENTER);
501        panel.add(gettingStarted, BorderLayout.CENTER);
502        menu = new MainMenu();
503
504        undoRedo.addCommandQueueListener(redoUndoListener);
505
506        // creating toolbar
507        contentPanePrivate.add(toolbar.control, BorderLayout.NORTH);
508
509        registerActionShortcut(menu.help, Shortcut.registerShortcut("system:help", tr("Help"),
510                KeyEvent.VK_F1, Shortcut.DIRECT));
511
512        // contains several initialization tasks to be executed (in parallel) by a ExecutorService
513        List<Callable<Void>> tasks = new ArrayList<>();
514
515        tasks.add(new InitializationTask(tr("Initializing OSM API")) {
516
517            @Override
518            public void initialize() throws Exception {
519                // We try to establish an API connection early, so that any API
520                // capabilities are already known to the editor instance. However
521                // if it goes wrong that's not critical at this stage.
522                try {
523                    OsmApi.getOsmApi().initialize(null, true);
524                } catch (Exception e) {
525                    Main.warn(getErrorMessage(Utils.getRootCause(e)));
526                }
527            }
528        });
529
530        tasks.add(new InitializationTask(tr("Initializing validator")) {
531
532            @Override
533            public void initialize() throws Exception {
534                validator = new OsmValidator();
535                MapView.addLayerChangeListener(validator);
536            }
537        });
538
539        tasks.add(new InitializationTask(tr("Initializing presets")) {
540
541            @Override
542            public void initialize() throws Exception {
543                TaggingPresets.initialize();
544            }
545        });
546
547        tasks.add(new InitializationTask(tr("Initializing map styles")) {
548
549            @Override
550            public void initialize() throws Exception {
551                MapPaintPreference.initialize();
552            }
553        });
554
555        tasks.add(new InitializationTask(tr("Loading imagery preferences")) {
556
557            @Override
558            public void initialize() throws Exception {
559                ImageryPreference.initialize();
560            }
561        });
562
563        try {
564            for (Future<Void> i : Executors.newFixedThreadPool(
565                    Runtime.getRuntime().availableProcessors()).invokeAll(tasks)) {
566                i.get();
567            }
568        } catch (Exception ex) {
569            throw new RuntimeException(ex);
570        }
571
572        // hooks for the jmapviewer component
573        FeatureAdapter.registerBrowserAdapter(new FeatureAdapter.BrowserAdapter() {
574            @Override
575            public void openLink(String url) {
576                OpenBrowser.displayUrl(url);
577            }
578        });
579        FeatureAdapter.registerTranslationAdapter(I18n.getTranslationAdapter());
580
581        if (initListener != null) {
582            initListener.updateStatus(tr("Updating user interface"));
583        }
584
585        toolbar.refreshToolbarControl();
586
587        toolbar.control.updateUI();
588        contentPanePrivate.updateUI();
589    }
590
591    private abstract class InitializationTask implements Callable<Void> {
592
593        private final String name;
594
595        protected InitializationTask(String name) {
596            this.name = name;
597        }
598
599        public abstract void initialize() throws Exception;
600
601        @Override
602        public Void call() throws Exception {
603            if (initListener != null) {
604                initListener.updateStatus(name);
605            }
606            final long startTime = System.currentTimeMillis();
607            initialize();
608            if (isDebugEnabled()) {
609                final long elapsedTime = System.currentTimeMillis() - startTime;
610                Main.debug(tr("{0} completed in {1}", name, Utils.getDurationString(elapsedTime)));
611            }
612            return null;
613        }
614    }
615
616    /**
617     * Add a new layer to the map. If no map exists, create one.
618     */
619    public final synchronized void addLayer(final Layer layer) {
620        boolean noMap = map == null;
621        if (noMap) {
622            createMapFrame(layer, null);
623        }
624        layer.hookUpMapView();
625        map.mapView.addLayer(layer);
626        if (noMap) {
627            Main.map.setVisible(true);
628        }
629    }
630
631    public synchronized void createMapFrame(Layer firstLayer, ViewportData viewportData) {
632        MapFrame mapFrame = new MapFrame(contentPanePrivate, viewportData);
633        setMapFrame(mapFrame);
634        if (firstLayer != null) {
635            mapFrame.selectMapMode((MapMode)mapFrame.getDefaultButtonAction(), firstLayer);
636        }
637        mapFrame.initializeDialogsPane();
638        // bootstrapping problem: make sure the layer list dialog is going to
639        // listen to change events of the very first layer
640        //
641        if (firstLayer != null) {
642            firstLayer.addPropertyChangeListener(LayerListDialog.getInstance().getModel());
643        }
644    }
645
646    /**
647     * Replies <code>true</code> if there is an edit layer
648     *
649     * @return <code>true</code> if there is an edit layer
650     */
651    public boolean hasEditLayer() {
652        if (getEditLayer() == null) return false;
653        return true;
654    }
655
656    /**
657     * Replies the current edit layer
658     *
659     * @return the current edit layer. <code>null</code>, if no current edit layer exists
660     */
661    public OsmDataLayer getEditLayer() {
662        if (!isDisplayingMapView()) return null;
663        return map.mapView.getEditLayer();
664    }
665
666    /**
667     * Replies the current data set.
668     *
669     * @return the current data set. <code>null</code>, if no current data set exists
670     */
671    public DataSet getCurrentDataSet() {
672        if (!hasEditLayer()) return null;
673        return getEditLayer().data;
674    }
675
676    /**
677     * Replies the current selected primitives, from a end-user point of view.
678     * It is not always technically the same collection of primitives than {@link DataSet#getSelected()}.
679     * Indeed, if the user is currently in drawing mode, only the way currently being drawn is returned,
680     * see {@link DrawAction#getInProgressSelection()}.
681     *
682     * @return The current selected primitives, from a end-user point of view. Can be {@code null}.
683     * @since 6546
684     */
685    public Collection<OsmPrimitive> getInProgressSelection() {
686        if (map != null && map.mapMode instanceof DrawAction) {
687            return ((DrawAction) map.mapMode).getInProgressSelection();
688        } else {
689            DataSet ds = getCurrentDataSet();
690            if (ds == null) return null;
691            return ds.getSelected();
692        }
693    }
694
695    /**
696     * Returns the currently active  layer
697     *
698     * @return the currently active layer. <code>null</code>, if currently no active layer exists
699     */
700    public Layer getActiveLayer() {
701        if (!isDisplayingMapView()) return null;
702        return map.mapView.getActiveLayer();
703    }
704
705    protected static final JPanel contentPanePrivate = new JPanel(new BorderLayout());
706
707    public static void redirectToMainContentPane(JComponent source) {
708        RedirectInputMap.redirect(source, contentPanePrivate);
709    }
710
711    public static void registerActionShortcut(JosmAction action) {
712        registerActionShortcut(action, action.getShortcut());
713    }
714
715    public static void registerActionShortcut(Action action, Shortcut shortcut) {
716        KeyStroke keyStroke = shortcut.getKeyStroke();
717        if (keyStroke == null)
718            return;
719
720        InputMap inputMap = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
721        Object existing = inputMap.get(keyStroke);
722        if (existing != null && !existing.equals(action)) {
723            info(String.format("Keystroke %s is already assigned to %s, will be overridden by %s", keyStroke, existing, action));
724        }
725        inputMap.put(keyStroke, action);
726
727        contentPanePrivate.getActionMap().put(action, action);
728    }
729
730    public static void unregisterShortcut(Shortcut shortcut) {
731        contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(shortcut.getKeyStroke());
732    }
733
734    public static void unregisterActionShortcut(JosmAction action) {
735        unregisterActionShortcut(action, action.getShortcut());
736    }
737
738    public static void unregisterActionShortcut(Action action, Shortcut shortcut) {
739        unregisterShortcut(shortcut);
740        contentPanePrivate.getActionMap().remove(action);
741    }
742
743    /**
744     * Replies the registered action for the given shortcut
745     * @param shortcut The shortcut to look for
746     * @return the registered action for the given shortcut
747     * @since 5696
748     */
749    public static Action getRegisteredActionShortcut(Shortcut shortcut) {
750        KeyStroke keyStroke = shortcut.getKeyStroke();
751        if (keyStroke == null)
752            return null;
753        Object action = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).get(keyStroke);
754        if (action instanceof Action)
755            return (Action) action;
756        return null;
757    }
758
759    ///////////////////////////////////////////////////////////////////////////
760    //  Implementation part
761    ///////////////////////////////////////////////////////////////////////////
762
763    /**
764     * Global panel.
765     */
766    public static final JPanel panel = new JPanel(new BorderLayout());
767
768    protected static WindowGeometry geometry;
769    protected static int windowState = JFrame.NORMAL;
770
771    private final CommandQueueListener redoUndoListener = new CommandQueueListener(){
772        @Override
773        public void commandChanged(final int queueSize, final int redoSize) {
774            menu.undo.setEnabled(queueSize > 0);
775            menu.redo.setEnabled(redoSize > 0);
776        }
777    };
778
779    /**
780     * Should be called before the main constructor to setup some parameter stuff
781     * @param args The parsed argument list.
782     */
783    public static void preConstructorInit(Map<Option, Collection<String>> args) {
784        ProjectionPreference.setProjection();
785
786        try {
787            String defaultlaf = platform.getDefaultStyle();
788            String laf = Main.pref.get("laf", defaultlaf);
789            try {
790                UIManager.setLookAndFeel(laf);
791            }
792            catch (final NoClassDefFoundError | ClassNotFoundException e) {
793                info("Look and Feel not found: " + laf);
794                Main.pref.put("laf", defaultlaf);
795            }
796            catch (final UnsupportedLookAndFeelException e) {
797                info("Look and Feel not supported: " + laf);
798                Main.pref.put("laf", defaultlaf);
799            }
800            toolbar = new ToolbarPreferences();
801            contentPanePrivate.updateUI();
802            panel.updateUI();
803        } catch (final Exception e) {
804            error(e);
805        }
806        UIManager.put("OptionPane.okIcon", ImageProvider.get("ok"));
807        UIManager.put("OptionPane.yesIcon", UIManager.get("OptionPane.okIcon"));
808        UIManager.put("OptionPane.cancelIcon", ImageProvider.get("cancel"));
809        UIManager.put("OptionPane.noIcon", UIManager.get("OptionPane.cancelIcon"));
810
811        I18n.translateJavaInternalMessages();
812
813        // init default coordinate format
814        //
815        try {
816            CoordinateFormat.setCoordinateFormat(CoordinateFormat.valueOf(Main.pref.get("coordinates")));
817        } catch (IllegalArgumentException iae) {
818            CoordinateFormat.setCoordinateFormat(CoordinateFormat.DECIMAL_DEGREES);
819        }
820
821        geometry = WindowGeometry.mainWindow("gui.geometry",
822            (args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null),
823            !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false));
824    }
825
826    protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) {
827        if (args.containsKey(Option.DOWNLOAD)) {
828            List<File> fileList = new ArrayList<>();
829            for (String s : args.get(Option.DOWNLOAD)) {
830                File f = null;
831                switch(paramType(s)) {
832                case httpUrl:
833                    downloadFromParamHttp(false, s);
834                    break;
835                case bounds:
836                    downloadFromParamBounds(false, s);
837                    break;
838                case fileUrl:
839                    try {
840                        f = new File(new URI(s));
841                    } catch (URISyntaxException e) {
842                        JOptionPane.showMessageDialog(
843                                Main.parent,
844                                tr("Ignoring malformed file URL: \"{0}\"", s),
845                                tr("Warning"),
846                                JOptionPane.WARNING_MESSAGE
847                                );
848                    }
849                    if (f!=null) {
850                        fileList.add(f);
851                    }
852                    break;
853                case fileName:
854                    f = new File(s);
855                    fileList.add(f);
856                    break;
857                }
858            }
859            if(!fileList.isEmpty())
860            {
861                OpenFileAction.openFiles(fileList, true);
862            }
863        }
864        if (args.containsKey(Option.DOWNLOADGPS)) {
865            for (String s : args.get(Option.DOWNLOADGPS)) {
866                switch(paramType(s)) {
867                case httpUrl:
868                    downloadFromParamHttp(true, s);
869                    break;
870                case bounds:
871                    downloadFromParamBounds(true, s);
872                    break;
873                case fileUrl:
874                case fileName:
875                    JOptionPane.showMessageDialog(
876                            Main.parent,
877                            tr("Parameter \"downloadgps\" does not accept file names or file URLs"),
878                            tr("Warning"),
879                            JOptionPane.WARNING_MESSAGE
880                            );
881                }
882            }
883        }
884        if (args.containsKey(Option.SELECTION)) {
885            for (String s : args.get(Option.SELECTION)) {
886                SearchAction.search(s, SearchAction.SearchMode.add);
887            }
888        }
889    }
890
891    /**
892     * Asks user to perform "save layer" operations (save .osm on disk and/or upload osm data to server) for all {@link OsmDataLayer} before JOSM exits.
893     * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
894     * @since 2025
895     */
896    public static boolean saveUnsavedModifications() {
897        if (!isDisplayingMapView()) return true;
898        return saveUnsavedModifications(map.mapView.getLayersOfType(OsmDataLayer.class), true);
899    }
900
901    /**
902     * Asks user to perform "save layer" operations (save .osm on disk and/or upload osm data to server) before osm layers deletion.
903     *
904     * @param selectedLayers The layers to check. Only instances of {@link OsmDataLayer} are considered.
905     * @param exit {@code true} if JOSM is exiting, {@code false} otherwise.
906     * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
907     * @since 5519
908     */
909    public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, boolean exit) {
910        SaveLayersDialog dialog = new SaveLayersDialog(parent);
911        List<OsmDataLayer> layersWithUnmodifiedChanges = new ArrayList<>();
912        for (Layer l: selectedLayers) {
913            if (!(l instanceof OsmDataLayer)) {
914                continue;
915            }
916            OsmDataLayer odl = (OsmDataLayer)l;
917            if ((odl.requiresSaveToFile() || (odl.requiresUploadToServer() && !odl.isUploadDiscouraged())) && odl.data.isModified()) {
918                layersWithUnmodifiedChanges.add(odl);
919            }
920        }
921        if (exit) {
922            dialog.prepareForSavingAndUpdatingLayersBeforeExit();
923        } else {
924            dialog.prepareForSavingAndUpdatingLayersBeforeDelete();
925        }
926        if (!layersWithUnmodifiedChanges.isEmpty()) {
927            dialog.getModel().populate(layersWithUnmodifiedChanges);
928            dialog.setVisible(true);
929            switch(dialog.getUserAction()) {
930            case CANCEL: return false;
931            case PROCEED: return true;
932            default: return false;
933            }
934        }
935
936        return true;
937    }
938
939    /**
940     * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). If there are some unsaved data layers, asks first for user confirmation.
941     * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code.
942     * @param exitCode The return code
943     * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation.
944     * @since 3378
945     */
946    public static boolean exitJosm(boolean exit, int exitCode) {
947        if (Main.saveUnsavedModifications()) {
948            geometry.remember("gui.geometry");
949            if (map != null) {
950                map.rememberToggleDialogWidth();
951            }
952            pref.put("gui.maximized", (windowState & JFrame.MAXIMIZED_BOTH) != 0);
953            // Remove all layers because somebody may rely on layerRemoved events (like AutosaveTask)
954            if (Main.isDisplayingMapView()) {
955                Collection<Layer> layers = new ArrayList<>(Main.map.mapView.getAllLayers());
956                for (Layer l: layers) {
957                    Main.main.removeLayer(l);
958                }
959            }
960            if (exit) {
961                System.exit(exitCode);
962            }
963            return true;
964        }
965        return false;
966    }
967
968    /**
969     * The type of a command line parameter, to be used in switch statements.
970     * @see #paramType
971     */
972    private enum DownloadParamType { httpUrl, fileUrl, bounds, fileName }
973
974    /**
975     * Guess the type of a parameter string specified on the command line with --download= or --downloadgps.
976     * @param s A parameter string
977     * @return The guessed parameter type
978     */
979    private static DownloadParamType paramType(String s) {
980        if(s.startsWith("http:") || s.startsWith("https:")) return DownloadParamType.httpUrl;
981        if(s.startsWith("file:")) return DownloadParamType.fileUrl;
982        String coorPattern = "\\s*[+-]?[0-9]+(\\.[0-9]+)?\\s*";
983        if(s.matches(coorPattern+"(,"+coorPattern+"){3}")) return DownloadParamType.bounds;
984        // everything else must be a file name
985        return DownloadParamType.fileName;
986    }
987
988    /**
989     * Download area specified on the command line as OSM URL.
990     * @param rawGps Flag to download raw GPS tracks
991     * @param s The URL parameter
992     */
993    private static void downloadFromParamHttp(final boolean rawGps, String s) {
994        final Bounds b = OsmUrlToBounds.parse(s);
995        if (b == null) {
996            JOptionPane.showMessageDialog(
997                    Main.parent,
998                    tr("Ignoring malformed URL: \"{0}\"", s),
999                    tr("Warning"),
1000                    JOptionPane.WARNING_MESSAGE
1001                    );
1002        } else {
1003            downloadFromParamBounds(rawGps, b);
1004        }
1005    }
1006
1007    /**
1008     * Download area specified on the command line as bounds string.
1009     * @param rawGps Flag to download raw GPS tracks
1010     * @param s The bounds parameter
1011     */
1012    private static void downloadFromParamBounds(final boolean rawGps, String s) {
1013        final StringTokenizer st = new StringTokenizer(s, ",");
1014        if (st.countTokens() == 4) {
1015            Bounds b = new Bounds(
1016                    new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken())),
1017                    new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken()))
1018                    );
1019            downloadFromParamBounds(rawGps, b);
1020        }
1021    }
1022
1023    /**
1024     * Download area specified as Bounds value.
1025     * @param rawGps Flag to download raw GPS tracks
1026     * @param b The bounds value
1027     * @see #downloadFromParamBounds(boolean, String)
1028     * @see #downloadFromParamHttp
1029     */
1030    private static void downloadFromParamBounds(final boolean rawGps, Bounds b) {
1031        DownloadTask task = rawGps ? new DownloadGpsTask() : new DownloadOsmTask();
1032        // asynchronously launch the download task ...
1033        Future<?> future = task.download(true, b, null);
1034        // ... and the continuation when the download is finished (this will wait for the download to finish)
1035        Main.worker.execute(new PostDownloadHandler(task, future));
1036    }
1037
1038    /**
1039     * Identifies the current operating system family and initializes the platform hook accordingly.
1040     * @since 1849
1041     */
1042    public static void determinePlatformHook() {
1043        String os = System.getProperty("os.name");
1044        if (os == null) {
1045            warn("Your operating system has no name, so I'm guessing its some kind of *nix.");
1046            platform = new PlatformHookUnixoid();
1047        } else if (os.toLowerCase().startsWith("windows")) {
1048            platform = new PlatformHookWindows();
1049        } else if ("Linux".equals(os) || "Solaris".equals(os) ||
1050                "SunOS".equals(os) || "AIX".equals(os) ||
1051                "FreeBSD".equals(os) || "NetBSD".equals(os) || "OpenBSD".equals(os)) {
1052            platform = new PlatformHookUnixoid();
1053        } else if (os.toLowerCase().startsWith("mac os x")) {
1054            platform = new PlatformHookOsx();
1055        } else {
1056            warn("I don't know your operating system '"+os+"', so I'm guessing its some kind of *nix.");
1057            platform = new PlatformHookUnixoid();
1058        }
1059    }
1060
1061    private static class WindowPositionSizeListener extends WindowAdapter implements
1062    ComponentListener {
1063        @Override
1064        public void windowStateChanged(WindowEvent e) {
1065            Main.windowState = e.getNewState();
1066        }
1067
1068        @Override
1069        public void componentHidden(ComponentEvent e) {
1070        }
1071
1072        @Override
1073        public void componentMoved(ComponentEvent e) {
1074            handleComponentEvent(e);
1075        }
1076
1077        @Override
1078        public void componentResized(ComponentEvent e) {
1079            handleComponentEvent(e);
1080        }
1081
1082        @Override
1083        public void componentShown(ComponentEvent e) {
1084        }
1085
1086        private void handleComponentEvent(ComponentEvent e) {
1087            Component c = e.getComponent();
1088            if (c instanceof JFrame && c.isVisible()) {
1089                if(Main.windowState == JFrame.NORMAL) {
1090                    Main.geometry = new WindowGeometry((JFrame) c);
1091                } else {
1092                    Main.geometry.fixScreen((JFrame) c);
1093                }
1094            }
1095        }
1096    }
1097
1098    protected static void addListener() {
1099        parent.addComponentListener(new WindowPositionSizeListener());
1100        ((JFrame)parent).addWindowStateListener(new WindowPositionSizeListener());
1101    }
1102
1103    /**
1104     * Checks that JOSM is at least running with Java 7.
1105     * @since 7001
1106     */
1107    public static void checkJavaVersion() {
1108        String version = System.getProperty("java.version");
1109        if (version != null) {
1110            if (version.matches("^(1\\.)?[789].*"))
1111                return;
1112            if (version.matches("^(1\\.)?[56].*")) {
1113                JMultilineLabel ho = new JMultilineLabel("<html>"+
1114                        tr("<h2>JOSM requires Java version {0}.</h2>"+
1115                                "Detected Java version: {1}.<br>"+
1116                                "You can <ul><li>update your Java (JRE) or</li>"+
1117                                "<li>use an earlier (Java {2} compatible) version of JOSM.</li></ul>"+
1118                                "More Info:", "7", version, "6")+"</html>");
1119                JTextArea link = new JTextArea(HelpUtil.getWikiBaseHelpUrl()+"/Help/SystemRequirements");
1120                link.setEditable(false);
1121                link.setBackground(panel.getBackground());
1122                JPanel panel = new JPanel(new GridBagLayout());
1123                GridBagConstraints gbc = new GridBagConstraints();
1124                gbc.gridwidth = GridBagConstraints.REMAINDER;
1125                gbc.anchor = GridBagConstraints.WEST;
1126                gbc.weightx = 1.0;
1127                panel.add(ho, gbc);
1128                panel.add(link, gbc);
1129                final String EXIT = tr("Exit JOSM");
1130                final String CONTINUE = tr("Continue, try anyway");
1131                int ret = JOptionPane.showOptionDialog(null, panel, tr("Error"), JOptionPane.YES_NO_OPTION,
1132                        JOptionPane.ERROR_MESSAGE, null, new String[] {EXIT, CONTINUE}, EXIT);
1133                if (ret == 0) {
1134                    System.exit(0);
1135                }
1136                return;
1137            }
1138        }
1139        error("Could not recognize Java Version: "+version);
1140    }
1141
1142    /* ----------------------------------------------------------------------------------------- */
1143    /* projection handling  - Main is a registry for a single, global projection instance        */
1144    /*                                                                                           */
1145    /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */
1146    /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class.     */
1147    /* ----------------------------------------------------------------------------------------- */
1148    /**
1149     * The projection method used.
1150     * use {@link #getProjection()} and {@link #setProjection(Projection)} for access.
1151     * Use {@link #setProjection(Projection)} in order to trigger a projection change event.
1152     */
1153    private static Projection proj;
1154
1155    /**
1156     * Replies the current projection.
1157     *
1158     * @return the currently active projection
1159     */
1160    public static Projection getProjection() {
1161        return proj;
1162    }
1163
1164    /**
1165     * Sets the current projection
1166     *
1167     * @param p the projection
1168     */
1169    public static void setProjection(Projection p) {
1170        CheckParameterUtil.ensureParameterNotNull(p);
1171        Projection oldValue = proj;
1172        Bounds b = isDisplayingMapView() ? map.mapView.getRealBounds() : null;
1173        proj = p;
1174        fireProjectionChanged(oldValue, proj, b);
1175    }
1176
1177    /*
1178     * Keep WeakReferences to the listeners. This relieves clients from the burden of
1179     * explicitly removing the listeners and allows us to transparently register every
1180     * created dataset as projection change listener.
1181     */
1182    private static final List<WeakReference<ProjectionChangeListener>> listeners = new ArrayList<>();
1183
1184    private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) {
1185        if (newValue == null ^ oldValue == null
1186                || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) {
1187
1188            synchronized(Main.class) {
1189                Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator();
1190                while (it.hasNext()){
1191                    WeakReference<ProjectionChangeListener> wr = it.next();
1192                    ProjectionChangeListener listener = wr.get();
1193                    if (listener == null) {
1194                        it.remove();
1195                        continue;
1196                    }
1197                    listener.projectionChanged(oldValue, newValue);
1198                }
1199            }
1200            if (newValue != null && oldBounds != null) {
1201                Main.map.mapView.zoomTo(oldBounds);
1202            }
1203            /* TODO - remove layers with fixed projection */
1204        }
1205    }
1206
1207    /**
1208     * Register a projection change listener.
1209     *
1210     * @param listener the listener. Ignored if <code>null</code>.
1211     */
1212    public static void addProjectionChangeListener(ProjectionChangeListener listener) {
1213        if (listener == null) return;
1214        synchronized (Main.class) {
1215            for (WeakReference<ProjectionChangeListener> wr : listeners) {
1216                // already registered ? => abort
1217                if (wr.get() == listener) return;
1218            }
1219            listeners.add(new WeakReference<>(listener));
1220        }
1221    }
1222
1223    /**
1224     * Removes a projection change listener.
1225     *
1226     * @param listener the listener. Ignored if <code>null</code>.
1227     */
1228    public static void removeProjectionChangeListener(ProjectionChangeListener listener) {
1229        if (listener == null) return;
1230        synchronized(Main.class){
1231            Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator();
1232            while (it.hasNext()){
1233                WeakReference<ProjectionChangeListener> wr = it.next();
1234                // remove the listener - and any other listener which got garbage
1235                // collected in the meantime
1236                if (wr.get() == null || wr.get() == listener) {
1237                    it.remove();
1238                }
1239            }
1240        }
1241    }
1242
1243    /**
1244     * Listener for window switch events.
1245     *
1246     * These are events, when the user activates a window of another application
1247     * or comes back to JOSM. Window switches from one JOSM window to another
1248     * are not reported.
1249     */
1250    public static interface WindowSwitchListener {
1251        /**
1252         * Called when the user activates a window of another application.
1253         */
1254        void toOtherApplication();
1255        /**
1256         * Called when the user comes from a window of another application
1257         * back to JOSM.
1258         */
1259        void fromOtherApplication();
1260    }
1261
1262    private static final List<WeakReference<WindowSwitchListener>> windowSwitchListeners = new ArrayList<>();
1263
1264    /**
1265     * Register a window switch listener.
1266     *
1267     * @param listener the listener. Ignored if <code>null</code>.
1268     */
1269    public static void addWindowSwitchListener(WindowSwitchListener listener) {
1270        if (listener == null) return;
1271        synchronized (Main.class) {
1272            for (WeakReference<WindowSwitchListener> wr : windowSwitchListeners) {
1273                // already registered ? => abort
1274                if (wr.get() == listener) return;
1275            }
1276            boolean wasEmpty = windowSwitchListeners.isEmpty();
1277            windowSwitchListeners.add(new WeakReference<>(listener));
1278            if (wasEmpty) {
1279                // The following call will have no effect, when there is no window
1280                // at the time. Therefore, MasterWindowListener.setup() will also be
1281                // called, as soon as the main window is shown.
1282                MasterWindowListener.setup();
1283            }
1284        }
1285    }
1286
1287    /**
1288     * Removes a window switch listener.
1289     *
1290     * @param listener the listener. Ignored if <code>null</code>.
1291     */
1292    public static void removeWindowSwitchListener(WindowSwitchListener listener) {
1293        if (listener == null) return;
1294        synchronized (Main.class){
1295            Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1296            while (it.hasNext()){
1297                WeakReference<WindowSwitchListener> wr = it.next();
1298                // remove the listener - and any other listener which got garbage
1299                // collected in the meantime
1300                if (wr.get() == null || wr.get() == listener) {
1301                    it.remove();
1302                }
1303            }
1304            if (windowSwitchListeners.isEmpty()) {
1305                MasterWindowListener.teardown();
1306            }
1307        }
1308    }
1309
1310    /**
1311     * WindowListener, that is registered on all Windows of the application.
1312     *
1313     * Its purpose is to notify WindowSwitchListeners, that the user switches to
1314     * another application, e.g. a browser, or back to JOSM.
1315     *
1316     * When changing from JOSM to another application and back (e.g. two times
1317     * alt+tab), the active Window within JOSM may be different.
1318     * Therefore, we need to register listeners to <strong>all</strong> (visible)
1319     * Windows in JOSM, and it does not suffice to monitor the one that was
1320     * deactivated last.
1321     *
1322     * This class is only "active" on demand, i.e. when there is at least one
1323     * WindowSwitchListener registered.
1324     */
1325    protected static class MasterWindowListener extends WindowAdapter {
1326
1327        private static MasterWindowListener INSTANCE;
1328
1329        public static MasterWindowListener getInstance() {
1330            if (INSTANCE == null) {
1331                INSTANCE = new MasterWindowListener();
1332            }
1333            return INSTANCE;
1334        }
1335
1336        /**
1337         * Register listeners to all non-hidden windows.
1338         *
1339         * Windows that are created later, will be cared for in {@link #windowDeactivated(WindowEvent)}.
1340         */
1341        public static void setup() {
1342            if (!windowSwitchListeners.isEmpty()) {
1343                for (Window w : Window.getWindows()) {
1344                    if (w.isShowing()) {
1345                        if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
1346                            w.addWindowListener(getInstance());
1347                        }
1348                    }
1349                }
1350            }
1351        }
1352
1353        /**
1354         * Unregister all listeners.
1355         */
1356        public static void teardown() {
1357            for (Window w : Window.getWindows()) {
1358                w.removeWindowListener(getInstance());
1359            }
1360        }
1361
1362        @Override
1363        public void windowActivated(WindowEvent e) {
1364            if (e.getOppositeWindow() == null) { // we come from a window of a different application
1365                // fire WindowSwitchListeners
1366                synchronized (Main.class) {
1367                    Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1368                    while (it.hasNext()){
1369                        WeakReference<WindowSwitchListener> wr = it.next();
1370                        WindowSwitchListener listener = wr.get();
1371                        if (listener == null) {
1372                            it.remove();
1373                            continue;
1374                        }
1375                        listener.fromOtherApplication();
1376                    }
1377                }
1378            }
1379        }
1380
1381        @Override
1382        public void windowDeactivated(WindowEvent e) {
1383            // set up windows that have been created in the meantime
1384            for (Window w : Window.getWindows()) {
1385                if (!w.isShowing()) {
1386                    w.removeWindowListener(getInstance());
1387                } else {
1388                    if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
1389                        w.addWindowListener(getInstance());
1390                    }
1391                }
1392            }
1393            if (e.getOppositeWindow() == null) { // we go to a window of a different application
1394                // fire WindowSwitchListeners
1395                synchronized (Main.class) {
1396                    Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1397                    while (it.hasNext()){
1398                        WeakReference<WindowSwitchListener> wr = it.next();
1399                        WindowSwitchListener listener = wr.get();
1400                        if (listener == null) {
1401                            it.remove();
1402                            continue;
1403                        }
1404                        listener.toOtherApplication();
1405                    }
1406                }
1407            }
1408        }
1409    }
1410
1411    /**
1412     * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes
1413     * @param listener The MapFrameListener
1414     * @return {@code true} if the listeners collection changed as a result of the call
1415     * @since 5957
1416     */
1417    public static boolean addMapFrameListener(MapFrameListener listener) {
1418        return listener != null ? mapFrameListeners.add(listener) : false;
1419    }
1420
1421    /**
1422     * Unregisters the given {@code MapFrameListener} from MapFrame changes
1423     * @param listener The MapFrameListener
1424     * @return {@code true} if the listeners collection changed as a result of the call
1425     * @since 5957
1426     */
1427    public static boolean removeMapFrameListener(MapFrameListener listener) {
1428        return listener != null ? mapFrameListeners.remove(listener) : false;
1429    }
1430
1431    /**
1432     * Adds a new network error that occur to give a hint about broken Internet connection.
1433     * Do not use this method for errors known for sure thrown because of a bad proxy configuration.
1434     *
1435     * @param url The accessed URL that caused the error
1436     * @param t The network error
1437     * @return The previous error associated to the given resource, if any. Can be {@code null}
1438     * @since 6642
1439     */
1440    public static Throwable addNetworkError(URL url, Throwable t) {
1441        if (url != null && t != null) {
1442            Throwable old = addNetworkError(url.toExternalForm(), t);
1443            if (old != null) {
1444                Main.warn("Already here "+old);
1445            }
1446            return old;
1447        }
1448        return null;
1449    }
1450
1451    /**
1452     * Adds a new network error that occur to give a hint about broken Internet connection.
1453     * Do not use this method for errors known for sure thrown because of a bad proxy configuration.
1454     *
1455     * @param url The accessed URL that caused the error
1456     * @param t The network error
1457     * @return The previous error associated to the given resource, if any. Can be {@code null}
1458     * @since 6642
1459     */
1460    public static Throwable addNetworkError(String url, Throwable t) {
1461        if (url != null && t != null) {
1462            return NETWORK_ERRORS.put(url, t);
1463        }
1464        return null;
1465    }
1466
1467    /**
1468     * Returns the network errors that occured until now.
1469     * @return the network errors that occured until now, indexed by URL
1470     * @since 6639
1471     */
1472    public static Map<String, Throwable> getNetworkErrors() {
1473        return new HashMap<>(NETWORK_ERRORS);
1474    }
1475
1476    /**
1477     * Returns the JOSM website URL.
1478     * @return the josm website URL
1479     * @since 6897
1480     */
1481    public static String getJOSMWebsite() {
1482        if (Main.pref != null)
1483            return Main.pref.get("josm.url", JOSM_WEBSITE);
1484        return JOSM_WEBSITE;
1485    }
1486
1487    /**
1488     * Returns the JOSM XML URL.
1489     * @return the josm XML URL
1490     * @since 6897
1491     */
1492    public static String getXMLBase() {
1493        // Always return HTTP (issues reported with HTTPS)
1494        return "http://josm.openstreetmap.de";
1495    }
1496
1497    /**
1498     * Returns the OSM website URL.
1499     * @return the OSM website URL
1500     * @since 6897
1501     */
1502    public static String getOSMWebsite() {
1503        if (Main.pref != null)
1504            return Main.pref.get("osm.url", OSM_WEBSITE);
1505        return OSM_WEBSITE;
1506    }
1507
1508    /**
1509     * Determines if we are currently running on OSX.
1510     * @return {@code true} if we are currently running on OSX
1511     * @since 6957
1512     */
1513    public static boolean isPlatformOsx() {
1514        return Main.platform instanceof PlatformHookOsx;
1515    }
1516
1517    /**
1518     * Determines if we are currently running on Windows.
1519     * @return {@code true} if we are currently running on Windows
1520     * @since 7335
1521     */
1522    public static boolean isPlatformWindows() {
1523        return Main.platform instanceof PlatformHookWindows;
1524    }
1525}