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