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.Component; 007import java.awt.GraphicsEnvironment; 008import java.io.IOException; 009import java.lang.ref.WeakReference; 010import java.net.URL; 011import java.nio.file.InvalidPathException; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.EnumSet; 015import java.util.HashMap; 016import java.util.List; 017import java.util.Map; 018import java.util.Objects; 019import java.util.Set; 020import java.util.concurrent.Callable; 021import java.util.concurrent.CopyOnWriteArrayList; 022import java.util.concurrent.ExecutionException; 023import java.util.concurrent.ExecutorService; 024import java.util.concurrent.Executors; 025import java.util.concurrent.Future; 026 027import org.openstreetmap.josm.data.Bounds; 028import org.openstreetmap.josm.data.Preferences; 029import org.openstreetmap.josm.data.UndoRedoHandler; 030import org.openstreetmap.josm.data.coor.conversion.CoordinateFormatManager; 031import org.openstreetmap.josm.data.coor.conversion.DecimalDegreesCoordinateFormat; 032import org.openstreetmap.josm.data.coor.conversion.ICoordinateFormat; 033import org.openstreetmap.josm.data.osm.DataSet; 034import org.openstreetmap.josm.data.osm.IPrimitive; 035import org.openstreetmap.josm.data.osm.OsmData; 036import org.openstreetmap.josm.data.osm.OsmPrimitive; 037import org.openstreetmap.josm.data.preferences.JosmBaseDirectories; 038import org.openstreetmap.josm.data.projection.Projection; 039import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 040import org.openstreetmap.josm.io.FileWatcher; 041import org.openstreetmap.josm.io.OnlineResource; 042import org.openstreetmap.josm.io.OsmApi; 043import org.openstreetmap.josm.spi.preferences.Config; 044import org.openstreetmap.josm.tools.CheckParameterUtil; 045import org.openstreetmap.josm.tools.ImageProvider; 046import org.openstreetmap.josm.tools.JosmRuntimeException; 047import org.openstreetmap.josm.tools.Logging; 048import org.openstreetmap.josm.tools.Platform; 049import org.openstreetmap.josm.tools.PlatformHook; 050import org.openstreetmap.josm.tools.PlatformHookOsx; 051import org.openstreetmap.josm.tools.PlatformHookWindows; 052import org.openstreetmap.josm.tools.Utils; 053import org.openstreetmap.josm.tools.bugreport.BugReport; 054 055/** 056 * Abstract class holding various static global variables and methods used in large parts of JOSM application. 057 * @since 98 058 */ 059public abstract class Main { 060 061 /** 062 * The JOSM website URL. 063 * @since 6897 (was public from 6143 to 6896) 064 */ 065 private static final String JOSM_WEBSITE = "https://josm.openstreetmap.de"; 066 067 /** 068 * The OSM website URL. 069 * @since 6897 (was public from 6453 to 6896) 070 */ 071 private static final String OSM_WEBSITE = "https://www.openstreetmap.org"; 072 073 /** 074 * Global parent component for all dialogs and message boxes 075 */ 076 public static Component parent; 077 078 /** 079 * Global application. 080 */ 081 public static volatile Main main; 082 083 /** 084 * Global application preferences 085 */ 086 public static final Preferences pref = new Preferences(JosmBaseDirectories.getInstance()); 087 088 /** 089 * The commands undo/redo handler. 090 */ 091 public final UndoRedoHandler undoRedo = new UndoRedoHandler(); 092 093 /** 094 * The file watcher service. 095 */ 096 public static final FileWatcher fileWatcher = new FileWatcher(); 097 098 private static final Map<String, Throwable> NETWORK_ERRORS = new HashMap<>(); 099 100 private static final Set<OnlineResource> OFFLINE_RESOURCES = EnumSet.noneOf(OnlineResource.class); 101 102 /** 103 * Platform specific code goes in here. 104 * Plugins may replace it, however, some hooks will be called before any plugins have been loaded. 105 * So if you need to hook into those early ones, split your class and send the one with the early hooks 106 * to the JOSM team for inclusion. 107 */ 108 public static volatile PlatformHook platform; 109 110 private static volatile InitStatusListener initListener; 111 112 /** 113 * Initialization task listener. 114 */ 115 public interface InitStatusListener { 116 117 /** 118 * Called when an initialization task updates its status. 119 * @param event task name 120 * @return new status 121 */ 122 Object updateStatus(String event); 123 124 /** 125 * Called when an initialization task completes. 126 * @param status final status 127 */ 128 void finish(Object status); 129 } 130 131 /** 132 * Sets initialization task listener. 133 * @param listener initialization task listener 134 */ 135 public static void setInitStatusListener(InitStatusListener listener) { 136 CheckParameterUtil.ensureParameterNotNull(listener); 137 initListener = listener; 138 } 139 140 /** 141 * Constructs new {@code Main} object. 142 * @see #initialize() 143 */ 144 protected Main() { 145 setInstance(this); 146 } 147 148 private static void setInstance(Main instance) { 149 main = instance; 150 } 151 152 /** 153 * Initializes the main object. A lot of global variables are initialized here. 154 * @since 10340 155 */ 156 public void initialize() { 157 // Initializes tasks that must be run before parallel tasks 158 runInitializationTasks(beforeInitializationTasks()); 159 160 // Initializes tasks to be executed (in parallel) by a ExecutorService 161 try { 162 ExecutorService service = Executors.newFixedThreadPool( 163 Runtime.getRuntime().availableProcessors(), Utils.newThreadFactory("main-init-%d", Thread.NORM_PRIORITY)); 164 for (Future<Void> i : service.invokeAll(parallelInitializationTasks())) { 165 i.get(); 166 } 167 // asynchronous initializations to be completed eventually 168 asynchronousRunnableTasks().forEach(service::submit); 169 asynchronousCallableTasks().forEach(service::submit); 170 try { 171 service.shutdown(); 172 } catch (SecurityException e) { 173 Logging.log(Logging.LEVEL_ERROR, "Unable to shutdown executor service", e); 174 } 175 } catch (InterruptedException | ExecutionException ex) { 176 throw new JosmRuntimeException(ex); 177 } 178 179 // Initializes tasks that must be run after parallel tasks 180 runInitializationTasks(afterInitializationTasks()); 181 } 182 183 private static void runInitializationTasks(List<InitializationTask> tasks) { 184 for (InitializationTask task : tasks) { 185 try { 186 task.call(); 187 } catch (JosmRuntimeException e) { 188 // Can happen if the current projection needs NTV2 grid which is not available 189 // In this case we want the user be able to change his projection 190 BugReport.intercept(e).warn(); 191 } 192 } 193 } 194 195 /** 196 * Returns tasks that must be run before parallel tasks. 197 * @return tasks that must be run before parallel tasks 198 * @see #afterInitializationTasks 199 * @see #parallelInitializationTasks 200 */ 201 protected List<InitializationTask> beforeInitializationTasks() { 202 return Collections.emptyList(); 203 } 204 205 /** 206 * Returns tasks to be executed (in parallel) by a ExecutorService. 207 * @return tasks to be executed (in parallel) by a ExecutorService 208 */ 209 protected Collection<InitializationTask> parallelInitializationTasks() { 210 return Collections.emptyList(); 211 } 212 213 /** 214 * Returns asynchronous callable initializations to be completed eventually 215 * @return asynchronous callable initializations to be completed eventually 216 */ 217 protected List<Callable<?>> asynchronousCallableTasks() { 218 return Collections.emptyList(); 219 } 220 221 /** 222 * Returns asynchronous runnable initializations to be completed eventually 223 * @return asynchronous runnable initializations to be completed eventually 224 */ 225 protected List<Runnable> asynchronousRunnableTasks() { 226 return Collections.emptyList(); 227 } 228 229 /** 230 * Returns tasks that must be run after parallel tasks. 231 * @return tasks that must be run after parallel tasks 232 * @see #beforeInitializationTasks 233 * @see #parallelInitializationTasks 234 */ 235 protected List<InitializationTask> afterInitializationTasks() { 236 return Collections.emptyList(); 237 } 238 239 protected static final class InitializationTask implements Callable<Void> { 240 241 private final String name; 242 private final Runnable task; 243 244 /** 245 * Constructs a new {@code InitializationTask}. 246 * @param name translated name to be displayed to user 247 * @param task runnable initialization task 248 */ 249 public InitializationTask(String name, Runnable task) { 250 this.name = name; 251 this.task = task; 252 } 253 254 @Override 255 public Void call() { 256 Object status = null; 257 if (initListener != null) { 258 status = initListener.updateStatus(name); 259 } 260 task.run(); 261 if (initListener != null) { 262 initListener.finish(status); 263 } 264 return null; 265 } 266 } 267 268 /** 269 * Replies the current selected OSM primitives, from a end-user point of view. 270 * It is not always technically the same collection of primitives than {@link DataSet#getSelected()}. 271 * @return The current selected OSM primitives, from a end-user point of view. Can be {@code null}. 272 * @since 6546 273 */ 274 public Collection<OsmPrimitive> getInProgressSelection() { 275 return Collections.emptyList(); 276 } 277 278 /** 279 * Replies the current selected primitives, from a end-user point of view. 280 * It is not always technically the same collection of primitives than {@link OsmData#getSelected()}. 281 * @return The current selected primitives, from a end-user point of view. Can be {@code null}. 282 * @since 13926 283 */ 284 public Collection<? extends IPrimitive> getInProgressISelection() { 285 return Collections.emptyList(); 286 } 287 288 /** 289 * Gets the active edit data set (not read-only). 290 * @return That data set, <code>null</code>. 291 * @see #getActiveDataSet 292 * @since 12691 293 */ 294 public abstract DataSet getEditDataSet(); 295 296 /** 297 * Gets the active data set (can be read-only). 298 * @return That data set, <code>null</code>. 299 * @see #getEditDataSet 300 * @since 13434 301 */ 302 public abstract DataSet getActiveDataSet(); 303 304 /** 305 * Sets the active data set (and also edit data set if not read-only). 306 * @param ds New data set, or <code>null</code> 307 * @since 13434 308 */ 309 public abstract void setActiveDataSet(DataSet ds); 310 311 /** 312 * Determines if the list of data sets managed by JOSM contains {@code ds}. 313 * @param ds the data set to look for 314 * @return {@code true} if the list of data sets managed by JOSM contains {@code ds} 315 * @since 12718 316 */ 317 public abstract boolean containsDataSet(DataSet ds); 318 319 /////////////////////////////////////////////////////////////////////////// 320 // Implementation part 321 /////////////////////////////////////////////////////////////////////////// 322 323 /** 324 * Should be called before the main constructor to setup some parameter stuff 325 */ 326 public static void preConstructorInit() { 327 // init default coordinate format 328 ICoordinateFormat fmt = CoordinateFormatManager.getCoordinateFormat(Config.getPref().get("coordinates")); 329 if (fmt == null) { 330 fmt = DecimalDegreesCoordinateFormat.INSTANCE; 331 } 332 CoordinateFormatManager.setCoordinateFormat(fmt); 333 } 334 335 /** 336 * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). 337 * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code. 338 * @param exitCode The return code 339 * @return {@code true} 340 * @since 12636 341 */ 342 public static boolean exitJosm(boolean exit, int exitCode) { 343 if (Main.main != null) { 344 Main.main.shutdown(); 345 } 346 347 if (exit) { 348 System.exit(exitCode); 349 } 350 return true; 351 } 352 353 /** 354 * Shutdown JOSM. 355 */ 356 protected void shutdown() { 357 if (!GraphicsEnvironment.isHeadless()) { 358 ImageProvider.shutdown(false); 359 } 360 try { 361 pref.saveDefaults(); 362 } catch (IOException | InvalidPathException ex) { 363 Logging.log(Logging.LEVEL_WARN, tr("Failed to save default preferences."), ex); 364 } 365 if (!GraphicsEnvironment.isHeadless()) { 366 ImageProvider.shutdown(true); 367 } 368 } 369 370 /** 371 * Identifies the current operating system family and initializes the platform hook accordingly. 372 * @since 1849 373 */ 374 public static void determinePlatformHook() { 375 platform = Platform.determinePlatform().accept(PlatformHook.CONSTRUCT_FROM_PLATFORM); 376 } 377 378 /* ----------------------------------------------------------------------------------------- */ 379 /* projection handling - Main is a registry for a single, global projection instance */ 380 /* */ 381 /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */ 382 /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class. */ 383 /* ----------------------------------------------------------------------------------------- */ 384 /** 385 * The projection method used. 386 * Use {@link #getProjection()} and {@link #setProjection(Projection)} for access. 387 * Use {@link #setProjection(Projection)} in order to trigger a projection change event. 388 */ 389 private static volatile Projection proj; 390 391 /** 392 * Replies the current projection. 393 * 394 * @return the currently active projection 395 */ 396 public static Projection getProjection() { 397 return proj; 398 } 399 400 /** 401 * Sets the current projection 402 * 403 * @param p the projection 404 */ 405 public static void setProjection(Projection p) { 406 CheckParameterUtil.ensureParameterNotNull(p); 407 Projection oldValue = proj; 408 Bounds b = main != null ? main.getRealBounds() : null; 409 proj = p; 410 fireProjectionChanged(oldValue, proj, b); 411 } 412 413 /** 414 * Returns the bounds for the current projection. Used for projection events. 415 * @return the bounds for the current projection 416 * @see #restoreOldBounds 417 */ 418 protected Bounds getRealBounds() { 419 // To be overriden 420 return null; 421 } 422 423 /** 424 * Restore clean state corresponding to old bounds after a projection change event. 425 * @param oldBounds bounds previously returned by {@link #getRealBounds}, before the change of projection 426 * @see #getRealBounds 427 */ 428 protected void restoreOldBounds(Bounds oldBounds) { 429 // To be overriden 430 } 431 432 /* 433 * Keep WeakReferences to the listeners. This relieves clients from the burden of 434 * explicitly removing the listeners and allows us to transparently register every 435 * created dataset as projection change listener. 436 */ 437 private static final List<WeakReference<ProjectionChangeListener>> listeners = new CopyOnWriteArrayList<>(); 438 439 private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) { 440 if ((newValue == null ^ oldValue == null) 441 || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) { 442 listeners.removeIf(x -> x.get() == null); 443 listeners.stream().map(WeakReference::get).filter(Objects::nonNull).forEach(x -> x.projectionChanged(oldValue, newValue)); 444 if (newValue != null && oldBounds != null && main != null) { 445 main.restoreOldBounds(oldBounds); 446 } 447 /* TODO - remove layers with fixed projection */ 448 } 449 } 450 451 /** 452 * Register a projection change listener. 453 * The listener is registered to be weak, so keep a reference of it if you want it to be preserved. 454 * 455 * @param listener the listener. Ignored if <code>null</code>. 456 */ 457 public static void addProjectionChangeListener(ProjectionChangeListener listener) { 458 if (listener == null) return; 459 for (WeakReference<ProjectionChangeListener> wr : listeners) { 460 // already registered ? => abort 461 if (wr.get() == listener) return; 462 } 463 listeners.add(new WeakReference<>(listener)); 464 } 465 466 /** 467 * Removes a projection change listener. 468 * 469 * @param listener the listener. Ignored if <code>null</code>. 470 */ 471 public static void removeProjectionChangeListener(ProjectionChangeListener listener) { 472 if (listener == null) return; 473 // remove the listener - and any other listener which got garbage collected in the meantime 474 listeners.removeIf(wr -> wr.get() == null || wr.get() == listener); 475 } 476 477 /** 478 * Remove all projection change listeners. For testing purposes only. 479 * @since 13322 480 */ 481 public static void clearProjectionChangeListeners() { 482 listeners.clear(); 483 } 484 485 /** 486 * Adds a new network error that occur to give a hint about broken Internet connection. 487 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 488 * 489 * @param url The accessed URL that caused the error 490 * @param t The network error 491 * @return The previous error associated to the given resource, if any. Can be {@code null} 492 * @since 6642 493 */ 494 public static Throwable addNetworkError(URL url, Throwable t) { 495 if (url != null && t != null) { 496 Throwable old = addNetworkError(url.toExternalForm(), t); 497 if (old != null) { 498 Logging.warn("Already here "+old); 499 } 500 return old; 501 } 502 return null; 503 } 504 505 /** 506 * Adds a new network error that occur to give a hint about broken Internet connection. 507 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 508 * 509 * @param url The accessed URL that caused the error 510 * @param t The network error 511 * @return The previous error associated to the given resource, if any. Can be {@code null} 512 * @since 6642 513 */ 514 public static Throwable addNetworkError(String url, Throwable t) { 515 if (url != null && t != null) { 516 return NETWORK_ERRORS.put(url, t); 517 } 518 return null; 519 } 520 521 /** 522 * Returns the network errors that occured until now. 523 * @return the network errors that occured until now, indexed by URL 524 * @since 6639 525 */ 526 public static Map<String, Throwable> getNetworkErrors() { 527 return new HashMap<>(NETWORK_ERRORS); 528 } 529 530 /** 531 * Clears the network errors cache. 532 * @since 12011 533 */ 534 public static void clearNetworkErrors() { 535 NETWORK_ERRORS.clear(); 536 } 537 538 /** 539 * Returns the JOSM website URL. 540 * @return the josm website URL 541 * @since 6897 542 */ 543 public static String getJOSMWebsite() { 544 if (Config.getPref() != null) 545 return Config.getPref().get("josm.url", JOSM_WEBSITE); 546 return JOSM_WEBSITE; 547 } 548 549 /** 550 * Returns the JOSM XML URL. 551 * @return the josm XML URL 552 * @since 6897 553 */ 554 public static String getXMLBase() { 555 // Always return HTTP (issues reported with HTTPS) 556 return "http://josm.openstreetmap.de"; 557 } 558 559 /** 560 * Returns the OSM website URL. 561 * @return the OSM website URL 562 * @since 6897 563 */ 564 public static String getOSMWebsite() { 565 if (Config.getPref() != null) 566 return Config.getPref().get("osm.url", OSM_WEBSITE); 567 return OSM_WEBSITE; 568 } 569 570 /** 571 * Returns the OSM website URL depending on the selected {@link OsmApi}. 572 * @return the OSM website URL depending on the selected {@link OsmApi} 573 */ 574 private static String getOSMWebsiteDependingOnSelectedApi() { 575 final String api = OsmApi.getOsmApi().getServerUrl(); 576 if (OsmApi.DEFAULT_API_URL.equals(api)) { 577 return getOSMWebsite(); 578 } else { 579 return api.replaceAll("/api$", ""); 580 } 581 } 582 583 /** 584 * Replies the base URL for browsing information about a primitive. 585 * @return the base URL, i.e. https://www.openstreetmap.org 586 * @since 7678 587 */ 588 public static String getBaseBrowseUrl() { 589 if (Config.getPref() != null) 590 return Config.getPref().get("osm-browse.url", getOSMWebsiteDependingOnSelectedApi()); 591 return getOSMWebsiteDependingOnSelectedApi(); 592 } 593 594 /** 595 * Replies the base URL for browsing information about a user. 596 * @return the base URL, i.e. https://www.openstreetmap.org/user 597 * @since 7678 598 */ 599 public static String getBaseUserUrl() { 600 if (Config.getPref() != null) 601 return Config.getPref().get("osm-user.url", getOSMWebsiteDependingOnSelectedApi() + "/user"); 602 return getOSMWebsiteDependingOnSelectedApi() + "/user"; 603 } 604 605 /** 606 * Determines if we are currently running on OSX. 607 * @return {@code true} if we are currently running on OSX 608 * @since 6957 609 */ 610 public static boolean isPlatformOsx() { 611 return Main.platform instanceof PlatformHookOsx; 612 } 613 614 /** 615 * Determines if we are currently running on Windows. 616 * @return {@code true} if we are currently running on Windows 617 * @since 7335 618 */ 619 public static boolean isPlatformWindows() { 620 return Main.platform instanceof PlatformHookWindows; 621 } 622 623 /** 624 * Determines if the given online resource is currently offline. 625 * @param r the online resource 626 * @return {@code true} if {@code r} is offline and should not be accessed 627 * @since 7434 628 */ 629 public static boolean isOffline(OnlineResource r) { 630 return OFFLINE_RESOURCES.contains(r) || OFFLINE_RESOURCES.contains(OnlineResource.ALL); 631 } 632 633 /** 634 * Sets the given online resource to offline state. 635 * @param r the online resource 636 * @return {@code true} if {@code r} was not already offline 637 * @since 7434 638 */ 639 public static boolean setOffline(OnlineResource r) { 640 return OFFLINE_RESOURCES.add(r); 641 } 642 643 /** 644 * Sets the given online resource to online state. 645 * @param r the online resource 646 * @return {@code true} if {@code r} was offline 647 * @since 8506 648 */ 649 public static boolean setOnline(OnlineResource r) { 650 return OFFLINE_RESOURCES.remove(r); 651 } 652 653 /** 654 * Replies the set of online resources currently offline. 655 * @return the set of online resources currently offline 656 * @since 7434 657 */ 658 public static Set<OnlineResource> getOfflineResources() { 659 return EnumSet.copyOf(OFFLINE_RESOURCES); 660 } 661}