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}