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.io.IOException; 018import java.lang.ref.WeakReference; 019import java.net.URI; 020import java.net.URISyntaxException; 021import java.net.URL; 022import java.text.MessageFormat; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.Iterator; 030import java.util.List; 031import java.util.Locale; 032import java.util.Map; 033import java.util.Objects; 034import java.util.Set; 035import java.util.StringTokenizer; 036import java.util.concurrent.Callable; 037import java.util.concurrent.ExecutorService; 038import java.util.concurrent.Executors; 039import java.util.concurrent.Future; 040import java.util.logging.Handler; 041import java.util.logging.Level; 042import java.util.logging.LogRecord; 043import java.util.logging.Logger; 044 045import javax.swing.Action; 046import javax.swing.InputMap; 047import javax.swing.JComponent; 048import javax.swing.JFrame; 049import javax.swing.JOptionPane; 050import javax.swing.JPanel; 051import javax.swing.JTextArea; 052import javax.swing.KeyStroke; 053import javax.swing.LookAndFeel; 054import javax.swing.UIManager; 055import javax.swing.UnsupportedLookAndFeelException; 056 057import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 058import org.openstreetmap.josm.actions.JosmAction; 059import org.openstreetmap.josm.actions.OpenFileAction; 060import org.openstreetmap.josm.actions.OpenLocationAction; 061import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask; 062import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 063import org.openstreetmap.josm.actions.downloadtasks.DownloadTask; 064import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 065import org.openstreetmap.josm.actions.mapmode.DrawAction; 066import org.openstreetmap.josm.actions.mapmode.MapMode; 067import org.openstreetmap.josm.actions.search.SearchAction; 068import org.openstreetmap.josm.data.Bounds; 069import org.openstreetmap.josm.data.Preferences; 070import org.openstreetmap.josm.data.ProjectionBounds; 071import org.openstreetmap.josm.data.UndoRedoHandler; 072import org.openstreetmap.josm.data.ViewportData; 073import org.openstreetmap.josm.data.cache.JCSCacheManager; 074import org.openstreetmap.josm.data.coor.CoordinateFormat; 075import org.openstreetmap.josm.data.coor.LatLon; 076import org.openstreetmap.josm.data.osm.DataSet; 077import org.openstreetmap.josm.data.osm.OsmPrimitive; 078import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy; 079import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 080import org.openstreetmap.josm.data.projection.Projection; 081import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 082import org.openstreetmap.josm.data.validation.OsmValidator; 083import org.openstreetmap.josm.gui.GettingStarted; 084import org.openstreetmap.josm.gui.MainApplication.Option; 085import org.openstreetmap.josm.gui.MainMenu; 086import org.openstreetmap.josm.gui.MapFrame; 087import org.openstreetmap.josm.gui.MapFrameListener; 088import org.openstreetmap.josm.gui.MapView; 089import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 090import org.openstreetmap.josm.gui.help.HelpUtil; 091import org.openstreetmap.josm.gui.io.SaveLayersDialog; 092import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer; 093import org.openstreetmap.josm.gui.layer.Layer; 094import org.openstreetmap.josm.gui.layer.OsmDataLayer; 095import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener; 096import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 097import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 098import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference; 099import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 100import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 101import org.openstreetmap.josm.gui.progress.ProgressMonitorExecutor; 102import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets; 103import org.openstreetmap.josm.gui.util.RedirectInputMap; 104import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 105import org.openstreetmap.josm.io.FileWatcher; 106import org.openstreetmap.josm.io.OnlineResource; 107import org.openstreetmap.josm.io.OsmApi; 108import org.openstreetmap.josm.plugins.PluginHandler; 109import org.openstreetmap.josm.tools.CheckParameterUtil; 110import org.openstreetmap.josm.tools.I18n; 111import org.openstreetmap.josm.tools.ImageProvider; 112import org.openstreetmap.josm.tools.OpenBrowser; 113import org.openstreetmap.josm.tools.OsmUrlToBounds; 114import org.openstreetmap.josm.tools.PlatformHook; 115import org.openstreetmap.josm.tools.PlatformHookOsx; 116import org.openstreetmap.josm.tools.PlatformHookUnixoid; 117import org.openstreetmap.josm.tools.PlatformHookWindows; 118import org.openstreetmap.josm.tools.Shortcut; 119import org.openstreetmap.josm.tools.Utils; 120import org.openstreetmap.josm.tools.WindowGeometry; 121 122/** 123 * Abstract class holding various static global variables and methods used in large parts of JOSM application. 124 * @since 98 125 */ 126public abstract class Main { 127 128 /** 129 * The JOSM website URL. 130 * @since 6897 (was public from 6143 to 6896) 131 */ 132 private static final String JOSM_WEBSITE = "https://josm.openstreetmap.de"; 133 134 /** 135 * The OSM website URL. 136 * @since 6897 (was public from 6453 to 6896) 137 */ 138 private static final String OSM_WEBSITE = "https://www.openstreetmap.org"; 139 140 /** 141 * Replies true if JOSM currently displays a map view. False, if it doesn't, i.e. if 142 * it only shows the MOTD panel. 143 * 144 * @return <code>true</code> if JOSM currently displays a map view 145 */ 146 public static boolean isDisplayingMapView() { 147 return map != null && map.mapView != null; 148 } 149 150 /** 151 * Global parent component for all dialogs and message boxes 152 */ 153 public static Component parent; 154 155 /** 156 * Global application. 157 */ 158 public static volatile Main main; 159 160 /** 161 * Command-line arguments used to run the application. 162 */ 163 protected static final List<String> COMMAND_LINE_ARGS = new ArrayList<>(); 164 165 /** 166 * The worker thread slave. This is for executing all long and intensive 167 * calculations. The executed runnables are guaranteed to be executed separately 168 * and sequential. 169 */ 170 public static final ExecutorService worker = new ProgressMonitorExecutor("main-worker-%d", Thread.NORM_PRIORITY); 171 172 /** 173 * Global application preferences 174 */ 175 public static Preferences pref; 176 177 /** 178 * The global paste buffer. 179 */ 180 public static final PrimitiveDeepCopy pasteBuffer = new PrimitiveDeepCopy(); 181 182 /** 183 * The layer source from which {@link Main#pasteBuffer} data comes from. 184 */ 185 public static Layer pasteSource; 186 187 /** 188 * The MapFrame. Use {@link Main#setMapFrame} to set or clear it. 189 */ 190 public static MapFrame map; 191 192 /** 193 * The toolbar preference control to register new actions. 194 */ 195 public static volatile ToolbarPreferences toolbar; 196 197 /** 198 * The commands undo/redo handler. 199 */ 200 public final UndoRedoHandler undoRedo = new UndoRedoHandler(); 201 202 /** 203 * The progress monitor being currently displayed. 204 */ 205 public static PleaseWaitProgressMonitor currentProgressMonitor; 206 207 /** 208 * The main menu bar at top of screen. 209 */ 210 public MainMenu menu; 211 212 /** 213 * The data validation handler. 214 */ 215 public OsmValidator validator; 216 217 /** 218 * The file watcher service. 219 */ 220 public static final FileWatcher fileWatcher = new FileWatcher(); 221 222 /** 223 * The MOTD Layer. 224 */ 225 public final GettingStarted gettingStarted = new GettingStarted(); 226 227 private static final Collection<MapFrameListener> mapFrameListeners = new ArrayList<>(); 228 229 protected static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>(); 230 231 // First lines of last 5 error and warning messages, used for bug reports 232 private static final List<String> ERRORS_AND_WARNINGS = Collections.<String>synchronizedList(new ArrayList<String>()); 233 234 private static final Set<OnlineResource> OFFLINE_RESOURCES = new HashSet<>(); 235 236 /** 237 * Logging level (5 = trace, 4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none). 238 * @since 6248 239 */ 240 public static int logLevel = 3; 241 242 private static void rememberWarnErrorMsg(String msg) { 243 // Only remember first line of message 244 int idx = msg.indexOf('\n'); 245 if (idx > 0) { 246 ERRORS_AND_WARNINGS.add(msg.substring(0, idx)); 247 } else { 248 ERRORS_AND_WARNINGS.add(msg); 249 } 250 // Only keep 5 lines to avoid memory leak and incomplete stacktraces in bug reports 251 while (ERRORS_AND_WARNINGS.size() > 5) { 252 ERRORS_AND_WARNINGS.remove(0); 253 } 254 } 255 256 /** 257 * Replies the first lines of last 5 error and warning messages, used for bug reports 258 * @return the first lines of last 5 error and warning messages 259 * @since 7420 260 */ 261 public static final Collection<String> getLastErrorAndWarnings() { 262 return Collections.unmodifiableList(ERRORS_AND_WARNINGS); 263 } 264 265 /** 266 * Clears the list of last error and warning messages. 267 * @since 8959 268 */ 269 public static void clearLastErrorAndWarnings() { 270 ERRORS_AND_WARNINGS.clear(); 271 } 272 273 /** 274 * Prints an error message if logging is on. 275 * @param msg The message to print. 276 * @since 6248 277 */ 278 public static void error(String msg) { 279 if (logLevel < 1) 280 return; 281 if (msg != null && !msg.isEmpty()) { 282 System.err.println(tr("ERROR: {0}", msg)); 283 rememberWarnErrorMsg("E: "+msg); 284 } 285 } 286 287 /** 288 * Prints a warning message if logging is on. 289 * @param msg The message to print. 290 */ 291 public static void warn(String msg) { 292 if (logLevel < 2) 293 return; 294 if (msg != null && !msg.isEmpty()) { 295 System.err.println(tr("WARNING: {0}", msg)); 296 rememberWarnErrorMsg("W: "+msg); 297 } 298 } 299 300 /** 301 * Prints an informational message if logging is on. 302 * @param msg The message to print. 303 */ 304 public static void info(String msg) { 305 if (logLevel < 3) 306 return; 307 if (msg != null && !msg.isEmpty()) { 308 System.out.println(tr("INFO: {0}", msg)); 309 } 310 } 311 312 /** 313 * Prints a debug message if logging is on. 314 * @param msg The message to print. 315 */ 316 public static void debug(String msg) { 317 if (logLevel < 4) 318 return; 319 if (msg != null && !msg.isEmpty()) { 320 System.out.println(tr("DEBUG: {0}", msg)); 321 } 322 } 323 324 /** 325 * Prints a trace message if logging is on. 326 * @param msg The message to print. 327 */ 328 public static void trace(String msg) { 329 if (logLevel < 5) 330 return; 331 if (msg != null && !msg.isEmpty()) { 332 System.out.print("TRACE: "); 333 System.out.println(msg); 334 } 335 } 336 337 /** 338 * Determines if debug log level is enabled. 339 * Useful to avoid costly construction of debug messages when not enabled. 340 * @return {@code true} if log level is at least debug, {@code false} otherwise 341 * @since 6852 342 */ 343 public static boolean isDebugEnabled() { 344 return logLevel >= 4; 345 } 346 347 /** 348 * Determines if trace log level is enabled. 349 * Useful to avoid costly construction of trace messages when not enabled. 350 * @return {@code true} if log level is at least trace, {@code false} otherwise 351 * @since 6852 352 */ 353 public static boolean isTraceEnabled() { 354 return logLevel >= 5; 355 } 356 357 /** 358 * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format} 359 * function to format text. 360 * @param msg The formatted message to print. 361 * @param objects The objects to insert into format string. 362 * @since 6248 363 */ 364 public static void error(String msg, Object... objects) { 365 error(MessageFormat.format(msg, objects)); 366 } 367 368 /** 369 * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format} 370 * function to format text. 371 * @param msg The formatted message to print. 372 * @param objects The objects to insert into format string. 373 */ 374 public static void warn(String msg, Object... objects) { 375 warn(MessageFormat.format(msg, objects)); 376 } 377 378 /** 379 * Prints a formatted informational message if logging is on. Calls {@link MessageFormat#format} 380 * function to format text. 381 * @param msg The formatted message to print. 382 * @param objects The objects to insert into format string. 383 */ 384 public static void info(String msg, Object... objects) { 385 info(MessageFormat.format(msg, objects)); 386 } 387 388 /** 389 * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format} 390 * function to format text. 391 * @param msg The formatted message to print. 392 * @param objects The objects to insert into format string. 393 */ 394 public static void debug(String msg, Object... objects) { 395 debug(MessageFormat.format(msg, objects)); 396 } 397 398 /** 399 * Prints a formatted trace message if logging is on. Calls {@link MessageFormat#format} 400 * function to format text. 401 * @param msg The formatted message to print. 402 * @param objects The objects to insert into format string. 403 */ 404 public static void trace(String msg, Object... objects) { 405 trace(MessageFormat.format(msg, objects)); 406 } 407 408 /** 409 * Prints an error message for the given Throwable. 410 * @param t The throwable object causing the error 411 * @since 6248 412 */ 413 public static void error(Throwable t) { 414 error(t, true); 415 } 416 417 /** 418 * Prints a warning message for the given Throwable. 419 * @param t The throwable object causing the error 420 * @since 6248 421 */ 422 public static void warn(Throwable t) { 423 warn(t, true); 424 } 425 426 /** 427 * Prints an error message for the given Throwable. 428 * @param t The throwable object causing the error 429 * @param stackTrace {@code true}, if the stacktrace should be displayed 430 * @since 6642 431 */ 432 public static void error(Throwable t, boolean stackTrace) { 433 error(getErrorMessage(t)); 434 if (stackTrace) { 435 t.printStackTrace(); 436 } 437 } 438 439 /** 440 * Prints a warning message for the given Throwable. 441 * @param t The throwable object causing the error 442 * @param stackTrace {@code true}, if the stacktrace should be displayed 443 * @since 6642 444 */ 445 public static void warn(Throwable t, boolean stackTrace) { 446 warn(getErrorMessage(t)); 447 if (stackTrace) { 448 t.printStackTrace(); 449 } 450 } 451 452 /** 453 * Returns a human-readable message of error, also usable for developers. 454 * @param t The error 455 * @return The human-readable error message 456 * @since 6642 457 */ 458 public static String getErrorMessage(Throwable t) { 459 if (t == null) { 460 return null; 461 } 462 StringBuilder sb = new StringBuilder(t.getClass().getName()); 463 String msg = t.getMessage(); 464 if (msg != null) { 465 sb.append(": ").append(msg.trim()); 466 } 467 Throwable cause = t.getCause(); 468 if (cause != null && !cause.equals(t)) { 469 sb.append(". ").append(tr("Cause: ")).append(getErrorMessage(cause)); 470 } 471 return sb.toString(); 472 } 473 474 /** 475 * Platform specific code goes in here. 476 * Plugins may replace it, however, some hooks will be called before any plugins have been loeaded. 477 * So if you need to hook into those early ones, split your class and send the one with the early hooks 478 * to the JOSM team for inclusion. 479 */ 480 public static volatile PlatformHook platform; 481 482 /** 483 * Whether or not the java vm is openjdk 484 * We use this to work around openjdk bugs 485 */ 486 public static boolean isOpenjdk; 487 488 /** 489 * Initializes {@code Main.pref} in normal application context. 490 * @since 6471 491 */ 492 public static void initApplicationPreferences() { 493 Main.pref = new Preferences(); 494 } 495 496 /** 497 * Set or clear (if passed <code>null</code>) the map. 498 * @param map The map to set {@link Main#map} to. Can be null. 499 */ 500 public final void setMapFrame(final MapFrame map) { 501 MapFrame old = Main.map; 502 panel.setVisible(false); 503 panel.removeAll(); 504 if (map != null) { 505 map.fillPanel(panel); 506 } else { 507 old.destroy(); 508 panel.add(gettingStarted, BorderLayout.CENTER); 509 } 510 panel.setVisible(true); 511 redoUndoListener.commandChanged(0, 0); 512 513 Main.map = map; 514 515 for (MapFrameListener listener : mapFrameListeners) { 516 listener.mapFrameInitialized(old, map); 517 } 518 if (map == null && currentProgressMonitor != null) { 519 currentProgressMonitor.showForegroundDialog(); 520 } 521 } 522 523 /** 524 * Remove the specified layer from the map. If it is the last layer, 525 * remove the map as well. 526 * @param layer The layer to remove 527 */ 528 public final synchronized void removeLayer(final Layer layer) { 529 if (map != null) { 530 map.mapView.removeLayer(layer); 531 if (isDisplayingMapView() && map.mapView.getAllLayers().isEmpty()) { 532 setMapFrame(null); 533 } 534 } 535 } 536 537 private static volatile InitStatusListener initListener; 538 539 public interface InitStatusListener { 540 541 Object updateStatus(String event); 542 543 void finish(Object status); 544 } 545 546 public static void setInitStatusListener(InitStatusListener listener) { 547 CheckParameterUtil.ensureParameterNotNull(listener); 548 initListener = listener; 549 } 550 551 /** 552 * Constructs new {@code Main} object. A lot of global variables are initialized here. 553 */ 554 public Main() { 555 main = this; 556 isOpenjdk = System.getProperty("java.vm.name").toUpperCase(Locale.ENGLISH).indexOf("OPENJDK") != -1; 557 fileWatcher.start(); 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 static 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 // Ensures caret color is the same than text foreground color, see #12257 977 // See http://docs.oracle.com/javase/7/docs/api/javax/swing/plaf/synth/doc-files/componentProperties.html 978 for (String p : Arrays.asList( 979 "EditorPane", "FormattedTextField", "PasswordField", "TextArea", "TextField", "TextPane")) { 980 UIManager.put(p+".caretForeground", UIManager.getColor(p+".foreground")); 981 } 982 983 I18n.translateJavaInternalMessages(); 984 985 // init default coordinate format 986 // 987 try { 988 CoordinateFormat.setCoordinateFormat(CoordinateFormat.valueOf(Main.pref.get("coordinates"))); 989 } catch (IllegalArgumentException iae) { 990 CoordinateFormat.setCoordinateFormat(CoordinateFormat.DECIMAL_DEGREES); 991 } 992 993 geometry = WindowGeometry.mainWindow("gui.geometry", 994 args.containsKey(Option.GEOMETRY) ? args.get(Option.GEOMETRY).iterator().next() : null, 995 !args.containsKey(Option.NO_MAXIMIZE) && Main.pref.getBoolean("gui.maximized", false)); 996 } 997 998 protected static void postConstructorProcessCmdLine(Map<Option, Collection<String>> args) { 999 if (args.containsKey(Option.DOWNLOAD)) { 1000 List<File> fileList = new ArrayList<>(); 1001 for (String s : args.get(Option.DOWNLOAD)) { 1002 DownloadParamType.paramType(s).download(s, fileList); 1003 } 1004 if (!fileList.isEmpty()) { 1005 OpenFileAction.openFiles(fileList, true); 1006 } 1007 } 1008 if (args.containsKey(Option.DOWNLOADGPS)) { 1009 for (String s : args.get(Option.DOWNLOADGPS)) { 1010 DownloadParamType.paramType(s).downloadGps(s); 1011 } 1012 } 1013 if (args.containsKey(Option.SELECTION)) { 1014 for (String s : args.get(Option.SELECTION)) { 1015 SearchAction.search(s, SearchAction.SearchMode.add); 1016 } 1017 } 1018 } 1019 1020 /** 1021 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) for all 1022 * {@link AbstractModifiableLayer} before JOSM exits. 1023 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. 1024 * {@code false} if the user cancels. 1025 * @since 2025 1026 */ 1027 public static boolean saveUnsavedModifications() { 1028 if (!isDisplayingMapView()) return true; 1029 return saveUnsavedModifications(map.mapView.getLayersOfType(AbstractModifiableLayer.class), true); 1030 } 1031 1032 /** 1033 * Asks user to perform "save layer" operations (save on disk and/or upload data to server) before data layers deletion. 1034 * 1035 * @param selectedLayers The layers to check. Only instances of {@link AbstractModifiableLayer} are considered. 1036 * @param exit {@code true} if JOSM is exiting, {@code false} otherwise. 1037 * @return {@code true} if there was nothing to save, or if the user wants to proceed to save operations. 1038 * {@code false} if the user cancels. 1039 * @since 5519 1040 */ 1041 public static boolean saveUnsavedModifications(Iterable<? extends Layer> selectedLayers, boolean exit) { 1042 SaveLayersDialog dialog = new SaveLayersDialog(parent); 1043 List<AbstractModifiableLayer> layersWithUnmodifiedChanges = new ArrayList<>(); 1044 for (Layer l: selectedLayers) { 1045 if (!(l instanceof AbstractModifiableLayer)) { 1046 continue; 1047 } 1048 AbstractModifiableLayer odl = (AbstractModifiableLayer) l; 1049 if (odl.isModified() && 1050 ((!odl.isSavable() && !odl.isUploadable()) || 1051 odl.requiresSaveToFile() || 1052 (odl.requiresUploadToServer() && !odl.isUploadDiscouraged()))) { 1053 layersWithUnmodifiedChanges.add(odl); 1054 } 1055 } 1056 if (exit) { 1057 dialog.prepareForSavingAndUpdatingLayersBeforeExit(); 1058 } else { 1059 dialog.prepareForSavingAndUpdatingLayersBeforeDelete(); 1060 } 1061 if (!layersWithUnmodifiedChanges.isEmpty()) { 1062 dialog.getModel().populate(layersWithUnmodifiedChanges); 1063 dialog.setVisible(true); 1064 switch(dialog.getUserAction()) { 1065 case PROCEED: return true; 1066 case CANCEL: 1067 default: return false; 1068 } 1069 } 1070 1071 return true; 1072 } 1073 1074 /** 1075 * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). 1076 * If there are some unsaved data layers, asks first for user confirmation. 1077 * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code. 1078 * @param exitCode The return code 1079 * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation. 1080 * @since 3378 1081 */ 1082 public static boolean exitJosm(boolean exit, int exitCode) { 1083 if (Main.saveUnsavedModifications()) { 1084 worker.shutdown(); 1085 ImageProvider.shutdown(false); 1086 JCSCacheManager.shutdown(); 1087 if (geometry != null) { 1088 geometry.remember("gui.geometry"); 1089 } 1090 if (map != null) { 1091 map.rememberToggleDialogWidth(); 1092 } 1093 pref.put("gui.maximized", (windowState & JFrame.MAXIMIZED_BOTH) != 0); 1094 // Remove all layers because somebody may rely on layerRemoved events (like AutosaveTask) 1095 if (Main.isDisplayingMapView()) { 1096 Collection<Layer> layers = new ArrayList<>(Main.map.mapView.getAllLayers()); 1097 for (Layer l: layers) { 1098 Main.main.removeLayer(l); 1099 } 1100 } 1101 try { 1102 pref.saveDefaults(); 1103 } catch (IOException ex) { 1104 Main.warn(tr("Failed to save default preferences.")); 1105 } 1106 worker.shutdownNow(); 1107 ImageProvider.shutdown(true); 1108 1109 if (exit) { 1110 System.exit(exitCode); 1111 } 1112 return true; 1113 } 1114 return false; 1115 } 1116 1117 /** 1118 * The type of a command line parameter, to be used in switch statements. 1119 * @see #paramType 1120 */ 1121 enum DownloadParamType { 1122 httpUrl { 1123 @Override 1124 void download(String s, Collection<File> fileList) { 1125 new OpenLocationAction().openUrl(false, s); 1126 } 1127 1128 @Override 1129 void downloadGps(String s) { 1130 final Bounds b = OsmUrlToBounds.parse(s); 1131 if (b == null) { 1132 JOptionPane.showMessageDialog( 1133 Main.parent, 1134 tr("Ignoring malformed URL: \"{0}\"", s), 1135 tr("Warning"), 1136 JOptionPane.WARNING_MESSAGE 1137 ); 1138 return; 1139 } 1140 downloadFromParamBounds(true, b); 1141 } 1142 }, fileUrl { 1143 @Override 1144 void download(String s, Collection<File> fileList) { 1145 File f = null; 1146 try { 1147 f = new File(new URI(s)); 1148 } catch (URISyntaxException e) { 1149 JOptionPane.showMessageDialog( 1150 Main.parent, 1151 tr("Ignoring malformed file URL: \"{0}\"", s), 1152 tr("Warning"), 1153 JOptionPane.WARNING_MESSAGE 1154 ); 1155 } 1156 if (f != null) { 1157 fileList.add(f); 1158 } 1159 } 1160 }, bounds { 1161 1162 /** 1163 * Download area specified on the command line as bounds string. 1164 * @param rawGps Flag to download raw GPS tracks 1165 * @param s The bounds parameter 1166 */ 1167 private void downloadFromParamBounds(final boolean rawGps, String s) { 1168 final StringTokenizer st = new StringTokenizer(s, ","); 1169 if (st.countTokens() == 4) { 1170 Bounds b = new Bounds( 1171 new LatLon(Double.parseDouble(st.nextToken()), Double.parseDouble(st.nextToken())), 1172 new LatLon(Double.parseDouble(st.nextToken()), Double.parseDouble(st.nextToken())) 1173 ); 1174 Main.downloadFromParamBounds(rawGps, b); 1175 } 1176 } 1177 1178 @Override 1179 void download(String param, Collection<File> fileList) { 1180 downloadFromParamBounds(false, param); 1181 } 1182 1183 @Override 1184 void downloadGps(String param) { 1185 downloadFromParamBounds(true, param); 1186 } 1187 }, fileName { 1188 @Override 1189 void download(String s, Collection<File> fileList) { 1190 fileList.add(new File(s)); 1191 } 1192 }; 1193 1194 /** 1195 * Performs the download 1196 * @param param represents the object to be downloaded 1197 * @param fileList files which shall be opened, should be added to this collection 1198 */ 1199 abstract void download(String param, Collection<File> fileList); 1200 1201 /** 1202 * Performs the GPS download 1203 * @param param represents the object to be downloaded 1204 */ 1205 void downloadGps(String param) { 1206 JOptionPane.showMessageDialog( 1207 Main.parent, 1208 tr("Parameter \"downloadgps\" does not accept file names or file URLs"), 1209 tr("Warning"), 1210 JOptionPane.WARNING_MESSAGE 1211 ); 1212 } 1213 1214 /** 1215 * Guess the type of a parameter string specified on the command line with --download= or --downloadgps. 1216 * 1217 * @param s A parameter string 1218 * @return The guessed parameter type 1219 */ 1220 static DownloadParamType paramType(String s) { 1221 if (s.startsWith("http:") || s.startsWith("https:")) return DownloadParamType.httpUrl; 1222 if (s.startsWith("file:")) return DownloadParamType.fileUrl; 1223 String coorPattern = "\\s*[+-]?[0-9]+(\\.[0-9]+)?\\s*"; 1224 if (s.matches(coorPattern + "(," + coorPattern + "){3}")) return DownloadParamType.bounds; 1225 // everything else must be a file name 1226 return DownloadParamType.fileName; 1227 } 1228 } 1229 1230 /** 1231 * Download area specified as Bounds value. 1232 * @param rawGps Flag to download raw GPS tracks 1233 * @param b The bounds value 1234 */ 1235 private static void downloadFromParamBounds(final boolean rawGps, Bounds b) { 1236 DownloadTask task = rawGps ? new DownloadGpsTask() : new DownloadOsmTask(); 1237 // asynchronously launch the download task ... 1238 Future<?> future = task.download(true, b, null); 1239 // ... and the continuation when the download is finished (this will wait for the download to finish) 1240 Main.worker.execute(new PostDownloadHandler(task, future)); 1241 } 1242 1243 /** 1244 * Identifies the current operating system family and initializes the platform hook accordingly. 1245 * @since 1849 1246 */ 1247 public static void determinePlatformHook() { 1248 String os = System.getProperty("os.name"); 1249 if (os == null) { 1250 warn("Your operating system has no name, so I'm guessing its some kind of *nix."); 1251 platform = new PlatformHookUnixoid(); 1252 } else if (os.toLowerCase(Locale.ENGLISH).startsWith("windows")) { 1253 platform = new PlatformHookWindows(); 1254 } else if ("Linux".equals(os) || "Solaris".equals(os) || 1255 "SunOS".equals(os) || "AIX".equals(os) || 1256 "FreeBSD".equals(os) || "NetBSD".equals(os) || "OpenBSD".equals(os)) { 1257 platform = new PlatformHookUnixoid(); 1258 } else if (os.toLowerCase(Locale.ENGLISH).startsWith("mac os x")) { 1259 platform = new PlatformHookOsx(); 1260 } else { 1261 warn("I don't know your operating system '"+os+"', so I'm guessing its some kind of *nix."); 1262 platform = new PlatformHookUnixoid(); 1263 } 1264 } 1265 1266 private static class WindowPositionSizeListener extends WindowAdapter implements 1267 ComponentListener { 1268 @Override 1269 public void windowStateChanged(WindowEvent e) { 1270 Main.windowState = e.getNewState(); 1271 } 1272 1273 @Override 1274 public void componentHidden(ComponentEvent e) { 1275 } 1276 1277 @Override 1278 public void componentMoved(ComponentEvent e) { 1279 handleComponentEvent(e); 1280 } 1281 1282 @Override 1283 public void componentResized(ComponentEvent e) { 1284 handleComponentEvent(e); 1285 } 1286 1287 @Override 1288 public void componentShown(ComponentEvent e) { 1289 } 1290 1291 private static void handleComponentEvent(ComponentEvent e) { 1292 Component c = e.getComponent(); 1293 if (c instanceof JFrame && c.isVisible()) { 1294 if (Main.windowState == JFrame.NORMAL) { 1295 Main.geometry = new WindowGeometry((JFrame) c); 1296 } else { 1297 Main.geometry.fixScreen((JFrame) c); 1298 } 1299 } 1300 } 1301 } 1302 1303 protected static void addListener() { 1304 parent.addComponentListener(new WindowPositionSizeListener()); 1305 ((JFrame) parent).addWindowStateListener(new WindowPositionSizeListener()); 1306 } 1307 1308 /** 1309 * Determines if JOSM currently runs with Java 8 or later. 1310 * @return {@code true} if the current JVM is at least Java 8, {@code false} otherwise 1311 * @since 7894 1312 */ 1313 public static boolean isJava8orLater() { 1314 String version = System.getProperty("java.version"); 1315 return version != null && !version.matches("^(1\\.)?[7].*"); 1316 } 1317 1318 /** 1319 * Checks that JOSM is at least running with Java 7. 1320 * @since 7001 1321 */ 1322 public static void checkJavaVersion() { 1323 String version = System.getProperty("java.version"); 1324 if (version != null) { 1325 if (version.matches("^(1\\.)?[789].*")) 1326 return; 1327 if (version.matches("^(1\\.)?[56].*")) { 1328 JMultilineLabel ho = new JMultilineLabel("<html>"+ 1329 tr("<h2>JOSM requires Java version {0}.</h2>"+ 1330 "Detected Java version: {1}.<br>"+ 1331 "You can <ul><li>update your Java (JRE) or</li>"+ 1332 "<li>use an earlier (Java {2} compatible) version of JOSM.</li></ul>"+ 1333 "More Info:", "7", version, "6")+"</html>"); 1334 JTextArea link = new JTextArea(HelpUtil.getWikiBaseHelpUrl()+"/Help/SystemRequirements"); 1335 link.setEditable(false); 1336 link.setBackground(panel.getBackground()); 1337 JPanel panel = new JPanel(new GridBagLayout()); 1338 GridBagConstraints gbc = new GridBagConstraints(); 1339 gbc.gridwidth = GridBagConstraints.REMAINDER; 1340 gbc.anchor = GridBagConstraints.WEST; 1341 gbc.weightx = 1.0; 1342 panel.add(ho, gbc); 1343 panel.add(link, gbc); 1344 final String EXIT = tr("Exit JOSM"); 1345 final String CONTINUE = tr("Continue, try anyway"); 1346 int ret = JOptionPane.showOptionDialog(null, panel, tr("Error"), JOptionPane.YES_NO_OPTION, 1347 JOptionPane.ERROR_MESSAGE, null, new String[] {EXIT, CONTINUE}, EXIT); 1348 if (ret == 0) { 1349 System.exit(0); 1350 } 1351 return; 1352 } 1353 } 1354 error("Could not recognize Java Version: "+version); 1355 } 1356 1357 /* ----------------------------------------------------------------------------------------- */ 1358 /* projection handling - Main is a registry for a single, global projection instance */ 1359 /* */ 1360 /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */ 1361 /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class. */ 1362 /* ----------------------------------------------------------------------------------------- */ 1363 /** 1364 * The projection method used. 1365 * use {@link #getProjection()} and {@link #setProjection(Projection)} for access. 1366 * Use {@link #setProjection(Projection)} in order to trigger a projection change event. 1367 */ 1368 private static volatile Projection proj; 1369 1370 /** 1371 * Replies the current projection. 1372 * 1373 * @return the currently active projection 1374 */ 1375 public static Projection getProjection() { 1376 return proj; 1377 } 1378 1379 /** 1380 * Sets the current projection 1381 * 1382 * @param p the projection 1383 */ 1384 public static void setProjection(Projection p) { 1385 CheckParameterUtil.ensureParameterNotNull(p); 1386 Projection oldValue = proj; 1387 Bounds b = isDisplayingMapView() ? map.mapView.getRealBounds() : null; 1388 proj = p; 1389 fireProjectionChanged(oldValue, proj, b); 1390 } 1391 1392 /* 1393 * Keep WeakReferences to the listeners. This relieves clients from the burden of 1394 * explicitly removing the listeners and allows us to transparently register every 1395 * created dataset as projection change listener. 1396 */ 1397 private static final List<WeakReference<ProjectionChangeListener>> listeners = new ArrayList<>(); 1398 1399 private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) { 1400 if (newValue == null ^ oldValue == null 1401 || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) { 1402 1403 synchronized (Main.class) { 1404 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator(); 1405 while (it.hasNext()) { 1406 WeakReference<ProjectionChangeListener> wr = it.next(); 1407 ProjectionChangeListener listener = wr.get(); 1408 if (listener == null) { 1409 it.remove(); 1410 continue; 1411 } 1412 listener.projectionChanged(oldValue, newValue); 1413 } 1414 } 1415 if (newValue != null && oldBounds != null) { 1416 Main.map.mapView.zoomTo(oldBounds); 1417 } 1418 /* TODO - remove layers with fixed projection */ 1419 } 1420 } 1421 1422 /** 1423 * Register a projection change listener. 1424 * 1425 * @param listener the listener. Ignored if <code>null</code>. 1426 */ 1427 public static void addProjectionChangeListener(ProjectionChangeListener listener) { 1428 if (listener == null) return; 1429 synchronized (Main.class) { 1430 for (WeakReference<ProjectionChangeListener> wr : listeners) { 1431 // already registered ? => abort 1432 if (wr.get() == listener) return; 1433 } 1434 listeners.add(new WeakReference<>(listener)); 1435 } 1436 } 1437 1438 /** 1439 * Removes a projection change listener. 1440 * 1441 * @param listener the listener. Ignored if <code>null</code>. 1442 */ 1443 public static void removeProjectionChangeListener(ProjectionChangeListener listener) { 1444 if (listener == null) return; 1445 synchronized (Main.class) { 1446 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator(); 1447 while (it.hasNext()) { 1448 WeakReference<ProjectionChangeListener> wr = it.next(); 1449 // remove the listener - and any other listener which got garbage 1450 // collected in the meantime 1451 if (wr.get() == null || wr.get() == listener) { 1452 it.remove(); 1453 } 1454 } 1455 } 1456 } 1457 1458 /** 1459 * Listener for window switch events. 1460 * 1461 * These are events, when the user activates a window of another application 1462 * or comes back to JOSM. Window switches from one JOSM window to another 1463 * are not reported. 1464 */ 1465 public interface WindowSwitchListener { 1466 /** 1467 * Called when the user activates a window of another application. 1468 */ 1469 void toOtherApplication(); 1470 1471 /** 1472 * Called when the user comes from a window of another application back to JOSM. 1473 */ 1474 void fromOtherApplication(); 1475 } 1476 1477 private static final List<WeakReference<WindowSwitchListener>> windowSwitchListeners = new ArrayList<>(); 1478 1479 /** 1480 * Register a window switch listener. 1481 * 1482 * @param listener the listener. Ignored if <code>null</code>. 1483 */ 1484 public static void addWindowSwitchListener(WindowSwitchListener listener) { 1485 if (listener == null) return; 1486 synchronized (Main.class) { 1487 for (WeakReference<WindowSwitchListener> wr : windowSwitchListeners) { 1488 // already registered ? => abort 1489 if (wr.get() == listener) return; 1490 } 1491 boolean wasEmpty = windowSwitchListeners.isEmpty(); 1492 windowSwitchListeners.add(new WeakReference<>(listener)); 1493 if (wasEmpty) { 1494 // The following call will have no effect, when there is no window 1495 // at the time. Therefore, MasterWindowListener.setup() will also be 1496 // called, as soon as the main window is shown. 1497 MasterWindowListener.setup(); 1498 } 1499 } 1500 } 1501 1502 /** 1503 * Removes a window switch listener. 1504 * 1505 * @param listener the listener. Ignored if <code>null</code>. 1506 */ 1507 public static void removeWindowSwitchListener(WindowSwitchListener listener) { 1508 if (listener == null) return; 1509 synchronized (Main.class) { 1510 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1511 while (it.hasNext()) { 1512 WeakReference<WindowSwitchListener> wr = it.next(); 1513 // remove the listener - and any other listener which got garbage 1514 // collected in the meantime 1515 if (wr.get() == null || wr.get() == listener) { 1516 it.remove(); 1517 } 1518 } 1519 if (windowSwitchListeners.isEmpty()) { 1520 MasterWindowListener.teardown(); 1521 } 1522 } 1523 } 1524 1525 /** 1526 * WindowListener, that is registered on all Windows of the application. 1527 * 1528 * Its purpose is to notify WindowSwitchListeners, that the user switches to 1529 * another application, e.g. a browser, or back to JOSM. 1530 * 1531 * When changing from JOSM to another application and back (e.g. two times 1532 * alt+tab), the active Window within JOSM may be different. 1533 * Therefore, we need to register listeners to <strong>all</strong> (visible) 1534 * Windows in JOSM, and it does not suffice to monitor the one that was 1535 * deactivated last. 1536 * 1537 * This class is only "active" on demand, i.e. when there is at least one 1538 * WindowSwitchListener registered. 1539 */ 1540 protected static class MasterWindowListener extends WindowAdapter { 1541 1542 private static MasterWindowListener INSTANCE; 1543 1544 public static synchronized MasterWindowListener getInstance() { 1545 if (INSTANCE == null) { 1546 INSTANCE = new MasterWindowListener(); 1547 } 1548 return INSTANCE; 1549 } 1550 1551 /** 1552 * Register listeners to all non-hidden windows. 1553 * 1554 * Windows that are created later, will be cared for in {@link #windowDeactivated(WindowEvent)}. 1555 */ 1556 public static void setup() { 1557 if (!windowSwitchListeners.isEmpty()) { 1558 for (Window w : Window.getWindows()) { 1559 if (w.isShowing()) { 1560 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) { 1561 w.addWindowListener(getInstance()); 1562 } 1563 } 1564 } 1565 } 1566 } 1567 1568 /** 1569 * Unregister all listeners. 1570 */ 1571 public static void teardown() { 1572 for (Window w : Window.getWindows()) { 1573 w.removeWindowListener(getInstance()); 1574 } 1575 } 1576 1577 @Override 1578 public void windowActivated(WindowEvent e) { 1579 if (e.getOppositeWindow() == null) { // we come from a window of a different application 1580 // fire WindowSwitchListeners 1581 synchronized (Main.class) { 1582 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1583 while (it.hasNext()) { 1584 WeakReference<WindowSwitchListener> wr = it.next(); 1585 WindowSwitchListener listener = wr.get(); 1586 if (listener == null) { 1587 it.remove(); 1588 continue; 1589 } 1590 listener.fromOtherApplication(); 1591 } 1592 } 1593 } 1594 } 1595 1596 @Override 1597 public void windowDeactivated(WindowEvent e) { 1598 // set up windows that have been created in the meantime 1599 for (Window w : Window.getWindows()) { 1600 if (!w.isShowing()) { 1601 w.removeWindowListener(getInstance()); 1602 } else { 1603 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) { 1604 w.addWindowListener(getInstance()); 1605 } 1606 } 1607 } 1608 if (e.getOppositeWindow() == null) { // we go to a window of a different application 1609 // fire WindowSwitchListeners 1610 synchronized (Main.class) { 1611 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1612 while (it.hasNext()) { 1613 WeakReference<WindowSwitchListener> wr = it.next(); 1614 WindowSwitchListener listener = wr.get(); 1615 if (listener == null) { 1616 it.remove(); 1617 continue; 1618 } 1619 listener.toOtherApplication(); 1620 } 1621 } 1622 } 1623 } 1624 } 1625 1626 /** 1627 * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes 1628 * @param listener The MapFrameListener 1629 * @param fireWhenMapViewPresent If true, will fire an initial mapFrameInitialized event 1630 * when the MapFrame is present. Otherwise will only fire when the MapFrame is created 1631 * or destroyed. 1632 * @return {@code true} if the listeners collection changed as a result of the call 1633 */ 1634 public static boolean addMapFrameListener(MapFrameListener listener, boolean fireWhenMapViewPresent) { 1635 boolean changed = listener != null && mapFrameListeners.add(listener); 1636 if (fireWhenMapViewPresent && changed && map != null) { 1637 listener.mapFrameInitialized(null, map); 1638 } 1639 return changed; 1640 } 1641 1642 /** 1643 * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes 1644 * @param listener The MapFrameListener 1645 * @return {@code true} if the listeners collection changed as a result of the call 1646 * @since 5957 1647 */ 1648 public static boolean addMapFrameListener(MapFrameListener listener) { 1649 return addMapFrameListener(listener, false); 1650 } 1651 1652 /** 1653 * Unregisters the given {@code MapFrameListener} from MapFrame changes 1654 * @param listener The MapFrameListener 1655 * @return {@code true} if the listeners collection changed as a result of the call 1656 * @since 5957 1657 */ 1658 public static boolean removeMapFrameListener(MapFrameListener listener) { 1659 return listener != null && mapFrameListeners.remove(listener); 1660 } 1661 1662 /** 1663 * Adds a new network error that occur to give a hint about broken Internet connection. 1664 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 1665 * 1666 * @param url The accessed URL that caused the error 1667 * @param t The network error 1668 * @return The previous error associated to the given resource, if any. Can be {@code null} 1669 * @since 6642 1670 */ 1671 public static Throwable addNetworkError(URL url, Throwable t) { 1672 if (url != null && t != null) { 1673 Throwable old = addNetworkError(url.toExternalForm(), t); 1674 if (old != null) { 1675 Main.warn("Already here "+old); 1676 } 1677 return old; 1678 } 1679 return null; 1680 } 1681 1682 /** 1683 * Adds a new network error that occur to give a hint about broken Internet connection. 1684 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 1685 * 1686 * @param url The accessed URL that caused the error 1687 * @param t The network error 1688 * @return The previous error associated to the given resource, if any. Can be {@code null} 1689 * @since 6642 1690 */ 1691 public static Throwable addNetworkError(String url, Throwable t) { 1692 if (url != null && t != null) { 1693 return NETWORK_ERRORS.put(url, t); 1694 } 1695 return null; 1696 } 1697 1698 /** 1699 * Returns the network errors that occured until now. 1700 * @return the network errors that occured until now, indexed by URL 1701 * @since 6639 1702 */ 1703 public static Map<String, Throwable> getNetworkErrors() { 1704 return new HashMap<>(NETWORK_ERRORS); 1705 } 1706 1707 /** 1708 * Returns the command-line arguments used to run the application. 1709 * @return the command-line arguments used to run the application 1710 * @since 8356 1711 */ 1712 public static List<String> getCommandLineArgs() { 1713 return Collections.unmodifiableList(COMMAND_LINE_ARGS); 1714 } 1715 1716 /** 1717 * Returns the JOSM website URL. 1718 * @return the josm website URL 1719 * @since 6897 1720 */ 1721 public static String getJOSMWebsite() { 1722 if (Main.pref != null) 1723 return Main.pref.get("josm.url", JOSM_WEBSITE); 1724 return JOSM_WEBSITE; 1725 } 1726 1727 /** 1728 * Returns the JOSM XML URL. 1729 * @return the josm XML URL 1730 * @since 6897 1731 */ 1732 public static String getXMLBase() { 1733 // Always return HTTP (issues reported with HTTPS) 1734 return "http://josm.openstreetmap.de"; 1735 } 1736 1737 /** 1738 * Returns the OSM website URL. 1739 * @return the OSM website URL 1740 * @since 6897 1741 */ 1742 public static String getOSMWebsite() { 1743 if (Main.pref != null) 1744 return Main.pref.get("osm.url", OSM_WEBSITE); 1745 return OSM_WEBSITE; 1746 } 1747 1748 /** 1749 * Replies the base URL for browsing information about a primitive. 1750 * @return the base URL, i.e. https://www.openstreetmap.org 1751 * @since 7678 1752 */ 1753 public static String getBaseBrowseUrl() { 1754 if (Main.pref != null) 1755 return Main.pref.get("osm-browse.url", getOSMWebsite()); 1756 return getOSMWebsite(); 1757 } 1758 1759 /** 1760 * Replies the base URL for browsing information about a user. 1761 * @return the base URL, i.e. https://www.openstreetmap.org/user 1762 * @since 7678 1763 */ 1764 public static String getBaseUserUrl() { 1765 if (Main.pref != null) 1766 return Main.pref.get("osm-user.url", getOSMWebsite() + "/user"); 1767 return getOSMWebsite() + "/user"; 1768 } 1769 1770 /** 1771 * Determines if we are currently running on OSX. 1772 * @return {@code true} if we are currently running on OSX 1773 * @since 6957 1774 */ 1775 public static boolean isPlatformOsx() { 1776 return Main.platform instanceof PlatformHookOsx; 1777 } 1778 1779 /** 1780 * Determines if we are currently running on Windows. 1781 * @return {@code true} if we are currently running on Windows 1782 * @since 7335 1783 */ 1784 public static boolean isPlatformWindows() { 1785 return Main.platform instanceof PlatformHookWindows; 1786 } 1787 1788 /** 1789 * Determines if the given online resource is currently offline. 1790 * @param r the online resource 1791 * @return {@code true} if {@code r} is offline and should not be accessed 1792 * @since 7434 1793 */ 1794 public static boolean isOffline(OnlineResource r) { 1795 return OFFLINE_RESOURCES.contains(r) || OFFLINE_RESOURCES.contains(OnlineResource.ALL); 1796 } 1797 1798 /** 1799 * Sets the given online resource to offline state. 1800 * @param r the online resource 1801 * @return {@code true} if {@code r} was not already offline 1802 * @since 7434 1803 */ 1804 public static boolean setOffline(OnlineResource r) { 1805 return OFFLINE_RESOURCES.add(r); 1806 } 1807 1808 /** 1809 * Sets the given online resource to online state. 1810 * @param r the online resource 1811 * @return {@code true} if {@code r} was offline 1812 * @since 8506 1813 */ 1814 public static boolean setOnline(OnlineResource r) { 1815 return OFFLINE_RESOURCES.remove(r); 1816 } 1817 1818 /** 1819 * Replies the set of online resources currently offline. 1820 * @return the set of online resources currently offline 1821 * @since 7434 1822 */ 1823 public static Set<OnlineResource> getOfflineResources() { 1824 return new HashSet<>(OFFLINE_RESOURCES); 1825 } 1826}