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