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 service.shutdown(); 168 } catch (InterruptedException | ExecutionException ex) { 169 throw new JosmRuntimeException(ex); 170 } 171 172 // Initializes tasks that must be run after parallel tasks 173 runInitializationTasks(afterInitializationTasks()); 174 } 175 176 private static void runInitializationTasks(List<InitializationTask> tasks) { 177 for (InitializationTask task : tasks) { 178 try { 179 task.call(); 180 } catch (JosmRuntimeException e) { 181 // Can happen if the current projection needs NTV2 grid which is not available 182 // In this case we want the user be able to change his projection 183 BugReport.intercept(e).warn(); 184 } 185 } 186 } 187 188 /** 189 * Returns tasks that must be run before parallel tasks. 190 * @return tasks that must be run before parallel tasks 191 * @see #afterInitializationTasks 192 * @see #parallelInitializationTasks 193 */ 194 protected List<InitializationTask> beforeInitializationTasks() { 195 return Collections.emptyList(); 196 } 197 198 /** 199 * Returns tasks to be executed (in parallel) by a ExecutorService. 200 * @return tasks to be executed (in parallel) by a ExecutorService 201 */ 202 protected Collection<InitializationTask> parallelInitializationTasks() { 203 return Collections.emptyList(); 204 } 205 206 /** 207 * Returns asynchronous callable initializations to be completed eventually 208 * @return asynchronous callable initializations to be completed eventually 209 */ 210 protected List<Callable<?>> asynchronousCallableTasks() { 211 return Collections.emptyList(); 212 } 213 214 /** 215 * Returns asynchronous runnable initializations to be completed eventually 216 * @return asynchronous runnable initializations to be completed eventually 217 */ 218 protected List<Runnable> asynchronousRunnableTasks() { 219 return Collections.emptyList(); 220 } 221 222 /** 223 * Returns tasks that must be run after parallel tasks. 224 * @return tasks that must be run after parallel tasks 225 * @see #beforeInitializationTasks 226 * @see #parallelInitializationTasks 227 */ 228 protected List<InitializationTask> afterInitializationTasks() { 229 return Collections.emptyList(); 230 } 231 232 protected static final class InitializationTask implements Callable<Void> { 233 234 private final String name; 235 private final Runnable task; 236 237 /** 238 * Constructs a new {@code InitializationTask}. 239 * @param name translated name to be displayed to user 240 * @param task runnable initialization task 241 */ 242 public InitializationTask(String name, Runnable task) { 243 this.name = name; 244 this.task = task; 245 } 246 247 @Override 248 public Void call() { 249 Object status = null; 250 if (initListener != null) { 251 status = initListener.updateStatus(name); 252 } 253 task.run(); 254 if (initListener != null) { 255 initListener.finish(status); 256 } 257 return null; 258 } 259 } 260 261 /** 262 * Replies the current selected primitives, from a end-user point of view. 263 * It is not always technically the same collection of primitives than {@link DataSet#getSelected()}. 264 * @return The current selected primitives, from a end-user point of view. Can be {@code null}. 265 * @since 6546 266 */ 267 public Collection<OsmPrimitive> getInProgressSelection() { 268 return Collections.emptyList(); 269 } 270 271 /** 272 * Gets the active edit data set (not read-only). 273 * @return That data set, <code>null</code>. 274 * @see #getActiveDataSet 275 * @since 12691 276 */ 277 public abstract DataSet getEditDataSet(); 278 279 /** 280 * Gets the active data set (can be read-only). 281 * @return That data set, <code>null</code>. 282 * @see #getEditDataSet 283 * @since 13434 284 */ 285 public abstract DataSet getActiveDataSet(); 286 287 /** 288 * Sets the active data set (and also edit data set if not read-only). 289 * @param ds New data set, or <code>null</code> 290 * @since 13434 291 */ 292 public abstract void setActiveDataSet(DataSet ds); 293 294 /** 295 * Determines if the list of data sets managed by JOSM contains {@code ds}. 296 * @param ds the data set to look for 297 * @return {@code true} if the list of data sets managed by JOSM contains {@code ds} 298 * @since 12718 299 */ 300 public abstract boolean containsDataSet(DataSet ds); 301 302 /////////////////////////////////////////////////////////////////////////// 303 // Implementation part 304 /////////////////////////////////////////////////////////////////////////// 305 306 /** 307 * Should be called before the main constructor to setup some parameter stuff 308 */ 309 public static void preConstructorInit() { 310 // init default coordinate format 311 ICoordinateFormat fmt = CoordinateFormatManager.getCoordinateFormat(Config.getPref().get("coordinates")); 312 if (fmt == null) { 313 fmt = DecimalDegreesCoordinateFormat.INSTANCE; 314 } 315 CoordinateFormatManager.setCoordinateFormat(fmt); 316 } 317 318 /** 319 * Closes JOSM and optionally terminates the Java Virtual Machine (JVM). 320 * @param exit If {@code true}, the JVM is terminated by running {@link System#exit} with a given return code. 321 * @param exitCode The return code 322 * @return {@code true} 323 * @since 12636 324 */ 325 public static boolean exitJosm(boolean exit, int exitCode) { 326 if (Main.main != null) { 327 Main.main.shutdown(); 328 } 329 330 if (exit) { 331 System.exit(exitCode); 332 } 333 return true; 334 } 335 336 /** 337 * Shutdown JOSM. 338 */ 339 protected void shutdown() { 340 if (!GraphicsEnvironment.isHeadless()) { 341 ImageProvider.shutdown(false); 342 } 343 try { 344 pref.saveDefaults(); 345 } catch (IOException ex) { 346 Logging.log(Logging.LEVEL_WARN, tr("Failed to save default preferences."), ex); 347 } 348 if (!GraphicsEnvironment.isHeadless()) { 349 ImageProvider.shutdown(true); 350 } 351 } 352 353 /** 354 * Identifies the current operating system family and initializes the platform hook accordingly. 355 * @since 1849 356 */ 357 public static void determinePlatformHook() { 358 platform = Platform.determinePlatform().accept(PlatformHook.CONSTRUCT_FROM_PLATFORM); 359 } 360 361 /* ----------------------------------------------------------------------------------------- */ 362 /* projection handling - Main is a registry for a single, global projection instance */ 363 /* */ 364 /* TODO: For historical reasons the registry is implemented by Main. An alternative approach */ 365 /* would be a singleton org.openstreetmap.josm.data.projection.ProjectionRegistry class. */ 366 /* ----------------------------------------------------------------------------------------- */ 367 /** 368 * The projection method used. 369 * Use {@link #getProjection()} and {@link #setProjection(Projection)} for access. 370 * Use {@link #setProjection(Projection)} in order to trigger a projection change event. 371 */ 372 private static volatile Projection proj; 373 374 /** 375 * Replies the current projection. 376 * 377 * @return the currently active projection 378 */ 379 public static Projection getProjection() { 380 return proj; 381 } 382 383 /** 384 * Sets the current projection 385 * 386 * @param p the projection 387 */ 388 public static void setProjection(Projection p) { 389 CheckParameterUtil.ensureParameterNotNull(p); 390 Projection oldValue = proj; 391 Bounds b = main != null ? main.getRealBounds() : null; 392 proj = p; 393 fireProjectionChanged(oldValue, proj, b); 394 } 395 396 /** 397 * Returns the bounds for the current projection. Used for projection events. 398 * @return the bounds for the current projection 399 * @see #restoreOldBounds 400 */ 401 protected Bounds getRealBounds() { 402 // To be overriden 403 return null; 404 } 405 406 /** 407 * Restore clean state corresponding to old bounds after a projection change event. 408 * @param oldBounds bounds previously returned by {@link #getRealBounds}, before the change of projection 409 * @see #getRealBounds 410 */ 411 protected void restoreOldBounds(Bounds oldBounds) { 412 // To be overriden 413 } 414 415 /* 416 * Keep WeakReferences to the listeners. This relieves clients from the burden of 417 * explicitly removing the listeners and allows us to transparently register every 418 * created dataset as projection change listener. 419 */ 420 private static final List<WeakReference<ProjectionChangeListener>> listeners = new CopyOnWriteArrayList<>(); 421 422 private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) { 423 if ((newValue == null ^ oldValue == null) 424 || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) { 425 listeners.removeIf(x -> x.get() == null); 426 listeners.stream().map(WeakReference::get).filter(Objects::nonNull).forEach(x -> x.projectionChanged(oldValue, newValue)); 427 if (newValue != null && oldBounds != null && main != null) { 428 main.restoreOldBounds(oldBounds); 429 } 430 /* TODO - remove layers with fixed projection */ 431 } 432 } 433 434 /** 435 * Register a projection change listener. 436 * The listener is registered to be weak, so keep a reference of it if you want it to be preserved. 437 * 438 * @param listener the listener. Ignored if <code>null</code>. 439 */ 440 public static void addProjectionChangeListener(ProjectionChangeListener listener) { 441 if (listener == null) return; 442 for (WeakReference<ProjectionChangeListener> wr : listeners) { 443 // already registered ? => abort 444 if (wr.get() == listener) return; 445 } 446 listeners.add(new WeakReference<>(listener)); 447 } 448 449 /** 450 * Removes a projection change listener. 451 * 452 * @param listener the listener. Ignored if <code>null</code>. 453 */ 454 public static void removeProjectionChangeListener(ProjectionChangeListener listener) { 455 if (listener == null) return; 456 // remove the listener - and any other listener which got garbage collected in the meantime 457 listeners.removeIf(wr -> wr.get() == null || wr.get() == listener); 458 } 459 460 /** 461 * Remove all projection change listeners. For testing purposes only. 462 * @since 13322 463 */ 464 public static void clearProjectionChangeListeners() { 465 listeners.clear(); 466 } 467 468 /** 469 * Adds a new network error that occur to give a hint about broken Internet connection. 470 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 471 * 472 * @param url The accessed URL that caused the error 473 * @param t The network error 474 * @return The previous error associated to the given resource, if any. Can be {@code null} 475 * @since 6642 476 */ 477 public static Throwable addNetworkError(URL url, Throwable t) { 478 if (url != null && t != null) { 479 Throwable old = addNetworkError(url.toExternalForm(), t); 480 if (old != null) { 481 Logging.warn("Already here "+old); 482 } 483 return old; 484 } 485 return null; 486 } 487 488 /** 489 * Adds a new network error that occur to give a hint about broken Internet connection. 490 * Do not use this method for errors known for sure thrown because of a bad proxy configuration. 491 * 492 * @param url The accessed URL that caused the error 493 * @param t The network error 494 * @return The previous error associated to the given resource, if any. Can be {@code null} 495 * @since 6642 496 */ 497 public static Throwable addNetworkError(String url, Throwable t) { 498 if (url != null && t != null) { 499 return NETWORK_ERRORS.put(url, t); 500 } 501 return null; 502 } 503 504 /** 505 * Returns the network errors that occured until now. 506 * @return the network errors that occured until now, indexed by URL 507 * @since 6639 508 */ 509 public static Map<String, Throwable> getNetworkErrors() { 510 return new HashMap<>(NETWORK_ERRORS); 511 } 512 513 /** 514 * Clears the network errors cache. 515 * @since 12011 516 */ 517 public static void clearNetworkErrors() { 518 NETWORK_ERRORS.clear(); 519 } 520 521 /** 522 * Returns the JOSM website URL. 523 * @return the josm website URL 524 * @since 6897 525 */ 526 public static String getJOSMWebsite() { 527 if (Config.getPref() != null) 528 return Config.getPref().get("josm.url", JOSM_WEBSITE); 529 return JOSM_WEBSITE; 530 } 531 532 /** 533 * Returns the JOSM XML URL. 534 * @return the josm XML URL 535 * @since 6897 536 */ 537 public static String getXMLBase() { 538 // Always return HTTP (issues reported with HTTPS) 539 return "http://josm.openstreetmap.de"; 540 } 541 542 /** 543 * Returns the OSM website URL. 544 * @return the OSM website URL 545 * @since 6897 546 */ 547 public static String getOSMWebsite() { 548 if (Config.getPref() != null) 549 return Config.getPref().get("osm.url", OSM_WEBSITE); 550 return OSM_WEBSITE; 551 } 552 553 /** 554 * Returns the OSM website URL depending on the selected {@link OsmApi}. 555 * @return the OSM website URL depending on the selected {@link OsmApi} 556 */ 557 private static String getOSMWebsiteDependingOnSelectedApi() { 558 final String api = OsmApi.getOsmApi().getServerUrl(); 559 if (OsmApi.DEFAULT_API_URL.equals(api)) { 560 return getOSMWebsite(); 561 } else { 562 return api.replaceAll("/api$", ""); 563 } 564 } 565 566 /** 567 * Replies the base URL for browsing information about a primitive. 568 * @return the base URL, i.e. https://www.openstreetmap.org 569 * @since 7678 570 */ 571 public static String getBaseBrowseUrl() { 572 if (Config.getPref() != null) 573 return Config.getPref().get("osm-browse.url", getOSMWebsiteDependingOnSelectedApi()); 574 return getOSMWebsiteDependingOnSelectedApi(); 575 } 576 577 /** 578 * Replies the base URL for browsing information about a user. 579 * @return the base URL, i.e. https://www.openstreetmap.org/user 580 * @since 7678 581 */ 582 public static String getBaseUserUrl() { 583 if (Config.getPref() != null) 584 return Config.getPref().get("osm-user.url", getOSMWebsiteDependingOnSelectedApi() + "/user"); 585 return getOSMWebsiteDependingOnSelectedApi() + "/user"; 586 } 587 588 /** 589 * Determines if we are currently running on OSX. 590 * @return {@code true} if we are currently running on OSX 591 * @since 6957 592 */ 593 public static boolean isPlatformOsx() { 594 return Main.platform instanceof PlatformHookOsx; 595 } 596 597 /** 598 * Determines if we are currently running on Windows. 599 * @return {@code true} if we are currently running on Windows 600 * @since 7335 601 */ 602 public static boolean isPlatformWindows() { 603 return Main.platform instanceof PlatformHookWindows; 604 } 605 606 /** 607 * Determines if the given online resource is currently offline. 608 * @param r the online resource 609 * @return {@code true} if {@code r} is offline and should not be accessed 610 * @since 7434 611 */ 612 public static boolean isOffline(OnlineResource r) { 613 return OFFLINE_RESOURCES.contains(r) || OFFLINE_RESOURCES.contains(OnlineResource.ALL); 614 } 615 616 /** 617 * Sets the given online resource to offline state. 618 * @param r the online resource 619 * @return {@code true} if {@code r} was not already offline 620 * @since 7434 621 */ 622 public static boolean setOffline(OnlineResource r) { 623 return OFFLINE_RESOURCES.add(r); 624 } 625 626 /** 627 * Sets the given online resource to online state. 628 * @param r the online resource 629 * @return {@code true} if {@code r} was offline 630 * @since 8506 631 */ 632 public static boolean setOnline(OnlineResource r) { 633 return OFFLINE_RESOURCES.remove(r); 634 } 635 636 /** 637 * Replies the set of online resources currently offline. 638 * @return the set of online resources currently offline 639 * @since 7434 640 */ 641 public static Set<OnlineResource> getOfflineResources() { 642 return EnumSet.copyOf(OFFLINE_RESOURCES); 643 } 644}