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