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