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}