source: josm/trunk/src/org/openstreetmap/josm/Main.java @ 12574

Last change on this file since 12574 was 12507, checked in by Don-vip, 3 months ago

fix #15062 - avoid NPE when restarting JOSM

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