001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.projection;
003
004import java.lang.ref.WeakReference;
005import java.util.List;
006import java.util.Objects;
007import java.util.concurrent.CopyOnWriteArrayList;
008
009import org.openstreetmap.josm.data.Bounds;
010import org.openstreetmap.josm.tools.CheckParameterUtil;
011
012/**
013 * Registry for a single, global projection instance.
014 * @since 14120
015 */
016public final class ProjectionRegistry {
017
018    /**
019     * The projection method used.
020     * Use {@link #getProjection()} and {@link #setProjection(Projection)} for access.
021     * Use {@link #setProjection(Projection)} in order to trigger a projection change event.
022     */
023    private static volatile Projection proj;
024
025    private static ProjectionBoundsProvider boundsProvider;
026
027    /*
028     * Keep WeakReferences to the listeners. This relieves clients from the burden of
029     * explicitly removing the listeners and allows us to transparently register every
030     * created dataset as projection change listener.
031     */
032    private static final List<WeakReference<ProjectionChangeListener>> listeners = new CopyOnWriteArrayList<>();
033
034    private ProjectionRegistry() {
035        // hide constructor
036    }
037
038    /**
039     * Replies the current projection.
040     *
041     * @return the currently active projection
042     */
043    public static Projection getProjection() {
044        return proj;
045    }
046
047    /**
048     * Sets the current projection
049     *
050     * @param p the projection
051     */
052    public static void setProjection(Projection p) {
053        CheckParameterUtil.ensureParameterNotNull(p);
054        Projection oldValue = proj;
055        Bounds b = boundsProvider != null ? boundsProvider.getRealBounds() : null;
056        proj = p;
057        fireProjectionChanged(oldValue, proj, b);
058    }
059
060    private static void fireProjectionChanged(Projection oldValue, Projection newValue, Bounds oldBounds) {
061        if ((newValue == null ^ oldValue == null)
062                || (newValue != null && oldValue != null && !Objects.equals(newValue.toCode(), oldValue.toCode()))) {
063            listeners.removeIf(x -> x.get() == null);
064            listeners.stream().map(WeakReference::get).filter(Objects::nonNull).forEach(x -> x.projectionChanged(oldValue, newValue));
065            if (newValue != null && oldBounds != null && boundsProvider != null) {
066                boundsProvider.restoreOldBounds(oldBounds);
067            }
068            /* TODO - remove layers with fixed projection */
069        }
070    }
071
072    /**
073     * Register a projection change listener.
074     * The listener is registered to be weak, so keep a reference of it if you want it to be preserved.
075     *
076     * @param listener the listener. Ignored if <code>null</code>.
077     */
078    public static void addProjectionChangeListener(ProjectionChangeListener listener) {
079        if (listener == null) return;
080        for (WeakReference<ProjectionChangeListener> wr : listeners) {
081            // already registered ? => abort
082            if (wr.get() == listener) return;
083        }
084        listeners.add(new WeakReference<>(listener));
085    }
086
087    /**
088     * Removes a projection change listener.
089     *
090     * @param listener the listener. Ignored if <code>null</code>.
091     */
092    public static void removeProjectionChangeListener(ProjectionChangeListener listener) {
093        if (listener == null) return;
094        // remove the listener - and any other listener which got garbage collected in the meantime
095        listeners.removeIf(wr -> wr.get() == null || wr.get() == listener);
096    }
097
098    /**
099     * Remove all projection change listeners. For testing purposes only.
100     */
101    public static void clearProjectionChangeListeners() {
102        listeners.clear();
103    }
104
105    /**
106     * Returns the bounds provider called in projection events.
107     * @return the bounds provider
108     */
109    public static ProjectionBoundsProvider getBoundsProvider() {
110        return boundsProvider;
111    }
112
113    /**
114     * Sets the bounds provider called in projection events. Must not be null
115     * @param provider the bounds provider
116     */
117    public static void setboundsProvider(ProjectionBoundsProvider provider) {
118        boundsProvider = Objects.requireNonNull(provider);
119    }
120}