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