001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.BorderLayout;
007import java.awt.Component;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.awt.Window;
011import java.awt.event.ComponentEvent;
012import java.awt.event.ComponentListener;
013import java.awt.event.KeyEvent;
014import java.awt.event.WindowAdapter;
015import java.awt.event.WindowEvent;
016import java.io.File;
017import java.lang.ref.WeakReference;
018import java.net.URI;
019import java.net.URISyntaxException;
020import java.net.URL;
021import java.text.MessageFormat;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Map;
028import java.util.StringTokenizer;
029import java.util.concurrent.Callable;
030import java.util.concurrent.ExecutorService;
031import java.util.concurrent.Executors;
032import java.util.concurrent.Future;
033
034import javax.swing.Action;
035import javax.swing.InputMap;
036import javax.swing.JComponent;
037import javax.swing.JFrame;
038import javax.swing.JLabel;
039import javax.swing.JOptionPane;
040import javax.swing.JPanel;
041import javax.swing.JTextArea;
042import javax.swing.KeyStroke;
043import javax.swing.UIManager;
044import javax.swing.UnsupportedLookAndFeelException;
045
046import org.openstreetmap.gui.jmapviewer.FeatureAdapter;
047import org.openstreetmap.josm.actions.JosmAction;
048import org.openstreetmap.josm.actions.OpenFileAction;
049import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask;
050import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
051import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
052import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
053import org.openstreetmap.josm.actions.mapmode.MapMode;
054import org.openstreetmap.josm.actions.search.SearchAction;
055import org.openstreetmap.josm.data.Bounds;
056import org.openstreetmap.josm.data.Preferences;
057import org.openstreetmap.josm.data.ServerSidePreferences;
058import org.openstreetmap.josm.data.UndoRedoHandler;
059import org.openstreetmap.josm.data.coor.CoordinateFormat;
060import org.openstreetmap.josm.data.coor.LatLon;
061import org.openstreetmap.josm.data.osm.DataSet;
062import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
063import org.openstreetmap.josm.data.projection.Projection;
064import org.openstreetmap.josm.data.projection.ProjectionChangeListener;
065import org.openstreetmap.josm.data.validation.OsmValidator;
066import org.openstreetmap.josm.gui.GettingStarted;
067import org.openstreetmap.josm.gui.MainApplication.Option;
068import org.openstreetmap.josm.gui.MainMenu;
069import org.openstreetmap.josm.gui.MapFrame;
070import org.openstreetmap.josm.gui.MapFrameListener;
071import org.openstreetmap.josm.gui.MapView;
072import org.openstreetmap.josm.gui.NavigatableComponent.ViewportData;
073import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
074import org.openstreetmap.josm.gui.help.HelpUtil;
075import org.openstreetmap.josm.gui.io.SaveLayersDialog;
076import org.openstreetmap.josm.gui.layer.Layer;
077import org.openstreetmap.josm.gui.layer.OsmDataLayer;
078import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener;
079import org.openstreetmap.josm.gui.preferences.ToolbarPreferences;
080import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference;
081import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference;
082import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference;
083import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
084import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor;
085import org.openstreetmap.josm.gui.progress.ProgressMonitorExecutor;
086import org.openstreetmap.josm.gui.util.RedirectInputMap;
087import org.openstreetmap.josm.io.OsmApi;
088import org.openstreetmap.josm.tools.CheckParameterUtil;
089import org.openstreetmap.josm.tools.I18n;
090import org.openstreetmap.josm.tools.ImageProvider;
091import org.openstreetmap.josm.tools.OpenBrowser;
092import org.openstreetmap.josm.tools.OsmUrlToBounds;
093import org.openstreetmap.josm.tools.PlatformHook;
094import org.openstreetmap.josm.tools.PlatformHookOsx;
095import org.openstreetmap.josm.tools.PlatformHookUnixoid;
096import org.openstreetmap.josm.tools.PlatformHookWindows;
097import org.openstreetmap.josm.tools.Shortcut;
098import org.openstreetmap.josm.tools.Utils;
099import org.openstreetmap.josm.tools.WindowGeometry;
100
101/**
102 * Abstract class holding various static global variables and methods used in large parts of JOSM application.
103 * @since 98
104 */
105abstract public class Main {
106
107    /**
108     * The JOSM website URL.
109     * @since 6143
110     */
111    public static final String JOSM_WEBSITE = "http://josm.openstreetmap.de";
112
113    /**
114     * The OSM website URL.
115     * @since 6453
116     */
117    public static final String OSM_WEBSITE = "http://www.openstreetmap.org";
118
119    /**
120     * Replies true if JOSM currently displays a map view. False, if it doesn't, i.e. if
121     * it only shows the MOTD panel.
122     *
123     * @return <code>true</code> if JOSM currently displays a map view
124     */
125    static public boolean isDisplayingMapView() {
126        if (map == null) return false;
127        if (map.mapView == null) return false;
128        return true;
129    }
130
131    /**
132     * Global parent component for all dialogs and message boxes
133     */
134    public static Component parent;
135
136    /**
137     * Global application.
138     */
139    public static Main main;
140
141    /**
142     * Command-line arguments used to run the application.
143     */
144    public static String[] commandLineArgs;
145
146    /**
147     * The worker thread slave. This is for executing all long and intensive
148     * calculations. The executed runnables are guaranteed to be executed separately
149     * and sequential.
150     */
151    public final static ExecutorService worker = new ProgressMonitorExecutor();
152
153    /**
154     * Global application preferences
155     */
156    public static Preferences pref;
157
158    /**
159     * The global paste buffer.
160     */
161    public static final PrimitiveDeepCopy pasteBuffer = new PrimitiveDeepCopy();
162
163    /**
164     * The layer source from which {@link Main#pasteBuffer} data comes from.
165     */
166    public static Layer pasteSource;
167
168    /**
169     * The MapFrame. Use {@link Main#setMapFrame} to set or clear it.
170     */
171    public static MapFrame map;
172
173    /**
174     * Set to <code>true</code>, when in applet mode
175     */
176    public static boolean applet = false;
177
178    /**
179     * The toolbar preference control to register new actions.
180     */
181    public static ToolbarPreferences toolbar;
182
183    /**
184     * The commands undo/redo handler.
185     */
186    public UndoRedoHandler undoRedo = new UndoRedoHandler();
187
188    /**
189     * The progress monitor being currently displayed.
190     */
191    public static PleaseWaitProgressMonitor currentProgressMonitor;
192
193    /**
194     * The main menu bar at top of screen.
195     */
196    public MainMenu menu;
197
198    /**
199     * The data validation handler.
200     */
201    public OsmValidator validator;
202    /**
203     * The MOTD Layer.
204     */
205    private GettingStarted gettingStarted = new GettingStarted();
206
207    private static final Collection<MapFrameListener> mapFrameListeners = new ArrayList<MapFrameListener>();
208
209    /**
210     * Logging level (4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none).
211     * @since 6248
212     */
213    public static int logLevel = 3;
214
215    /**
216     * Prints an error message if logging is on.
217     * @param msg The message to print.
218     * @since 6248
219     */
220    public static void error(String msg) {
221        if (logLevel < 1)
222            return;
223        System.err.println(tr("ERROR: {0}", msg));
224    }
225
226    /**
227     * Prints a warning message if logging is on.
228     * @param msg The message to print.
229     */
230    public static void warn(String msg) {
231        if (logLevel < 2)
232            return;
233        System.err.println(tr("WARNING: {0}", msg));
234    }
235
236    /**
237     * Prints an informational message if logging is on.
238     * @param msg The message to print.
239     */
240    public static void info(String msg) {
241        if (logLevel < 3)
242            return;
243        System.out.println(tr("INFO: {0}", msg));
244    }
245
246    /**
247     * Prints a debug message if logging is on.
248     * @param msg The message to print.
249     */
250    public static void debug(String msg) {
251        if (logLevel < 4)
252            return;
253        System.out.println(tr("DEBUG: {0}", msg));
254    }
255
256    /**
257     * Prints a formated error message if logging is on. Calls {@link MessageFormat#format}
258     * function to format text.
259     * @param msg The formated message to print.
260     * @param objects The objects to insert into format string.
261     * @since 6248
262     */
263    public static void error(String msg, Object... objects) {
264        error(MessageFormat.format(msg, objects));
265    }
266
267    /**
268     * Prints a formated warning message if logging is on. Calls {@link MessageFormat#format}
269     * function to format text.
270     * @param msg The formated message to print.
271     * @param objects The objects to insert into format string.
272     */
273    public static void warn(String msg, Object... objects) {
274        warn(MessageFormat.format(msg, objects));
275    }
276
277    /**
278     * Prints a formated informational message if logging is on. Calls {@link MessageFormat#format}
279     * function to format text.
280     * @param msg The formated message to print.
281     * @param objects The objects to insert into format string.
282     */
283    public static void info(String msg, Object... objects) {
284        info(MessageFormat.format(msg, objects));
285    }
286
287    /**
288     * Prints a formated debug message if logging is on. Calls {@link MessageFormat#format}
289     * function to format text.
290     * @param msg The formated message to print.
291     * @param objects The objects to insert into format string.
292     */
293    public static void debug(String msg, Object... objects) {
294        debug(MessageFormat.format(msg, objects));
295    }
296
297    /**
298     * Prints an error message for the given Throwable.
299     * @param t The throwable object causing the error
300     * @since 6248
301     */
302    public static void error(Throwable t) {
303        error(getErrorMessage(t));
304    }
305
306    /**
307     * Prints a warning message for the given Throwable.
308     * @param t The throwable object causing the error
309     * @since 6248
310     */
311    public static void warn(Throwable t) {
312        warn(getErrorMessage(t));
313    }
314
315    private static String getErrorMessage(Throwable t) {
316        StringBuilder sb = new StringBuilder(t.getClass().getName());
317        String msg = t.getMessage();
318        if (msg != null) {
319            sb.append(": ").append(msg.trim());
320        }
321        Throwable cause = t.getCause();
322        if (cause != null && !cause.equals(t)) {
323            sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause));
324        }
325        return sb.toString();
326    }
327
328    /**
329     * Platform specific code goes in here.
330     * Plugins may replace it, however, some hooks will be called before any plugins have been loeaded.
331     * So if you need to hook into those early ones, split your class and send the one with the early hooks
332     * to the JOSM team for inclusion.
333     */
334    public static PlatformHook platform;
335
336    /**
337     * Whether or not the java vm is openjdk
338     * We use this to work around openjdk bugs
339     */
340    public static boolean isOpenjdk;
341
342    /**
343     * Initializes {@code Main.pref} in applet context.
344     * @param serverURL The server URL hosting the user preferences.
345     * @since 6471
346     */
347    public static void initAppletPreferences(URL serverURL) {
348        Main.pref = new ServerSidePreferences(serverURL);
349    }
350
351    /**
352     * Initializes {@code Main.pref} in normal application context.
353     * @since 6471
354     */
355    public static void initApplicationPreferences() {
356        Main.pref = new Preferences();
357    }
358
359    /**
360     * Set or clear (if passed <code>null</code>) the map.
361     * @param map The map to set {@link Main#map} to. Can be null.
362     */
363    public final void setMapFrame(final MapFrame map) {
364        MapFrame old = Main.map;
365        panel.setVisible(false);
366        panel.removeAll();
367        if (map != null) {
368            map.fillPanel(panel);
369        } else {
370            old.destroy();
371            panel.add(gettingStarted, BorderLayout.CENTER);
372        }
373        panel.setVisible(true);
374        redoUndoListener.commandChanged(0,0);
375
376        Main.map = map;
377
378        for (MapFrameListener listener : mapFrameListeners ) {
379            listener.mapFrameInitialized(old, map);
380        }
381        if (map == null && currentProgressMonitor != null) {
382            currentProgressMonitor.showForegroundDialog();
383        }
384    }
385
386    /**
387     * Remove the specified layer from the map. If it is the last layer,
388     * remove the map as well.
389     * @param layer The layer to remove
390     */
391    public final synchronized void removeLayer(final Layer layer) {
392        if (map != null) {
393            map.mapView.removeLayer(layer);
394            if (isDisplayingMapView() && map.mapView.getAllLayers().isEmpty()) {
395                setMapFrame(null);
396            }
397        }
398    }
399
400    private static InitStatusListener initListener = null;
401
402    public static interface InitStatusListener {
403
404        void updateStatus(String event);
405    }
406
407    public static void setInitStatusListener(InitStatusListener listener) {
408        initListener = listener;
409    }
410
411    public Main() {
412        main = this;
413        isOpenjdk = System.getProperty("java.vm.name").toUpperCase().indexOf("OPENJDK") != -1;
414
415        if (initListener != null) {
416            initListener.updateStatus(tr("Executing platform startup hook"));
417        }
418        platform.startupHook();
419
420        if (initListener != null) {
421            initListener.updateStatus(tr("Building main menu"));
422        }
423        contentPanePrivate.add(panel, BorderLayout.CENTER);
424        panel.add(gettingStarted, BorderLayout.CENTER);
425        menu = new MainMenu();
426
427        undoRedo.addCommandQueueListener(redoUndoListener);
428
429        // creating toolbar
430        contentPanePrivate.add(toolbar.control, BorderLayout.NORTH);
431
432        registerActionShortcut(menu.help, Shortcut.registerShortcut("system:help", tr("Help"),
433                KeyEvent.VK_F1, Shortcut.DIRECT));
434
435        // contains several initialization tasks to be executed (in parallel) by a ExecutorService
436        List<Callable<Void>> tasks = new ArrayList<Callable<Void>>();
437
438        tasks.add(new Callable<Void>() {
439
440            @Override
441            public Void call() throws Exception {
442                // We try to establish an API connection early, so that any API
443                // capabilities are already known to the editor instance. However
444                // if it goes wrong that's not critical at this stage.
445                if (initListener != null) {
446                    initListener.updateStatus(tr("Initializing OSM API"));
447                }
448                try {
449                    OsmApi.getOsmApi().initialize(null, true);
450                } catch (Exception x) {
451                    // ignore any exception here.
452                }
453                return null;
454            }
455        });
456
457        tasks.add(new Callable<Void>() {
458
459            @Override
460            public Void call() throws Exception {
461                if (initListener != null) {
462                    initListener.updateStatus(tr("Initializing presets"));
463                }
464                TaggingPresetPreference.initialize();
465                // some validator tests require the presets to be initialized
466                // TODO remove this dependency for parallel initialization
467                if (initListener != null) {
468                    initListener.updateStatus(tr("Initializing validator"));
469                }
470                validator = new OsmValidator();
471                MapView.addLayerChangeListener(validator);
472                return null;
473            }
474        });
475
476        tasks.add(new Callable<Void>() {
477
478            @Override
479            public Void call() throws Exception {
480                if (initListener != null) {
481                    initListener.updateStatus(tr("Initializing map styles"));
482                }
483                MapPaintPreference.initialize();
484                return null;
485            }
486        });
487
488        tasks.add(new Callable<Void>() {
489
490            @Override
491            public Void call() throws Exception {
492                if (initListener != null) {
493                    initListener.updateStatus(tr("Loading imagery preferences"));
494                }
495                ImageryPreference.initialize();
496                return null;
497            }
498        });
499
500        try {
501            for (Future<Void> i : Executors.newFixedThreadPool(
502                    Runtime.getRuntime().availableProcessors()).invokeAll(tasks)) {
503                i.get();
504            }
505        } catch (Exception ex) {
506            throw new RuntimeException(ex);
507        }
508
509        // hooks for the jmapviewer component
510        FeatureAdapter.registerBrowserAdapter(new FeatureAdapter.BrowserAdapter() {
511            @Override
512            public void openLink(String url) {
513                OpenBrowser.displayUrl(url);
514            }
515        });
516        FeatureAdapter.registerTranslationAdapter(I18n.getTranslationAdapter());
517
518        if (initListener != null) {
519            initListener.updateStatus(tr("Updating user interface"));
520        }
521
522        toolbar.refreshToolbarControl();
523
524        toolbar.control.updateUI();
525        contentPanePrivate.updateUI();
526
527    }
528
529    /**
530     * Add a new layer to the map. If no map exists, create one.
531     */
532    public final synchronized void addLayer(final Layer layer) {
533        boolean noMap = map == null;
534        if (noMap) {
535            createMapFrame(layer, null);
536        }
537        layer.hookUpMapView();
538        map.mapView.addLayer(layer);
539        if (noMap) {
540            Main.map.setVisible(true);
541        }
542    }
543
544    public synchronized void createMapFrame(Layer firstLayer, ViewportData viewportData) {
545        MapFrame mapFrame = new MapFrame(contentPanePrivate, viewportData);
546        setMapFrame(mapFrame);
547        if (firstLayer != null) {
548            mapFrame.selectMapMode((MapMode)mapFrame.getDefaultButtonAction(), firstLayer);
549        }
550        mapFrame.initializeDialogsPane();
551        // bootstrapping problem: make sure the layer list dialog is going to
552        // listen to change events of the very first layer
553        //
554        if (firstLayer != null) {
555            firstLayer.addPropertyChangeListener(LayerListDialog.getInstance().getModel());
556        }
557    }
558
559    /**
560     * Replies <code>true</code> if there is an edit layer
561     *
562     * @return <code>true</code> if there is an edit layer
563     */
564    public boolean hasEditLayer() {
565        if (getEditLayer() == null) return false;
566        return true;
567    }
568
569    /**
570     * Replies the current edit layer
571     *
572     * @return the current edit layer. <code>null</code>, if no current edit layer exists
573     */
574    public OsmDataLayer getEditLayer() {
575        if (!isDisplayingMapView()) return null;
576        return map.mapView.getEditLayer();
577    }
578
579    /**
580     * Replies the current data set.
581     *
582     * @return the current data set. <code>null</code>, if no current data set exists
583     */
584    public DataSet getCurrentDataSet() {
585        if (!hasEditLayer()) return null;
586        return getEditLayer().data;
587    }
588
589    /**
590     * Returns the currently active  layer
591     *
592     * @return the currently active layer. <code>null</code>, if currently no active layer exists
593     */
594    public Layer getActiveLayer() {
595        if (!isDisplayingMapView()) return null;
596        return map.mapView.getActiveLayer();
597    }
598
599    protected static final JPanel contentPanePrivate = new JPanel(new BorderLayout());
600
601    public static void redirectToMainContentPane(JComponent source) {
602        RedirectInputMap.redirect(source, contentPanePrivate);
603    }
604
605    public static void registerActionShortcut(JosmAction action) {
606        registerActionShortcut(action, action.getShortcut());
607    }
608
609    public static void registerActionShortcut(Action action, Shortcut shortcut) {
610        KeyStroke keyStroke = shortcut.getKeyStroke();
611        if (keyStroke == null)
612            return;
613
614        InputMap inputMap = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
615        Object existing = inputMap.get(keyStroke);
616        if (existing != null && !existing.equals(action)) {
617            info(String.format("Keystroke %s is already assigned to %s, will be overridden by %s", keyStroke, existing, action));
618        }
619        inputMap.put(keyStroke, action);
620
621        contentPanePrivate.getActionMap().put(action, action);
622    }
623
624    public static void unregisterShortcut(Shortcut shortcut) {
625        contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(shortcut.getKeyStroke());
626    }
627
628    public static void unregisterActionShortcut(JosmAction action) {
629        unregisterActionShortcut(action, action.getShortcut());
630    }
631
632    public static void unregisterActionShortcut(Action action, Shortcut shortcut) {
633        unregisterShortcut(shortcut);
634        contentPanePrivate.getActionMap().remove(action);
635    }
636
637    /**
638     * Replies the registered action for the given shortcut
639     * @param shortcut The shortcut to look for
640     * @return the registered action for the given shortcut
641     * @since 5696
642     */
643    public static Action getRegisteredActionShortcut(Shortcut shortcut) {
644        KeyStroke keyStroke = shortcut.getKeyStroke();
645        if (keyStroke == null)
646            return null;
647        Object action = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).get(keyStroke);
648        if (action instanceof Action)
649            return (Action) action;
650        return null;
651    }
652
653    ///////////////////////////////////////////////////////////////////////////
654    //  Implementation part
655    ///////////////////////////////////////////////////////////////////////////
656
657    /**
658     * Global panel.
659     */
660    public static final JPanel panel = new JPanel(new BorderLayout());
661
662    protected static WindowGeometry geometry;
663    protected static int windowState = JFrame.NORMAL;
664
665    private final CommandQueueListener redoUndoListener = new CommandQueueListener(){
666        @Override
667        public void commandChanged(final int queueSize, final int redoSize) {
668            menu.undo.setEnabled(queueSize > 0);
669            menu.redo.setEnabled(redoSize > 0);
670        }
671    };
672
673    /**
674     * Should be called before the main constructor to setup some parameter stuff
675     * @param args The parsed argument list.
676     */
677    public static void preConstructorInit(Map<Option, Collection<String>> args) {
678        ProjectionPreference.setProjection();
679
680        try {
681            String defaultlaf = platform.getDefaultStyle();
682            String laf = Main.pref.get("laf", defaultlaf);
683            try {
684                UIManager.setLookAndFeel(laf);
685            }
686            catch (final ClassNotFoundException e) {
687                info("Look and Feel not found: " + laf);
688                Main.pref.put("laf", defaultlaf);
689            }
690            catch (final UnsupportedLookAndFeelException e) {
691                info("Look and Feel not supported: " + laf);
692                Main.pref.put("laf", defaultlaf);
693            }
694            toolbar = new ToolbarPreferences();
695            contentPanePrivate.updateUI();
696            panel.updateUI();
697        } catch (final Exception e) {
698            e.printStackTrace();
699        }
700        UIManager.put("OptionPane.okIcon", ImageProvider.get("ok"));
701        UIManager.put("OptionPane.yesIcon", UIManager.get("OptionPane.okIcon"));
702        UIManager.put("OptionPane.cancelIcon", ImageProvider.get("cancel"));
703        UIManager.put("OptionPane.noIcon", UIManager.get("OptionPane.cancelIcon"));
704
705        I18n.translateJavaInternalMessages();
706
707        // init default coordinate format
708        //
709        try {
710            CoordinateFormat.setCoordinateFormat(CoordinateFormat.valueOf(Main.pref.get("coordinates")));
711        } catch (IllegalArgumentException iae) {
712            CoordinateFormat.setCoordinateFormat(CoordinateFormat.DECIMAL_DEGREES);
713        }
714
715        geometry = WindowGeometry.mainWindow("gui.geometry",
716            (args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null),
717            !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false));
718    }
719
720    protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) {
721        if (args.containsKey(Option.DOWNLOAD)) {
722            List<File> fileList = new ArrayList<File>();
723            for (String s : args.get(Option.DOWNLOAD)) {
724                File f = null;
725                switch(paramType(s)) {
726                case httpUrl:
727                    downloadFromParamHttp(false, s);
728                    break;
729                case bounds:
730                    downloadFromParamBounds(false, s);
731                    break;
732                case fileUrl:
733                    try {
734                        f = new File(new URI(s));
735                    } catch (URISyntaxException e) {
736                        JOptionPane.showMessageDialog(
737                                Main.parent,
738                                tr("Ignoring malformed file URL: \"{0}\"", s),
739                                tr("Warning"),
740                                JOptionPane.WARNING_MESSAGE
741                                );
742                    }
743                    if (f!=null) {
744                        fileList.add(f);
745                    }
746                    break;
747                case fileName:
748                    f = new File(s);
749                    fileList.add(f);
750                    break;
751                }
752            }
753            if(!fileList.isEmpty())
754            {
755                OpenFileAction.openFiles(fileList, true);
756            }
757        }
758        if (args.containsKey(Option.DOWNLOADGPS)) {
759            for (String s : args.get(Option.DOWNLOADGPS)) {
760                switch(paramType(s)) {
761                case httpUrl:
762                    downloadFromParamHttp(true, s);
763                    break;
764                case bounds:
765                    downloadFromParamBounds(true, s);
766                    break;
767                case fileUrl:
768                case fileName:
769                    JOptionPane.showMessageDialog(
770                            Main.parent,
771                            tr("Parameter \"downloadgps\" does not accept file names or file URLs"),
772                            tr("Warning"),
773                            JOptionPane.WARNING_MESSAGE
774                            );
775                }
776            }
777        }
778        if (args.containsKey(Option.SELECTION)) {
779            for (String s : args.get(Option.SELECTION)) {
780                SearchAction.search(s, SearchAction.SearchMode.add);
781            }
782        }
783    }
784
785    /**
786     * Asks user to perform "save layer" operations (save .osm on disk and/or upload osm data to server) for all {@link OsmDataLayer} before JOSM exits.
787     * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
788     * @since 2025
789     */
790    public static boolean saveUnsavedModifications() {
791        if (!isDisplayingMapView()) return true;
792        return saveUnsavedModifications(map.mapView.getLayersOfType(OsmDataLayer.class), true);
793    }
794
795    /**
796     * Asks user to perform "save layer" operations (save .osm on disk and/or upload osm data to server) before osm layers deletion.
797     *
798     * @param selectedLayers The layers to check. Only instances of {@link OsmDataLayer} are considered.
799     * @param exit {@code true} if JOSM is exiting, {@code false} otherwise.
800     * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. {@code false} if the user cancels.
801     * @since 5519
802     */
803    public static boolean saveUnsavedModifications(List<? extends Layer> selectedLayers, boolean exit) {
804        SaveLayersDialog dialog = new SaveLayersDialog(parent);
805        List<OsmDataLayer> layersWithUnmodifiedChanges = new ArrayList<OsmDataLayer>();
806        for (Layer l: selectedLayers) {
807            if (!(l instanceof OsmDataLayer)) {
808                continue;
809            }
810            OsmDataLayer odl = (OsmDataLayer)l;
811            if ((odl.requiresSaveToFile() || (odl.requiresUploadToServer() && !odl.isUploadDiscouraged())) && odl.data.isModified()) {
812                layersWithUnmodifiedChanges.add(odl);
813            }
814        }
815        if (exit) {
816            dialog.prepareForSavingAndUpdatingLayersBeforeExit();
817        } else {
818            dialog.prepareForSavingAndUpdatingLayersBeforeDelete();
819        }
820        if (!layersWithUnmodifiedChanges.isEmpty()) {
821            dialog.getModel().populate(layersWithUnmodifiedChanges);
822            dialog.setVisible(true);
823            switch(dialog.getUserAction()) {
824            case CANCEL: return false;
825            case PROCEED: return true;
826            default: return false;
827            }
828        }
829
830        return true;
831    }
832
833    /**
834     * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). If there are some unsaved data layers, asks first for user confirmation.
835     * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code.
836     * @param exitCode The return code
837     * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation.
838     * @since 3378
839     */
840    public static boolean exitJosm(boolean exit, int exitCode) {
841        if (Main.saveUnsavedModifications()) {
842            geometry.remember("gui.geometry");
843            if (map != null) {
844                map.rememberToggleDialogWidth();
845            }
846            pref.put("gui.maximized", (windowState & JFrame.MAXIMIZED_BOTH) != 0);
847            // Remove all layers because somebody may rely on layerRemoved events (like AutosaveTask)
848            if (Main.isDisplayingMapView()) {
849                Collection<Layer> layers = new ArrayList<Layer>(Main.map.mapView.getAllLayers());
850                for (Layer l: layers) {
851                    Main.main.removeLayer(l);
852                }
853            }
854            if (exit) {
855                System.exit(exitCode);
856            }
857            return true;
858        }
859        return false;
860    }
861
862    /**
863     * The type of a command line parameter, to be used in switch statements.
864     * @see #paramType
865     */
866    private enum DownloadParamType { httpUrl, fileUrl, bounds, fileName }
867
868    /**
869     * Guess the type of a parameter string specified on the command line with --download= or --downloadgps.
870     * @param s A parameter string
871     * @return The guessed parameter type
872     */
873    private static DownloadParamType paramType(String s) {
874        if(s.startsWith("http:")) return DownloadParamType.httpUrl;
875        if(s.startsWith("file:")) return DownloadParamType.fileUrl;
876        String coorPattern = "\\s*[+-]?[0-9]+(\\.[0-9]+)?\\s*";
877        if(s.matches(coorPattern+"(,"+coorPattern+"){3}")) return DownloadParamType.bounds;
878        // everything else must be a file name
879        return DownloadParamType.fileName;
880    }
881
882    /**
883     * Download area specified on the command line as OSM URL.
884     * @param rawGps Flag to download raw GPS tracks
885     * @param s The URL parameter
886     */
887    private static void downloadFromParamHttp(final boolean rawGps, String s) {
888        final Bounds b = OsmUrlToBounds.parse(s);
889        if (b == null) {
890            JOptionPane.showMessageDialog(
891                    Main.parent,
892                    tr("Ignoring malformed URL: \"{0}\"", s),
893                    tr("Warning"),
894                    JOptionPane.WARNING_MESSAGE
895                    );
896        } else {
897            downloadFromParamBounds(rawGps, b);
898        }
899    }
900
901    /**
902     * Download area specified on the command line as bounds string.
903     * @param rawGps Flag to download raw GPS tracks
904     * @param s The bounds parameter
905     */
906    private static void downloadFromParamBounds(final boolean rawGps, String s) {
907        final StringTokenizer st = new StringTokenizer(s, ",");
908        if (st.countTokens() == 4) {
909            Bounds b = new Bounds(
910                    new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken())),
911                    new LatLon(Double.parseDouble(st.nextToken()),Double.parseDouble(st.nextToken()))
912                    );
913            downloadFromParamBounds(rawGps, b);
914        }
915    }
916
917    /**
918     * Download area specified as Bounds value.
919     * @param rawGps Flag to download raw GPS tracks
920     * @param b The bounds value
921     * @see #downloadFromParamBounds(boolean, String)
922     * @see #downloadFromParamHttp
923     */
924    private static void downloadFromParamBounds(final boolean rawGps, Bounds b) {
925        DownloadTask task = rawGps ? new DownloadGpsTask() : new DownloadOsmTask();
926        // asynchronously launch the download task ...
927        Future<?> future = task.download(true, b, null);
928        // ... and the continuation when the download is finished (this will wait for the download to finish)
929        Main.worker.execute(new PostDownloadHandler(task, future));
930    }
931
932    /**
933     * Identifies the current operating system family and initializes the platform hook accordingly.
934     * @since 1849
935     */
936    public static void determinePlatformHook() {
937        String os = System.getProperty("os.name");
938        if (os == null) {
939            warn("Your operating system has no name, so I'm guessing its some kind of *nix.");
940            platform = new PlatformHookUnixoid();
941        } else if (os.toLowerCase().startsWith("windows")) {
942            platform = new PlatformHookWindows();
943        } else if (os.equals("Linux") || os.equals("Solaris") ||
944                os.equals("SunOS") || os.equals("AIX") ||
945                os.equals("FreeBSD") || os.equals("NetBSD") || os.equals("OpenBSD")) {
946            platform = new PlatformHookUnixoid();
947        } else if (os.toLowerCase().startsWith("mac os x")) {
948            platform = new PlatformHookOsx();
949        } else {
950            warn("I don't know your operating system '"+os+"', so I'm guessing its some kind of *nix.");
951            platform = new PlatformHookUnixoid();
952        }
953    }
954
955    private static class WindowPositionSizeListener extends WindowAdapter implements
956    ComponentListener {
957        @Override
958        public void windowStateChanged(WindowEvent e) {
959            Main.windowState = e.getNewState();
960        }
961
962        @Override
963        public void componentHidden(ComponentEvent e) {
964        }
965
966        @Override
967        public void componentMoved(ComponentEvent e) {
968            handleComponentEvent(e);
969        }
970
971        @Override
972        public void componentResized(ComponentEvent e) {
973            handleComponentEvent(e);
974        }
975
976        @Override
977        public void componentShown(ComponentEvent e) {
978        }
979
980        private void handleComponentEvent(ComponentEvent e) {
981            Component c = e.getComponent();
982            if (c instanceof JFrame && c.isVisible()) {
983                if(Main.windowState == JFrame.NORMAL) {
984                    Main.geometry = new WindowGeometry((JFrame) c);
985                } else {
986                    Main.geometry.fixScreen((JFrame) c);
987                }
988            }
989        }
990    }
991
992    protected static void addListener() {
993        parent.addComponentListener(new WindowPositionSizeListener());
994        ((JFrame)parent).addWindowStateListener(new WindowPositionSizeListener());
995    }
996
997    /**
998     * Checks that JOSM is at least running with Java 6.
999     * @since 3815
1000     */
1001    public static void checkJava6() {
1002        String version = System.getProperty("java.version");
1003        if (version != null) {
1004            if (version.startsWith("1.6") || version.startsWith("6") ||
1005                    version.startsWith("1.7") || version.startsWith("7") ||
1006                    version.startsWith("1.8") || version.startsWith("8") ||
1007                    version.startsWith("1.9") || version.startsWith("9"))
1008                return;
1009            if (version.startsWith("1.5") || version.startsWith("5")) {
1010                JLabel ho = new JLabel("<html>"+
1011                        tr("<h2>JOSM requires Java version 6.</h2>"+
1012                                "Detected Java version: {0}.<br>"+
1013                                "You can <ul><li>update your Java (JRE) or</li>"+
1014                                "<li>use an earlier (Java 5 compatible) version of JOSM.</li></ul>"+
1015                                "More Info:", version)+"</html>");
1016                JTextArea link = new JTextArea(HelpUtil.getWikiBaseHelpUrl()+"/Help/SystemRequirements");
1017                link.setEditable(false);
1018                link.setBackground(panel.getBackground());
1019                JPanel panel = new JPanel(new GridBagLayout());
1020                GridBagConstraints gbc = new GridBagConstraints();
1021                gbc.gridwidth = GridBagConstraints.REMAINDER;
1022                gbc.anchor = GridBagConstraints.WEST;
1023                gbc.weightx = 1.0;
1024                panel.add(ho, gbc);
1025                panel.add(link, gbc);
1026                final String EXIT = tr("Exit JOSM");
1027                final String CONTINUE = tr("Continue, try anyway");
1028                int ret = JOptionPane.showOptionDialog(null, panel, tr("Error"), JOptionPane.YES_NO_OPTION, JOptionPane.ERROR_MESSAGE, null, new String[] {EXIT, CONTINUE}, EXIT);
1029                if (ret == 0) {
1030                    System.exit(0);
1031                }
1032                return;
1033            }
1034        }
1035        error("Could not recognize Java Version: "+version);
1036    }
1037
1038    /* ----------------------------------------------------------------------------------------- */
1039    /* projection handling  - Main is a registry for a single, global projection instance        */
1040    /*                                                                                           */
1041    /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */
1042    /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class.     */
1043    /* ----------------------------------------------------------------------------------------- */
1044    /**
1045     * The projection method used.
1046     * use {@link #getProjection()} and {@link #setProjection(Projection)} for access.
1047     * Use {@link #setProjection(Projection)} in order to trigger a projection change event.
1048     */
1049    private static Projection proj;
1050
1051    /**
1052     * Replies the current projection.
1053     *
1054     * @return the currently active projection
1055     */
1056    public static Projection getProjection() {
1057        return proj;
1058    }
1059
1060    /**
1061     * Sets the current projection
1062     *
1063     * @param p the projection
1064     */
1065    public static void setProjection(Projection p) {
1066        CheckParameterUtil.ensureParameterNotNull(p);
1067        Projection oldValue = proj;
1068        Bounds b = isDisplayingMapView() ? map.mapView.getRealBounds() : null;
1069        proj = p;
1070        fireProjectionChanged(oldValue, proj, b);
1071    }
1072
1073    /*
1074     * Keep WeakReferences to the listeners. This relieves clients from the burden of
1075     * explicitly removing the listeners and allows us to transparently register every
1076     * created dataset as projection change listener.
1077     */
1078    private static final List<WeakReference<ProjectionChangeListener>> listeners = new ArrayList<WeakReference<ProjectionChangeListener>>();
1079
1080    private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) {
1081        if (newValue == null ^ oldValue == null
1082                || (newValue != null && oldValue != null && !Utils.equal(newValue.toCode(), oldValue.toCode()))) {
1083
1084            synchronized(Main.class) {
1085                Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator();
1086                while (it.hasNext()){
1087                    WeakReference<ProjectionChangeListener> wr = it.next();
1088                    ProjectionChangeListener listener = wr.get();
1089                    if (listener == null) {
1090                        it.remove();
1091                        continue;
1092                    }
1093                    listener.projectionChanged(oldValue, newValue);
1094                }
1095            }
1096            if (newValue != null && oldBounds != null) {
1097                Main.map.mapView.zoomTo(oldBounds);
1098            }
1099            /* TODO - remove layers with fixed projection */
1100        }
1101    }
1102
1103    /**
1104     * Register a projection change listener.
1105     *
1106     * @param listener the listener. Ignored if <code>null</code>.
1107     */
1108    public static void addProjectionChangeListener(ProjectionChangeListener listener) {
1109        if (listener == null) return;
1110        synchronized (Main.class) {
1111            for (WeakReference<ProjectionChangeListener> wr : listeners) {
1112                // already registered ? => abort
1113                if (wr.get() == listener) return;
1114            }
1115            listeners.add(new WeakReference<ProjectionChangeListener>(listener));
1116        }
1117    }
1118
1119    /**
1120     * Removes a projection change listener.
1121     *
1122     * @param listener the listener. Ignored if <code>null</code>.
1123     */
1124    public static void removeProjectionChangeListener(ProjectionChangeListener listener) {
1125        if (listener == null) return;
1126        synchronized(Main.class){
1127            Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator();
1128            while (it.hasNext()){
1129                WeakReference<ProjectionChangeListener> wr = it.next();
1130                // remove the listener - and any other listener which got garbage
1131                // collected in the meantime
1132                if (wr.get() == null || wr.get() == listener) {
1133                    it.remove();
1134                }
1135            }
1136        }
1137    }
1138
1139    /**
1140     * Listener for window switch events.
1141     *
1142     * These are events, when the user activates a window of another application
1143     * or comes back to JOSM. Window switches from one JOSM window to another
1144     * are not reported.
1145     */
1146    public static interface WindowSwitchListener {
1147        /**
1148         * Called when the user activates a window of another application.
1149         */
1150        void toOtherApplication();
1151        /**
1152         * Called when the user comes from a window of another application
1153         * back to JOSM.
1154         */
1155        void fromOtherApplication();
1156    }
1157
1158    private static final List<WeakReference<WindowSwitchListener>> windowSwitchListeners = new ArrayList<WeakReference<WindowSwitchListener>>();
1159
1160    /**
1161     * Register a window switch listener.
1162     *
1163     * @param listener the listener. Ignored if <code>null</code>.
1164     */
1165    public static void addWindowSwitchListener(WindowSwitchListener listener) {
1166        if (listener == null) return;
1167        synchronized (Main.class) {
1168            for (WeakReference<WindowSwitchListener> wr : windowSwitchListeners) {
1169                // already registered ? => abort
1170                if (wr.get() == listener) return;
1171            }
1172            boolean wasEmpty = windowSwitchListeners.isEmpty();
1173            windowSwitchListeners.add(new WeakReference<WindowSwitchListener>(listener));
1174            if (wasEmpty) {
1175                // The following call will have no effect, when there is no window
1176                // at the time. Therefore, MasterWindowListener.setup() will also be
1177                // called, as soon as the main window is shown.
1178                MasterWindowListener.setup();
1179            }
1180        }
1181    }
1182
1183    /**
1184     * Removes a window switch listener.
1185     *
1186     * @param listener the listener. Ignored if <code>null</code>.
1187     */
1188    public static void removeWindowSwitchListener(WindowSwitchListener listener) {
1189        if (listener == null) return;
1190        synchronized (Main.class){
1191            Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1192            while (it.hasNext()){
1193                WeakReference<WindowSwitchListener> wr = it.next();
1194                // remove the listener - and any other listener which got garbage
1195                // collected in the meantime
1196                if (wr.get() == null || wr.get() == listener) {
1197                    it.remove();
1198                }
1199            }
1200            if (windowSwitchListeners.isEmpty()) {
1201                MasterWindowListener.teardown();
1202            }
1203        }
1204    }
1205
1206    /**
1207     * WindowListener, that is registered on all Windows of the application.
1208     *
1209     * Its purpose is to notify WindowSwitchListeners, that the user switches to
1210     * another application, e.g. a browser, or back to JOSM.
1211     *
1212     * When changing from JOSM to another application and back (e.g. two times
1213     * alt+tab), the active Window within JOSM may be different.
1214     * Therefore, we need to register listeners to <strong>all</strong> (visible)
1215     * Windows in JOSM, and it does not suffice to monitor the one that was
1216     * deactivated last.
1217     *
1218     * This class is only "active" on demand, i.e. when there is at least one
1219     * WindowSwitchListener registered.
1220     */
1221    protected static class MasterWindowListener extends WindowAdapter {
1222
1223        private static MasterWindowListener INSTANCE;
1224
1225        public static MasterWindowListener getInstance() {
1226            if (INSTANCE == null) {
1227                INSTANCE = new MasterWindowListener();
1228            }
1229            return INSTANCE;
1230        }
1231
1232        /**
1233         * Register listeners to all non-hidden windows.
1234         *
1235         * Windows that are created later, will be cared for in {@link #windowDeactivated(WindowEvent)}.
1236         */
1237        public static void setup() {
1238            if (!windowSwitchListeners.isEmpty()) {
1239                for (Window w : Window.getWindows()) {
1240                    if (w.isShowing()) {
1241                        if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
1242                            w.addWindowListener(getInstance());
1243                        }
1244                    }
1245                }
1246            }
1247        }
1248
1249        /**
1250         * Unregister all listeners.
1251         */
1252        public static void teardown() {
1253            for (Window w : Window.getWindows()) {
1254                w.removeWindowListener(getInstance());
1255            }
1256        }
1257
1258        @Override
1259        public void windowActivated(WindowEvent e) {
1260            if (e.getOppositeWindow() == null) { // we come from a window of a different application
1261                // fire WindowSwitchListeners
1262                synchronized (Main.class) {
1263                    Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1264                    while (it.hasNext()){
1265                        WeakReference<WindowSwitchListener> wr = it.next();
1266                        WindowSwitchListener listener = wr.get();
1267                        if (listener == null) {
1268                            it.remove();
1269                            continue;
1270                        }
1271                        listener.fromOtherApplication();
1272                    }
1273                }
1274            }
1275        }
1276
1277        @Override
1278        public void windowDeactivated(WindowEvent e) {
1279            // set up windows that have been created in the meantime
1280            for (Window w : Window.getWindows()) {
1281                if (!w.isShowing()) {
1282                    w.removeWindowListener(getInstance());
1283                } else {
1284                    if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) {
1285                        w.addWindowListener(getInstance());
1286                    }
1287                }
1288            }
1289            if (e.getOppositeWindow() == null) { // we go to a window of a different application
1290                // fire WindowSwitchListeners
1291                synchronized (Main.class) {
1292                    Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator();
1293                    while (it.hasNext()){
1294                        WeakReference<WindowSwitchListener> wr = it.next();
1295                        WindowSwitchListener listener = wr.get();
1296                        if (listener == null) {
1297                            it.remove();
1298                            continue;
1299                        }
1300                        listener.toOtherApplication();
1301                    }
1302                }
1303            }
1304        }
1305    }
1306
1307    /**
1308     * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes
1309     * @param listener The MapFrameListener
1310     * @return {@code true} if the listeners collection changed as a result of the call
1311     * @since 5957
1312     */
1313    public static boolean addMapFrameListener(MapFrameListener listener) {
1314        return listener != null ? mapFrameListeners.add(listener) : false;
1315    }
1316
1317    /**
1318     * Unregisters the given {@code MapFrameListener} from MapFrame changes
1319     * @param listener The MapFrameListener
1320     * @return {@code true} if the listeners collection changed as a result of the call
1321     * @since 5957
1322     */
1323    public static boolean removeMapFrameListener(MapFrameListener listener) {
1324        return listener != null ? mapFrameListeners.remove(listener) : false;
1325    }
1326}