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