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