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