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