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