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