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