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.GraphicsEnvironment; 009import java.awt.Window; 010import java.awt.event.KeyEvent; 011import java.awt.event.WindowAdapter; 012import java.awt.event.WindowEvent; 013import java.io.File; 014import java.io.IOException; 015import java.lang.ref.WeakReference; 016import java.net.URI; 017import java.net.URISyntaxException; 018import java.net.URL; 019import java.text.MessageFormat; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.EnumSet; 025import java.util.HashMap; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Locale; 029import java.util.Map; 030import java.util.Objects; 031import java.util.Set; 032import java.util.StringTokenizer; 033import java.util.concurrent.Callable; 034import java.util.concurrent.ExecutionException; 035import java.util.concurrent.ExecutorService; 036import java.util.concurrent.Executors; 037import java.util.concurrent.Future; 038 039import javax.swing.Action; 040import javax.swing.InputMap; 041import javax.swing.JComponent; 042import javax.swing.JOptionPane; 043import javax.swing.JPanel; 044import javax.swing.KeyStroke; 045import javax.swing.LookAndFeel; 046import javax.swing.UIManager; 047import javax.swing.UnsupportedLookAndFeelException; 048 049import org.openstreetmap.gui.jmapviewer.FeatureAdapter; 050import org.openstreetmap.josm.actions.JosmAction; 051import org.openstreetmap.josm.actions.OpenFileAction; 052import org.openstreetmap.josm.actions.OpenLocationAction; 053import org.openstreetmap.josm.actions.downloadtasks.DownloadGpsTask; 054import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask; 055import org.openstreetmap.josm.actions.downloadtasks.DownloadTask; 056import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler; 057import org.openstreetmap.josm.actions.mapmode.DrawAction; 058import org.openstreetmap.josm.actions.search.SearchAction; 059import org.openstreetmap.josm.data.Bounds; 060import org.openstreetmap.josm.data.Preferences; 061import org.openstreetmap.josm.data.ProjectionBounds; 062import org.openstreetmap.josm.data.UndoRedoHandler; 063import org.openstreetmap.josm.data.ViewportData; 064import org.openstreetmap.josm.data.cache.JCSCacheManager; 065import org.openstreetmap.josm.data.coor.CoordinateFormat; 066import org.openstreetmap.josm.data.coor.LatLon; 067import org.openstreetmap.josm.data.osm.DataSet; 068import org.openstreetmap.josm.data.osm.OsmPrimitive; 069import org.openstreetmap.josm.data.projection.Projection; 070import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 071import org.openstreetmap.josm.data.validation.OsmValidator; 072import org.openstreetmap.josm.gui.MainFrame; 073import org.openstreetmap.josm.gui.MainMenu; 074import org.openstreetmap.josm.gui.MainPanel; 075import org.openstreetmap.josm.gui.MapFrame; 076import org.openstreetmap.josm.gui.MapFrameListener; 077import org.openstreetmap.josm.gui.ProgramArguments; 078import org.openstreetmap.josm.gui.ProgramArguments.Option; 079import org.openstreetmap.josm.gui.io.SaveLayersDialog; 080import org.openstreetmap.josm.gui.layer.Layer; 081import org.openstreetmap.josm.gui.layer.MainLayerManager; 082import org.openstreetmap.josm.gui.layer.OsmDataLayer.CommandQueueListener; 083import org.openstreetmap.josm.gui.layer.TMSLayer; 084import org.openstreetmap.josm.gui.preferences.ToolbarPreferences; 085import org.openstreetmap.josm.gui.preferences.imagery.ImageryPreference; 086import org.openstreetmap.josm.gui.preferences.map.MapPaintPreference; 087import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 088import org.openstreetmap.josm.gui.progress.PleaseWaitProgressMonitor; 089import org.openstreetmap.josm.gui.progress.ProgressMonitorExecutor; 090import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets; 091import org.openstreetmap.josm.gui.util.GuiHelper; 092import org.openstreetmap.josm.gui.util.RedirectInputMap; 093import org.openstreetmap.josm.io.FileWatcher; 094import org.openstreetmap.josm.io.OnlineResource; 095import org.openstreetmap.josm.io.OsmApi; 096import org.openstreetmap.josm.io.OsmApiInitializationException; 097import org.openstreetmap.josm.io.OsmTransferCanceledException; 098import org.openstreetmap.josm.plugins.PluginHandler; 099import org.openstreetmap.josm.tools.CheckParameterUtil; 100import org.openstreetmap.josm.tools.I18n; 101import org.openstreetmap.josm.tools.ImageProvider; 102import org.openstreetmap.josm.tools.Logging; 103import org.openstreetmap.josm.tools.OpenBrowser; 104import org.openstreetmap.josm.tools.OsmUrlToBounds; 105import org.openstreetmap.josm.tools.OverpassTurboQueryWizard; 106import org.openstreetmap.josm.tools.PlatformHook; 107import org.openstreetmap.josm.tools.PlatformHookOsx; 108import org.openstreetmap.josm.tools.PlatformHookUnixoid; 109import org.openstreetmap.josm.tools.PlatformHookWindows; 110import org.openstreetmap.josm.tools.Shortcut; 111import org.openstreetmap.josm.tools.Utils; 112 113/** 114 * Abstract class holding various static global variables and methods used in large parts of JOSM application. 115 * @since 98 116 */ 117public abstract class Main { 118 119 /** 120 * The JOSM website URL. 121 * @since 6897 (was public from 6143 to 6896) 122 */ 123 private static final String JOSM_WEBSITE = "https://josm.openstreetmap.de"; 124 125 /** 126 * The OSM website URL. 127 * @since 6897 (was public from 6453 to 6896) 128 */ 129 private static final String OSM_WEBSITE = "https://www.openstreetmap.org"; 130 131 /** 132 * Replies true if JOSM currently displays a map view. False, if it doesn't, i.e. if 133 * it only shows the MOTD panel. 134 * <p> 135 * You do not need this when accessing the layer manager. The layer manager will be empty if no map view is shown. 136 * 137 * @return <code>true</code> if JOSM currently displays a map view 138 */ 139 public static boolean isDisplayingMapView() { 140 return map != null && map.mapView != null; 141 } 142 143 /** 144 * Global parent component for all dialogs and message boxes 145 */ 146 public static Component parent; 147 148 /** 149 * Global application. 150 */ 151 public static volatile Main main; 152 153 /** 154 * Command-line arguments used to run the application. 155 */ 156 protected static final List<String> COMMAND_LINE_ARGS = new ArrayList<>(); 157 158 /** 159 * The worker thread slave. This is for executing all long and intensive 160 * calculations. The executed runnables are guaranteed to be executed separately 161 * and sequential. 162 */ 163 public static final ExecutorService worker = new ProgressMonitorExecutor("main-worker-%d", Thread.NORM_PRIORITY); 164 165 /** 166 * Global application preferences 167 */ 168 public static final Preferences pref = new Preferences(); 169 170 /** 171 * The MapFrame. 172 * <p> 173 * There should be no need to access this to access any map data. Use {@link #layerManager} instead. 174 * 175 * @see MainPanel 176 */ 177 public static MapFrame map; 178 179 /** 180 * Provides access to the layers displayed in the main view. 181 * @since 10271 182 */ 183 private static final MainLayerManager layerManager = new MainLayerManager(); 184 185 /** 186 * The toolbar preference control to register new actions. 187 */ 188 public static volatile ToolbarPreferences toolbar; 189 190 /** 191 * The commands undo/redo handler. 192 */ 193 public final UndoRedoHandler undoRedo = new UndoRedoHandler(); 194 195 /** 196 * The progress monitor being currently displayed. 197 */ 198 public static PleaseWaitProgressMonitor currentProgressMonitor; 199 200 /** 201 * The main menu bar at top of screen. 202 */ 203 public MainMenu menu; 204 205 /** 206 * The file watcher service. 207 */ 208 public static final FileWatcher fileWatcher = new FileWatcher(); 209 210 protected static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>(); 211 212 private static final Set<OnlineResource> OFFLINE_RESOURCES = EnumSet.noneOf(OnlineResource.class); 213 214 /** 215 * Logging level (5 = trace, 4 = debug, 3 = info, 2 = warn, 1 = error, 0 = none). 216 * @since 6248 217 * @deprecated Use {@link Logging} class. 218 */ 219 @Deprecated 220 public static int logLevel = 3; 221 222 /** 223 * The real main panel. This field may be removed any time and made private to {@link MainFrame} 224 * @see #panel 225 */ 226 protected static final MainPanel mainPanel = new MainPanel(getLayerManager()); 227 228 /** 229 * Replies the first lines of last 5 error and warning messages, used for bug reports 230 * @return the first lines of last 5 error and warning messages 231 * @since 7420 232 */ 233 public static final Collection<String> getLastErrorAndWarnings() { 234 return Logging.getLastErrorAndWarnings(); 235 } 236 237 /** 238 * Clears the list of last error and warning messages. 239 * @since 8959 240 */ 241 public static void clearLastErrorAndWarnings() { 242 Logging.clearLastErrorAndWarnings(); 243 } 244 245 /** 246 * Prints an error message if logging is on. 247 * @param msg The message to print. 248 * @since 6248 249 */ 250 public static void error(String msg) { 251 Logging.error(msg); 252 } 253 254 /** 255 * Prints a warning message if logging is on. 256 * @param msg The message to print. 257 */ 258 public static void warn(String msg) { 259 Logging.warn(msg); 260 } 261 262 /** 263 * Prints an informational message if logging is on. 264 * @param msg The message to print. 265 */ 266 public static void info(String msg) { 267 Logging.info(msg); 268 } 269 270 /** 271 * Prints a debug message if logging is on. 272 * @param msg The message to print. 273 */ 274 public static void debug(String msg) { 275 Logging.debug(msg); 276 } 277 278 /** 279 * Prints a trace message if logging is on. 280 * @param msg The message to print. 281 */ 282 public static void trace(String msg) { 283 Logging.trace(msg); 284 } 285 286 /** 287 * Determines if debug log level is enabled. 288 * Useful to avoid costly construction of debug messages when not enabled. 289 * @return {@code true} if log level is at least debug, {@code false} otherwise 290 * @since 6852 291 */ 292 public static boolean isDebugEnabled() { 293 return Logging.isLoggingEnabled(Logging.LEVEL_DEBUG); 294 } 295 296 /** 297 * Determines if trace log level is enabled. 298 * Useful to avoid costly construction of trace messages when not enabled. 299 * @return {@code true} if log level is at least trace, {@code false} otherwise 300 * @since 6852 301 */ 302 public static boolean isTraceEnabled() { 303 return Logging.isLoggingEnabled(Logging.LEVEL_TRACE); 304 } 305 306 /** 307 * Prints a formatted error message if logging is on. Calls {@link MessageFormat#format} 308 * function to format text. 309 * @param msg The formatted message to print. 310 * @param objects The objects to insert into format string. 311 * @since 6248 312 */ 313 public static void error(String msg, Object... objects) { 314 Logging.error(msg, objects); 315 } 316 317 /** 318 * Prints a formatted warning message if logging is on. Calls {@link MessageFormat#format} 319 * function to format text. 320 * @param msg The formatted message to print. 321 * @param objects The objects to insert into format string. 322 */ 323 public static void warn(String msg, Object... objects) { 324 Logging.warn(msg, objects); 325 } 326 327 /** 328 * Prints a formatted informational message if logging is on. Calls {@link MessageFormat#format} 329 * function to format text. 330 * @param msg The formatted message to print. 331 * @param objects The objects to insert into format string. 332 */ 333 public static void info(String msg, Object... objects) { 334 Logging.info(msg, objects); 335 } 336 337 /** 338 * Prints a formatted debug message if logging is on. Calls {@link MessageFormat#format} 339 * function to format text. 340 * @param msg The formatted message to print. 341 * @param objects The objects to insert into format string. 342 */ 343 public static void debug(String msg, Object... objects) { 344 Logging.debug(msg, objects); 345 } 346 347 /** 348 * Prints a formatted trace message if logging is on. Calls {@link MessageFormat#format} 349 * function to format text. 350 * @param msg The formatted message to print. 351 * @param objects The objects to insert into format string. 352 */ 353 public static void trace(String msg, Object... objects) { 354 Logging.trace(msg, objects); 355 } 356 357 /** 358 * Prints an error message for the given Throwable. 359 * @param t The throwable object causing the error 360 * @since 6248 361 */ 362 public static void error(Throwable t) { 363 Logging.logWithStackTrace(Logging.LEVEL_ERROR, t); 364 } 365 366 /** 367 * Prints a warning message for the given Throwable. 368 * @param t The throwable object causing the error 369 * @since 6248 370 */ 371 public static void warn(Throwable t) { 372 Logging.logWithStackTrace(Logging.LEVEL_WARN, t); 373 } 374 375 /** 376 * Prints a debug message for the given Throwable. Useful for exceptions usually ignored 377 * @param t The throwable object causing the error 378 * @since 10420 379 */ 380 public static void debug(Throwable t) { 381 Logging.log(Logging.LEVEL_DEBUG, t); 382 } 383 384 /** 385 * Prints a trace message for the given Throwable. Useful for exceptions usually ignored 386 * @param t The throwable object causing the error 387 * @since 10420 388 */ 389 public static void trace(Throwable t) { 390 Logging.log(Logging.LEVEL_TRACE, t); 391 } 392 393 /** 394 * Prints an error message for the given Throwable. 395 * @param t The throwable object causing the error 396 * @param stackTrace {@code true}, if the stacktrace should be displayed 397 * @since 6642 398 */ 399 public static void error(Throwable t, boolean stackTrace) { 400 if (stackTrace) { 401 Logging.log(Logging.LEVEL_ERROR, t); 402 } else { 403 Logging.logWithStackTrace(Logging.LEVEL_ERROR, t); 404 } 405 } 406 407 /** 408 * Prints an error message for the given Throwable. 409 * @param t The throwable object causing the error 410 * @param message additional error message 411 * @since 10420 412 */ 413 public static void error(Throwable t, String message) { 414 Logging.log(Logging.LEVEL_ERROR, message, t); 415 } 416 417 /** 418 * Prints a warning message for the given Throwable. 419 * @param t The throwable object causing the error 420 * @param stackTrace {@code true}, if the stacktrace should be displayed 421 * @since 6642 422 */ 423 public static void warn(Throwable t, boolean stackTrace) { 424 if (stackTrace) { 425 Logging.log(Logging.LEVEL_WARN, t); 426 } else { 427 Logging.logWithStackTrace(Logging.LEVEL_WARN, t); 428 } 429 } 430 431 /** 432 * Prints a warning message for the given Throwable. 433 * @param t The throwable object causing the error 434 * @param message additional error message 435 * @since 10420 436 */ 437 public static void warn(Throwable t, String message) { 438 Logging.log(Logging.LEVEL_WARN, message, t); 439 } 440 441 /** 442 * Returns a human-readable message of error, also usable for developers. 443 * @param t The error 444 * @return The human-readable error message 445 * @since 6642 446 */ 447 public static String getErrorMessage(Throwable t) { 448 if (t == null) { 449 return null; 450 } else { 451 return Logging.getErrorMessage(t); 452 } 453 } 454 455 /** 456 * Platform specific code goes in here. 457 * Plugins may replace it, however, some hooks will be called before any plugins have been loeaded. 458 * So if you need to hook into those early ones, split your class and send the one with the early hooks 459 * to the JOSM team for inclusion. 460 */ 461 public static volatile PlatformHook platform; 462 463 private static volatile InitStatusListener initListener; 464 465 public interface InitStatusListener { 466 467 Object updateStatus(String event); 468 469 void finish(Object status); 470 } 471 472 public static void setInitStatusListener(InitStatusListener listener) { 473 CheckParameterUtil.ensureParameterNotNull(listener); 474 initListener = listener; 475 } 476 477 /** 478 * Constructs new {@code Main} object. 479 * @see #initialize() 480 */ 481 public Main() { 482 main = this; 483 mainPanel.addMapFrameListener((o, n) -> redoUndoListener.commandChanged(0, 0)); 484 } 485 486 /** 487 * Initializes the main object. A lot of global variables are initialized here. 488 * @since 10340 489 */ 490 public void initialize() { 491 fileWatcher.start(); 492 493 new InitializationTask(tr("Executing platform startup hook"), platform::startupHook).call(); 494 495 new InitializationTask(tr("Building main menu"), this::initializeMainWindow).call(); 496 497 undoRedo.addCommandQueueListener(redoUndoListener); 498 499 // creating toolbar 500 contentPanePrivate.add(toolbar.control, BorderLayout.NORTH); 501 502 registerActionShortcut(menu.help, Shortcut.registerShortcut("system:help", tr("Help"), 503 KeyEvent.VK_F1, Shortcut.DIRECT)); 504 505 // contains several initialization tasks to be executed (in parallel) by a ExecutorService 506 List<Callable<Void>> tasks = new ArrayList<>(); 507 508 tasks.add(new InitializationTask(tr("Initializing OSM API"), () -> { 509 // We try to establish an API connection early, so that any API 510 // capabilities are already known to the editor instance. However 511 // if it goes wrong that's not critical at this stage. 512 try { 513 OsmApi.getOsmApi().initialize(null, true); 514 } catch (OsmTransferCanceledException | OsmApiInitializationException e) { 515 Main.warn(getErrorMessage(Utils.getRootCause(e))); 516 } 517 })); 518 519 tasks.add(new InitializationTask(tr("Initializing validator"), OsmValidator::initialize)); 520 521 tasks.add(new InitializationTask(tr("Initializing presets"), TaggingPresets::initialize)); 522 523 tasks.add(new InitializationTask(tr("Initializing map styles"), MapPaintPreference::initialize)); 524 525 tasks.add(new InitializationTask(tr("Loading imagery preferences"), ImageryPreference::initialize)); 526 527 try { 528 ExecutorService service = Executors.newFixedThreadPool( 529 Runtime.getRuntime().availableProcessors(), Utils.newThreadFactory("main-init-%d", Thread.NORM_PRIORITY)); 530 for (Future<Void> i : service.invokeAll(tasks)) { 531 i.get(); 532 } 533 // asynchronous initializations to be completed eventually 534 service.submit((Runnable) TMSLayer::getCache); 535 service.submit((Runnable) OsmValidator::initializeTests); 536 service.submit(OverpassTurboQueryWizard::getInstance); 537 service.shutdown(); 538 } catch (InterruptedException | ExecutionException ex) { 539 throw new RuntimeException(ex); 540 } 541 542 // hooks for the jmapviewer component 543 FeatureAdapter.registerBrowserAdapter(OpenBrowser::displayUrl); 544 FeatureAdapter.registerTranslationAdapter(I18n.getTranslationAdapter()); 545 FeatureAdapter.registerLoggingAdapter(name -> Logging.getLogger()); 546 547 new InitializationTask(tr("Updating user interface"), () -> { 548 toolbar.refreshToolbarControl(); 549 toolbar.control.updateUI(); 550 contentPanePrivate.updateUI(); 551 }).call(); 552 } 553 554 /** 555 * Called once at startup to initialize the main window content. 556 * Should set {@link #menu} 557 */ 558 protected void initializeMainWindow() { 559 // can be implementd by subclasses 560 } 561 562 private static class InitializationTask implements Callable<Void> { 563 564 private final String name; 565 private final Runnable task; 566 567 protected InitializationTask(String name, Runnable task) { 568 this.name = name; 569 this.task = task; 570 } 571 572 @Override 573 public Void call() { 574 Object status = null; 575 if (initListener != null) { 576 status = initListener.updateStatus(name); 577 } 578 task.run(); 579 if (initListener != null) { 580 initListener.finish(status); 581 } 582 return null; 583 } 584 } 585 586 /** 587 * Returns the main layer manager that is used by the map view. 588 * @return The layer manager. The value returned will never change. 589 * @since 10279 590 */ 591 public static MainLayerManager getLayerManager() { 592 return layerManager; 593 } 594 595 /** 596 * Add a new layer to the map. 597 * 598 * If no map exists, create one. 599 * 600 * @param layer the layer 601 * @param bounds the bounds of the layer (target zoom area); can be null, then 602 * the viewport isn't changed 603 */ 604 public final void addLayer(Layer layer, ProjectionBounds bounds) { 605 addLayer(layer, bounds == null ? null : new ViewportData(bounds)); 606 } 607 608 /** 609 * Add a new layer to the map. 610 * 611 * If no map exists, create one. 612 * 613 * @param layer the layer 614 * @param viewport the viewport to zoom to; can be null, then the viewport isn't changed 615 */ 616 public final void addLayer(Layer layer, ViewportData viewport) { 617 getLayerManager().addLayer(layer); 618 if (viewport != null && Main.map.mapView != null) { 619 // MapView may be null in headless mode here. 620 Main.map.mapView.scheduleZoomTo(viewport); 621 } 622 } 623 624 /** 625 * Replies the current selected primitives, from a end-user point of view. 626 * It is not always technically the same collection of primitives than {@link DataSet#getSelected()}. 627 * Indeed, if the user is currently in drawing mode, only the way currently being drawn is returned, 628 * see {@link DrawAction#getInProgressSelection()}. 629 * 630 * @return The current selected primitives, from a end-user point of view. Can be {@code null}. 631 * @since 6546 632 */ 633 public Collection<OsmPrimitive> getInProgressSelection() { 634 if (map != null && map.mapMode instanceof DrawAction) { 635 return ((DrawAction) map.mapMode).getInProgressSelection(); 636 } else { 637 DataSet ds = getLayerManager().getEditDataSet(); 638 if (ds == null) return null; 639 return ds.getSelected(); 640 } 641 } 642 643 protected static final JPanel contentPanePrivate = new JPanel(new BorderLayout()); 644 645 public static void redirectToMainContentPane(JComponent source) { 646 RedirectInputMap.redirect(source, contentPanePrivate); 647 } 648 649 public static void registerActionShortcut(JosmAction action) { 650 registerActionShortcut(action, action.getShortcut()); 651 } 652 653 public static void registerActionShortcut(Action action, Shortcut shortcut) { 654 KeyStroke keyStroke = shortcut.getKeyStroke(); 655 if (keyStroke == null) 656 return; 657 658 InputMap inputMap = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); 659 Object existing = inputMap.get(keyStroke); 660 if (existing != null && !existing.equals(action)) { 661 info(String.format("Keystroke %s is already assigned to %s, will be overridden by %s", keyStroke, existing, action)); 662 } 663 inputMap.put(keyStroke, action); 664 665 contentPanePrivate.getActionMap().put(action, action); 666 } 667 668 public static void unregisterShortcut(Shortcut shortcut) { 669 contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(shortcut.getKeyStroke()); 670 } 671 672 public static void unregisterActionShortcut(JosmAction action) { 673 unregisterActionShortcut(action, action.getShortcut()); 674 } 675 676 public static void unregisterActionShortcut(Action action, Shortcut shortcut) { 677 unregisterShortcut(shortcut); 678 contentPanePrivate.getActionMap().remove(action); 679 } 680 681 /** 682 * Replies the registered action for the given shortcut 683 * @param shortcut The shortcut to look for 684 * @return the registered action for the given shortcut 685 * @since 5696 686 */ 687 public static Action getRegisteredActionShortcut(Shortcut shortcut) { 688 KeyStroke keyStroke = shortcut.getKeyStroke(); 689 if (keyStroke == null) 690 return null; 691 Object action = contentPanePrivate.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).get(keyStroke); 692 if (action instanceof Action) 693 return (Action) action; 694 return null; 695 } 696 697 /////////////////////////////////////////////////////////////////////////// 698 // Implementation part 699 /////////////////////////////////////////////////////////////////////////// 700 701 /** 702 * Global panel. 703 */ 704 public static final JPanel panel = mainPanel; 705 706 private final CommandQueueListener redoUndoListener = (queueSize, redoSize) -> { 707 menu.undo.setEnabled(queueSize > 0); 708 menu.redo.setEnabled(redoSize > 0); 709 }; 710 711 /** 712 * Should be called before the main constructor to setup some parameter stuff 713 */ 714 public static void preConstructorInit() { 715 ProjectionPreference.setProjection(); 716 717 String defaultlaf = platform.getDefaultStyle(); 718 String laf = Main.pref.get("laf", defaultlaf); 719 try { 720 UIManager.setLookAndFeel(laf); 721 } catch (final NoClassDefFoundError | ClassNotFoundException e) { 722 // Try to find look and feel in plugin classloaders 723 Main.trace(e); 724 Class<?> klass = null; 725 for (ClassLoader cl : PluginHandler.getResourceClassLoaders()) { 726 try { 727 klass = cl.loadClass(laf); 728 break; 729 } catch (ClassNotFoundException ex) { 730 Main.trace(ex); 731 } 732 } 733 if (klass != null && LookAndFeel.class.isAssignableFrom(klass)) { 734 try { 735 UIManager.setLookAndFeel((LookAndFeel) klass.getConstructor().newInstance()); 736 } catch (ReflectiveOperationException ex) { 737 warn(ex, "Cannot set Look and Feel: " + laf + ": "+ex.getMessage()); 738 } catch (UnsupportedLookAndFeelException ex) { 739 info("Look and Feel not supported: " + laf); 740 Main.pref.put("laf", defaultlaf); 741 trace(ex); 742 } 743 } else { 744 info("Look and Feel not found: " + laf); 745 Main.pref.put("laf", defaultlaf); 746 } 747 } catch (UnsupportedLookAndFeelException e) { 748 info("Look and Feel not supported: " + laf); 749 Main.pref.put("laf", defaultlaf); 750 trace(e); 751 } catch (InstantiationException | IllegalAccessException e) { 752 error(e); 753 } 754 toolbar = new ToolbarPreferences(); 755 contentPanePrivate.updateUI(); 756 panel.updateUI(); 757 758 UIManager.put("OptionPane.okIcon", ImageProvider.get("ok")); 759 UIManager.put("OptionPane.yesIcon", UIManager.get("OptionPane.okIcon")); 760 UIManager.put("OptionPane.cancelIcon", ImageProvider.get("cancel")); 761 UIManager.put("OptionPane.noIcon", UIManager.get("OptionPane.cancelIcon")); 762 // Ensures caret color is the same than text foreground color, see #12257 763 // See http://docs.oracle.com/javase/8/docs/api/javax/swing/plaf/synth/doc-files/componentProperties.html 764 for (String p : Arrays.asList( 765 "EditorPane", "FormattedTextField", "PasswordField", "TextArea", "TextField", "TextPane")) { 766 UIManager.put(p+".caretForeground", UIManager.getColor(p+".foreground")); 767 } 768 769 I18n.translateJavaInternalMessages(); 770 771 // init default coordinate format 772 // 773 try { 774 CoordinateFormat.setCoordinateFormat(CoordinateFormat.valueOf(Main.pref.get("coordinates"))); 775 } catch (IllegalArgumentException iae) { 776 Main.trace(iae); 777 CoordinateFormat.setCoordinateFormat(CoordinateFormat.DECIMAL_DEGREES); 778 } 779 } 780 781 protected static void postConstructorProcessCmdLine(ProgramArguments args) { 782 List<File> fileList = new ArrayList<>(); 783 for (String s : args.get(Option.DOWNLOAD)) { 784 DownloadParamType.paramType(s).download(s, fileList); 785 } 786 if (!fileList.isEmpty()) { 787 OpenFileAction.openFiles(fileList, true); 788 } 789 for (String s : args.get(Option.DOWNLOADGPS)) { 790 DownloadParamType.paramType(s).downloadGps(s); 791 } 792 for (String s : args.get(Option.SELECTION)) { 793 SearchAction.search(s, SearchAction.SearchMode.add); 794 } 795 } 796 797 /** 798 * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). 799 * If there are some unsaved data layers, asks first for user confirmation. 800 * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code. 801 * @param exitCode The return code 802 * @param reason the reason for exiting 803 * @return {@code true} if JOSM has been closed, {@code false} if the user has cancelled the operation. 804 * @since 11093 (3378 with a different function signature) 805 */ 806 public static boolean exitJosm(boolean exit, int exitCode, SaveLayersDialog.Reason reason) { 807 final boolean proceed = Boolean.TRUE.equals(GuiHelper.runInEDTAndWaitAndReturn(() -> 808 SaveLayersDialog.saveUnsavedModifications(getLayerManager().getLayers(), 809 reason != null ? reason : SaveLayersDialog.Reason.EXIT))); 810 if (proceed) { 811 if (Main.main != null) { 812 Main.main.shutdown(); 813 } 814 815 if (exit) { 816 System.exit(exitCode); 817 } 818 return true; 819 } 820 return false; 821 } 822 823 protected void shutdown() { 824 if (!GraphicsEnvironment.isHeadless()) { 825 worker.shutdown(); 826 ImageProvider.shutdown(false); 827 JCSCacheManager.shutdown(); 828 } 829 if (map != null) { 830 map.rememberToggleDialogWidth(); 831 } 832 // Remove all layers because somebody may rely on layerRemoved events (like AutosaveTask) 833 getLayerManager().resetState(); 834 try { 835 pref.saveDefaults(); 836 } catch (IOException ex) { 837 Main.warn(ex, tr("Failed to save default preferences.")); 838 } 839 if (!GraphicsEnvironment.isHeadless()) { 840 worker.shutdownNow(); 841 ImageProvider.shutdown(true); 842 } 843 } 844 845 /** 846 * The type of a command line parameter, to be used in switch statements. 847 * @see #paramType 848 */ 849 enum DownloadParamType { 850 httpUrl { 851 @Override 852 void download(String s, Collection<File> fileList) { 853 new OpenLocationAction().openUrl(false, s); 854 } 855 856 @Override 857 void downloadGps(String s) { 858 final Bounds b = OsmUrlToBounds.parse(s); 859 if (b == null) { 860 JOptionPane.showMessageDialog( 861 Main.parent, 862 tr("Ignoring malformed URL: \"{0}\"", s), 863 tr("Warning"), 864 JOptionPane.WARNING_MESSAGE 865 ); 866 return; 867 } 868 downloadFromParamBounds(true, b); 869 } 870 }, fileUrl { 871 @Override 872 void download(String s, Collection<File> fileList) { 873 File f = null; 874 try { 875 f = new File(new URI(s)); 876 } catch (URISyntaxException e) { 877 Main.warn(e); 878 JOptionPane.showMessageDialog( 879 Main.parent, 880 tr("Ignoring malformed file URL: \"{0}\"", s), 881 tr("Warning"), 882 JOptionPane.WARNING_MESSAGE 883 ); 884 } 885 if (f != null) { 886 fileList.add(f); 887 } 888 } 889 }, bounds { 890 891 /** 892 * Download area specified on the command line as bounds string. 893 * @param rawGps Flag to download raw GPS tracks 894 * @param s The bounds parameter 895 */ 896 private void downloadFromParamBounds(final boolean rawGps, String s) { 897 final StringTokenizer st = new StringTokenizer(s, ","); 898 if (st.countTokens() == 4) { 899 Bounds b = new Bounds( 900 new LatLon(Double.parseDouble(st.nextToken()), Double.parseDouble(st.nextToken())), 901 new LatLon(Double.parseDouble(st.nextToken()), Double.parseDouble(st.nextToken())) 902 ); 903 Main.downloadFromParamBounds(rawGps, b); 904 } 905 } 906 907 @Override 908 void download(String param, Collection<File> fileList) { 909 downloadFromParamBounds(false, param); 910 } 911 912 @Override 913 void downloadGps(String param) { 914 downloadFromParamBounds(true, param); 915 } 916 }, fileName { 917 @Override 918 void download(String s, Collection<File> fileList) { 919 fileList.add(new File(s)); 920 } 921 }; 922 923 /** 924 * Performs the download 925 * @param param represents the object to be downloaded 926 * @param fileList files which shall be opened, should be added to this collection 927 */ 928 abstract void download(String param, Collection<File> fileList); 929 930 /** 931 * Performs the GPS download 932 * @param param represents the object to be downloaded 933 */ 934 void downloadGps(String param) { 935 JOptionPane.showMessageDialog( 936 Main.parent, 937 tr("Parameter \"downloadgps\" does not accept file names or file URLs"), 938 tr("Warning"), 939 JOptionPane.WARNING_MESSAGE 940 ); 941 } 942 943 /** 944 * Guess the type of a parameter string specified on the command line with --download= or --downloadgps. 945 * 946 * @param s A parameter string 947 * @return The guessed parameter type 948 */ 949 static DownloadParamType paramType(String s) { 950 if (s.startsWith("http:") || s.startsWith("https:")) return DownloadParamType.httpUrl; 951 if (s.startsWith("file:")) return DownloadParamType.fileUrl; 952 String coorPattern = "\\s*[+-]?[0-9]+(\\.[0-9]+)?\\s*"; 953 if (s.matches(coorPattern + "(," + coorPattern + "){3}")) return DownloadParamType.bounds; 954 // everything else must be a file name 955 return DownloadParamType.fileName; 956 } 957 } 958 959 /** 960 * Download area specified as Bounds value. 961 * @param rawGps Flag to download raw GPS tracks 962 * @param b The bounds value 963 */ 964 private static void downloadFromParamBounds(final boolean rawGps, Bounds b) { 965 DownloadTask task = rawGps ? new DownloadGpsTask() : new DownloadOsmTask(); 966 // asynchronously launch the download task ... 967 Future<?> future = task.download(true, b, null); 968 // ... and the continuation when the download is finished (this will wait for the download to finish) 969 Main.worker.execute(new PostDownloadHandler(task, future)); 970 } 971 972 /** 973 * Identifies the current operating system family and initializes the platform hook accordingly. 974 * @since 1849 975 */ 976 public static void determinePlatformHook() { 977 String os = System.getProperty("os.name"); 978 if (os == null) { 979 warn("Your operating system has no name, so I'm guessing its some kind of *nix."); 980 platform = new PlatformHookUnixoid(); 981 } else if (os.toLowerCase(Locale.ENGLISH).startsWith("windows")) { 982 platform = new PlatformHookWindows(); 983 } else if ("Linux".equals(os) || "Solaris".equals(os) || 984 "SunOS".equals(os) || "AIX".equals(os) || 985 "FreeBSD".equals(os) || "NetBSD".equals(os) || "OpenBSD".equals(os)) { 986 platform = new PlatformHookUnixoid(); 987 } else if (os.toLowerCase(Locale.ENGLISH).startsWith("mac os x")) { 988 platform = new PlatformHookOsx(); 989 } else { 990 warn("I don't know your operating system '"+os+"', so I'm guessing its some kind of *nix."); 991 platform = new PlatformHookUnixoid(); 992 } 993 } 994 995 /* ----------------------------------------------------------------------------------------- */ 996 /* projection handling - Main is a registry for a single, global projection instance */ 997 /* */ 998 /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */ 999 /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class. */ 1000 /* ----------------------------------------------------------------------------------------- */ 1001 /** 1002 * The projection method used. 1003 * use {@link #getProjection()} and {@link #setProjection(Projection)} for access. 1004 * Use {@link #setProjection(Projection)} in order to trigger a projection change event. 1005 */ 1006 private static volatile Projection proj; 1007 1008 /** 1009 * Replies the current projection. 1010 * 1011 * @return the currently active projection 1012 */ 1013 public static Projection getProjection() { 1014 return proj; 1015 } 1016 1017 /** 1018 * Sets the current projection 1019 * 1020 * @param p the projection 1021 */ 1022 public static void setProjection(Projection p) { 1023 CheckParameterUtil.ensureParameterNotNull(p); 1024 Projection oldValue = proj; 1025 Bounds b = isDisplayingMapView() ? map.mapView.getRealBounds() : null; 1026 proj = p; 1027 fireProjectionChanged(oldValue, proj, b); 1028 } 1029 1030 /* 1031 * Keep WeakReferences to the listeners. This relieves clients from the burden of 1032 * explicitly removing the listeners and allows us to transparently register every 1033 * created dataset as projection change listener. 1034 */ 1035 private static final List<WeakReference<ProjectionChangeListener>> listeners = new ArrayList<>(); 1036 1037 private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) { 1038 if ((newValue == null ^ oldValue == null) 1039 || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) { 1040 if (Main.map != null) { 1041 // This needs to be called first 1042 Main.map.mapView.fixProjection(); 1043 } 1044 synchronized (Main.class) { 1045 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator(); 1046 while (it.hasNext()) { 1047 WeakReference<ProjectionChangeListener> wr = it.next(); 1048 ProjectionChangeListener listener = wr.get(); 1049 if (listener == null) { 1050 it.remove(); 1051 continue; 1052 } 1053 listener.projectionChanged(oldValue, newValue); 1054 } 1055 } 1056 if (newValue != null && oldBounds != null) { 1057 Main.map.mapView.zoomTo(oldBounds); 1058 } 1059 /* TODO - remove layers with fixed projection */ 1060 } 1061 } 1062 1063 /** 1064 * Register a projection change listener. 1065 * 1066 * @param listener the listener. Ignored if <code>null</code>. 1067 */ 1068 public static void addProjectionChangeListener(ProjectionChangeListener listener) { 1069 if (listener == null) return; 1070 synchronized (Main.class) { 1071 for (WeakReference<ProjectionChangeListener> wr : listeners) { 1072 // already registered ? => abort 1073 if (wr.get() == listener) return; 1074 } 1075 listeners.add(new WeakReference<>(listener)); 1076 } 1077 } 1078 1079 /** 1080 * Removes a projection change listener. 1081 * 1082 * @param listener the listener. Ignored if <code>null</code>. 1083 */ 1084 public static void removeProjectionChangeListener(ProjectionChangeListener listener) { 1085 if (listener == null) return; 1086 synchronized (Main.class) { 1087 Iterator<WeakReference<ProjectionChangeListener>> it = listeners.iterator(); 1088 while (it.hasNext()) { 1089 WeakReference<ProjectionChangeListener> wr = it.next(); 1090 // remove the listener - and any other listener which got garbage 1091 // collected in the meantime 1092 if (wr.get() == null || wr.get() == listener) { 1093 it.remove(); 1094 } 1095 } 1096 } 1097 } 1098 1099 /** 1100 * Listener for window switch events. 1101 * 1102 * These are events, when the user activates a window of another application 1103 * or comes back to JOSM. Window switches from one JOSM window to another 1104 * are not reported. 1105 */ 1106 public interface WindowSwitchListener { 1107 /** 1108 * Called when the user activates a window of another application. 1109 */ 1110 void toOtherApplication(); 1111 1112 /** 1113 * Called when the user comes from a window of another application back to JOSM. 1114 */ 1115 void fromOtherApplication(); 1116 } 1117 1118 private static final List<WeakReference<WindowSwitchListener>> windowSwitchListeners = new ArrayList<>(); 1119 1120 /** 1121 * Register a window switch listener. 1122 * 1123 * @param listener the listener. Ignored if <code>null</code>. 1124 */ 1125 public static void addWindowSwitchListener(WindowSwitchListener listener) { 1126 if (listener == null) return; 1127 synchronized (Main.class) { 1128 for (WeakReference<WindowSwitchListener> wr : windowSwitchListeners) { 1129 // already registered ? => abort 1130 if (wr.get() == listener) return; 1131 } 1132 boolean wasEmpty = windowSwitchListeners.isEmpty(); 1133 windowSwitchListeners.add(new WeakReference<>(listener)); 1134 if (wasEmpty) { 1135 // The following call will have no effect, when there is no window 1136 // at the time. Therefore, MasterWindowListener.setup() will also be 1137 // called, as soon as the main window is shown. 1138 MasterWindowListener.setup(); 1139 } 1140 } 1141 } 1142 1143 /** 1144 * Removes a window switch listener. 1145 * 1146 * @param listener the listener. Ignored if <code>null</code>. 1147 */ 1148 public static void removeWindowSwitchListener(WindowSwitchListener listener) { 1149 if (listener == null) return; 1150 synchronized (Main.class) { 1151 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1152 while (it.hasNext()) { 1153 WeakReference<WindowSwitchListener> wr = it.next(); 1154 // remove the listener - and any other listener which got garbage 1155 // collected in the meantime 1156 if (wr.get() == null || wr.get() == listener) { 1157 it.remove(); 1158 } 1159 } 1160 if (windowSwitchListeners.isEmpty()) { 1161 MasterWindowListener.teardown(); 1162 } 1163 } 1164 } 1165 1166 /** 1167 * WindowListener, that is registered on all Windows of the application. 1168 * 1169 * Its purpose is to notify WindowSwitchListeners, that the user switches to 1170 * another application, e.g. a browser, or back to JOSM. 1171 * 1172 * When changing from JOSM to another application and back (e.g. two times 1173 * alt+tab), the active Window within JOSM may be different. 1174 * Therefore, we need to register listeners to <strong>all</strong> (visible) 1175 * Windows in JOSM, and it does not suffice to monitor the one that was 1176 * deactivated last. 1177 * 1178 * This class is only "active" on demand, i.e. when there is at least one 1179 * WindowSwitchListener registered. 1180 */ 1181 protected static class MasterWindowListener extends WindowAdapter { 1182 1183 private static MasterWindowListener INSTANCE; 1184 1185 public static synchronized MasterWindowListener getInstance() { 1186 if (INSTANCE == null) { 1187 INSTANCE = new MasterWindowListener(); 1188 } 1189 return INSTANCE; 1190 } 1191 1192 /** 1193 * Register listeners to all non-hidden windows. 1194 * 1195 * Windows that are created later, will be cared for in {@link #windowDeactivated(WindowEvent)}. 1196 */ 1197 public static void setup() { 1198 if (!windowSwitchListeners.isEmpty()) { 1199 for (Window w : Window.getWindows()) { 1200 if (w.isShowing() && !Arrays.asList(w.getWindowListeners()).contains(getInstance())) { 1201 w.addWindowListener(getInstance()); 1202 } 1203 } 1204 } 1205 } 1206 1207 /** 1208 * Unregister all listeners. 1209 */ 1210 public static void teardown() { 1211 for (Window w : Window.getWindows()) { 1212 w.removeWindowListener(getInstance()); 1213 } 1214 } 1215 1216 @Override 1217 public void windowActivated(WindowEvent e) { 1218 if (e.getOppositeWindow() == null) { // we come from a window of a different application 1219 // fire WindowSwitchListeners 1220 synchronized (Main.class) { 1221 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1222 while (it.hasNext()) { 1223 WeakReference<WindowSwitchListener> wr = it.next(); 1224 WindowSwitchListener listener = wr.get(); 1225 if (listener == null) { 1226 it.remove(); 1227 continue; 1228 } 1229 listener.fromOtherApplication(); 1230 } 1231 } 1232 } 1233 } 1234 1235 @Override 1236 public void windowDeactivated(WindowEvent e) { 1237 // set up windows that have been created in the meantime 1238 for (Window w : Window.getWindows()) { 1239 if (!w.isShowing()) { 1240 w.removeWindowListener(getInstance()); 1241 } else { 1242 if (!Arrays.asList(w.getWindowListeners()).contains(getInstance())) { 1243 w.addWindowListener(getInstance()); 1244 } 1245 } 1246 } 1247 if (e.getOppositeWindow() == null) { // we go to a window of a different application 1248 // fire WindowSwitchListeners 1249 synchronized (Main.class) { 1250 Iterator<WeakReference<WindowSwitchListener>> it = windowSwitchListeners.iterator(); 1251 while (it.hasNext()) { 1252 WeakReference<WindowSwitchListener> wr = it.next(); 1253 WindowSwitchListener listener = wr.get(); 1254 if (listener == null) { 1255 it.remove(); 1256 continue; 1257 } 1258 listener.toOtherApplication(); 1259 } 1260 } 1261 } 1262 } 1263 } 1264 1265 /** 1266 * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes 1267 * @param listener The MapFrameListener 1268 * @param fireWhenMapViewPresent If true, will fire an initial mapFrameInitialized event 1269 * when the MapFrame is present. Otherwise will only fire when the MapFrame is created 1270 * or destroyed. 1271 * @return {@code true} if the listeners collection changed as a result of the call 1272 */ 1273 public static boolean addMapFrameListener(MapFrameListener listener, boolean fireWhenMapViewPresent) { 1274 if (fireWhenMapViewPresent) { 1275 return mainPanel.addAndFireMapFrameListener(listener); 1276 } else { 1277 return mainPanel.addMapFrameListener(listener); 1278 } 1279 } 1280 1281 /** 1282 * Registers a new {@code MapFrameListener} that will be notified of MapFrame changes 1283 * @param listener The MapFrameListener 1284 * @return {@code true} if the listeners collection changed as a result of the call 1285 * @since 5957 1286 */ 1287 public static boolean addMapFrameListener(MapFrameListener listener) { 1288 return mainPanel.addMapFrameListener(listener); 1289 } 1290 1291 /** 1292 * Unregisters the given {@code MapFrameListener} from MapFrame changes 1293 * @param listener The MapFrameListener 1294 * @return {@code true} if the listeners collection changed as a result of the call 1295 * @since 5957 1296 */ 1297 public static boolean removeMapFrameListener(MapFrameListener listener) { 1298 return mainPanel.removeMapFrameListener(listener); 1299 } 1300 1301 /** 1302 * Adds a new network error that occur to give a hint about broken Internet connection. 1303 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 1304 * 1305 * @param url The accessed URL that caused the error 1306 * @param t The network error 1307 * @return The previous error associated to the given resource, if any. Can be {@code null} 1308 * @since 6642 1309 */ 1310 public static Throwable addNetworkError(URL url, Throwable t) { 1311 if (url != null && t != null) { 1312 Throwable old = addNetworkError(url.toExternalForm(), t); 1313 if (old != null) { 1314 Main.warn("Already here "+old); 1315 } 1316 return old; 1317 } 1318 return null; 1319 } 1320 1321 /** 1322 * Adds a new network error that occur to give a hint about broken Internet connection. 1323 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 1324 * 1325 * @param url The accessed URL that caused the error 1326 * @param t The network error 1327 * @return The previous error associated to the given resource, if any. Can be {@code null} 1328 * @since 6642 1329 */ 1330 public static Throwable addNetworkError(String url, Throwable t) { 1331 if (url != null && t != null) { 1332 return NETWORK_ERRORS.put(url, t); 1333 } 1334 return null; 1335 } 1336 1337 /** 1338 * Returns the network errors that occured until now. 1339 * @return the network errors that occured until now, indexed by URL 1340 * @since 6639 1341 */ 1342 public static Map<String, Throwable> getNetworkErrors() { 1343 return new HashMap<>(NETWORK_ERRORS); 1344 } 1345 1346 /** 1347 * Returns the command-line arguments used to run the application. 1348 * @return the command-line arguments used to run the application 1349 * @since 8356 1350 */ 1351 public static List<String> getCommandLineArgs() { 1352 return Collections.unmodifiableList(COMMAND_LINE_ARGS); 1353 } 1354 1355 /** 1356 * Returns the JOSM website URL. 1357 * @return the josm website URL 1358 * @since 6897 1359 */ 1360 public static String getJOSMWebsite() { 1361 if (Main.pref != null) 1362 return Main.pref.get("josm.url", JOSM_WEBSITE); 1363 return JOSM_WEBSITE; 1364 } 1365 1366 /** 1367 * Returns the JOSM XML URL. 1368 * @return the josm XML URL 1369 * @since 6897 1370 */ 1371 public static String getXMLBase() { 1372 // Always return HTTP (issues reported with HTTPS) 1373 return "http://josm.openstreetmap.de"; 1374 } 1375 1376 /** 1377 * Returns the OSM website URL. 1378 * @return the OSM website URL 1379 * @since 6897 1380 */ 1381 public static String getOSMWebsite() { 1382 if (Main.pref != null) 1383 return Main.pref.get("osm.url", OSM_WEBSITE); 1384 return OSM_WEBSITE; 1385 } 1386 1387 /** 1388 * Replies the base URL for browsing information about a primitive. 1389 * @return the base URL, i.e. https://www.openstreetmap.org 1390 * @since 7678 1391 */ 1392 public static String getBaseBrowseUrl() { 1393 if (Main.pref != null) 1394 return Main.pref.get("osm-browse.url", getOSMWebsite()); 1395 return getOSMWebsite(); 1396 } 1397 1398 /** 1399 * Replies the base URL for browsing information about a user. 1400 * @return the base URL, i.e. https://www.openstreetmap.org/user 1401 * @since 7678 1402 */ 1403 public static String getBaseUserUrl() { 1404 if (Main.pref != null) 1405 return Main.pref.get("osm-user.url", getOSMWebsite() + "/user"); 1406 return getOSMWebsite() + "/user"; 1407 } 1408 1409 /** 1410 * Determines if we are currently running on OSX. 1411 * @return {@code true} if we are currently running on OSX 1412 * @since 6957 1413 */ 1414 public static boolean isPlatformOsx() { 1415 return Main.platform instanceof PlatformHookOsx; 1416 } 1417 1418 /** 1419 * Determines if we are currently running on Windows. 1420 * @return {@code true} if we are currently running on Windows 1421 * @since 7335 1422 */ 1423 public static boolean isPlatformWindows() { 1424 return Main.platform instanceof PlatformHookWindows; 1425 } 1426 1427 /** 1428 * Determines if the given online resource is currently offline. 1429 * @param r the online resource 1430 * @return {@code true} if {@code r} is offline and should not be accessed 1431 * @since 7434 1432 */ 1433 public static boolean isOffline(OnlineResource r) { 1434 return OFFLINE_RESOURCES.contains(r) || OFFLINE_RESOURCES.contains(OnlineResource.ALL); 1435 } 1436 1437 /** 1438 * Sets the given online resource to offline state. 1439 * @param r the online resource 1440 * @return {@code true} if {@code r} was not already offline 1441 * @since 7434 1442 */ 1443 public static boolean setOffline(OnlineResource r) { 1444 return OFFLINE_RESOURCES.add(r); 1445 } 1446 1447 /** 1448 * Sets the given online resource to online state. 1449 * @param r the online resource 1450 * @return {@code true} if {@code r} was offline 1451 * @since 8506 1452 */ 1453 public static boolean setOnline(OnlineResource r) { 1454 return OFFLINE_RESOURCES.remove(r); 1455 } 1456 1457 /** 1458 * Replies the set of online resources currently offline. 1459 * @return the set of online resources currently offline 1460 * @since 7434 1461 */ 1462 public static Set<OnlineResource> getOfflineResources() { 1463 return EnumSet.copyOf(OFFLINE_RESOURCES); 1464 } 1465}