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