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