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}