001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.history;
003
004import java.text.MessageFormat;
005import java.util.ArrayList;
006import java.util.Collection;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012import java.util.concurrent.CopyOnWriteArrayList;
013
014import org.openstreetmap.josm.data.osm.Changeset;
015import org.openstreetmap.josm.data.osm.IPrimitive;
016import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
017import org.openstreetmap.josm.data.osm.PrimitiveId;
018import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
019import org.openstreetmap.josm.gui.MainApplication;
020import org.openstreetmap.josm.gui.layer.LayerManager.LayerAddEvent;
021import org.openstreetmap.josm.gui.layer.LayerManager.LayerChangeListener;
022import org.openstreetmap.josm.gui.layer.LayerManager.LayerOrderChangeEvent;
023import org.openstreetmap.josm.gui.layer.LayerManager.LayerRemoveEvent;
024import org.openstreetmap.josm.tools.CheckParameterUtil;
025
026/**
027 * A data set holding histories of OSM primitives.
028 * @since 1670
029 * @since 10386 (new LayerChangeListener interface)
030 */
031public class HistoryDataSet implements LayerChangeListener {
032    /** the unique instance */
033    private static HistoryDataSet historyDataSet;
034
035    /**
036     * Replies the unique instance of the history data set
037     *
038     * @return the unique instance of the history data set
039     */
040    public static synchronized HistoryDataSet getInstance() {
041        if (historyDataSet == null) {
042            historyDataSet = new HistoryDataSet();
043            MainApplication.getLayerManager().addLayerChangeListener(historyDataSet);
044        }
045        return historyDataSet;
046    }
047
048    /** the history data */
049    private final Map<PrimitiveId, ArrayList<HistoryOsmPrimitive>> data;
050    private final CopyOnWriteArrayList<HistoryDataSetListener> listeners;
051    private final Map<Long, Changeset> changesets;
052
053    /**
054     * Constructs a new {@code HistoryDataSet}.
055     */
056    public HistoryDataSet() {
057        data = new HashMap<>();
058        listeners = new CopyOnWriteArrayList<>();
059        changesets = new HashMap<>();
060    }
061
062    /**
063     * Adds a listener that listens to history data set events.
064     * @param listener The listener
065     */
066    public void addHistoryDataSetListener(HistoryDataSetListener listener) {
067        if (listener != null) {
068            listeners.addIfAbsent(listener);
069        }
070    }
071
072    /**
073     * Removes a listener that listens to history data set events.
074     * @param listener The listener
075     */
076    public void removeHistoryDataSetListener(HistoryDataSetListener listener) {
077        listeners.remove(listener);
078    }
079
080    protected void fireHistoryUpdated(PrimitiveId id) {
081        for (HistoryDataSetListener l : listeners) {
082            l.historyUpdated(this, id);
083        }
084    }
085
086    protected void fireCacheCleared() {
087        for (HistoryDataSetListener l : listeners) {
088            l.historyDataSetCleared(this);
089        }
090    }
091
092    /**
093     * Replies the history primitive for the primitive with id <code>id</code>
094     * and version <code>version</code>. null, if no such primitive exists.
095     *
096     * @param id the id of the primitive. &gt; 0 required.
097     * @param type the primitive type. Must not be null.
098     * @param version the version of the primitive. &gt; 0 required
099     * @return the history primitive for the primitive with id <code>id</code>,
100     * type <code>type</code>, and version <code>version</code>
101     */
102    public HistoryOsmPrimitive get(long id, OsmPrimitiveType type, long version) {
103        if (id <= 0)
104            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "id", id));
105        CheckParameterUtil.ensureParameterNotNull(type, "type");
106        if (version <= 0)
107            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "version", version));
108
109        SimplePrimitiveId pid = new SimplePrimitiveId(id, type);
110        List<HistoryOsmPrimitive> versions = data.get(pid);
111        if (versions == null)
112            return null;
113        for (HistoryOsmPrimitive primitive: versions) {
114            if (primitive.matches(id, version))
115                return primitive;
116        }
117        return null;
118    }
119
120    /**
121     * Adds a history primitive to the data set
122     *
123     * @param primitive  the history primitive to add
124     */
125    public void put(HistoryOsmPrimitive primitive) {
126        PrimitiveId id = new SimplePrimitiveId(primitive.getId(), primitive.getType());
127        data.computeIfAbsent(id, k-> new ArrayList<>()).add(primitive);
128        fireHistoryUpdated(id);
129    }
130
131    /**
132     * Adds a changeset to the data set
133     *
134     * @param changeset the changeset to add
135     */
136    public void putChangeset(Changeset changeset) {
137        changesets.put((long) changeset.getId(), changeset);
138        fireHistoryUpdated(null);
139    }
140
141    /**
142     * Replies the history for a given primitive with id <code>id</code>
143     * and type <code>type</code>.
144     *
145     * @param id the id the if of the primitive. &gt; 0 required
146     * @param type the type of the primitive. Must not be null.
147     * @return the history. null, if there isn't a history for <code>id</code> and
148     * <code>type</code>.
149     * @throws IllegalArgumentException if id &lt;= 0
150     * @throws IllegalArgumentException if type is null
151     */
152    public History getHistory(long id, OsmPrimitiveType type) {
153        if (id <= 0)
154            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "id", id));
155        CheckParameterUtil.ensureParameterNotNull(type, "type");
156        SimplePrimitiveId pid = new SimplePrimitiveId(id, type);
157        return getHistory(pid);
158    }
159
160    /**
161     * Replies the history for a primitive with id <code>id</code>. null, if no
162     * such history exists.
163     *
164     * @param pid the primitive id. Must not be null.
165     * @return the history for a primitive with id <code>id</code>. null, if no
166     * such history exists
167     * @throws IllegalArgumentException if pid is null
168     */
169    public History getHistory(PrimitiveId pid) {
170        CheckParameterUtil.ensureParameterNotNull(pid, "pid");
171        List<HistoryOsmPrimitive> versions = data.get(pid);
172        if (versions == null && pid instanceof IPrimitive) {
173            versions = data.get(((IPrimitive) pid).getPrimitiveId());
174        }
175        if (versions == null)
176            return null;
177        for (HistoryOsmPrimitive i : versions) {
178            i.setChangeset(changesets.get(i.getChangesetId()));
179        }
180        return new History(pid.getUniqueId(), pid.getType(), versions);
181    }
182
183    /**
184     * merges the histories from the {@link HistoryDataSet} other in this history data set
185     *
186     * @param other the other history data set. Ignored if null.
187     */
188    public void mergeInto(HistoryDataSet other) {
189        if (other == null)
190            return;
191        this.data.putAll(other.data);
192        this.changesets.putAll(other.changesets);
193        fireHistoryUpdated(null);
194    }
195
196    /**
197     * Gets a unsorted set of all changeset ids that were used by the primitives in this data set
198     * @return The ids
199     */
200    public Collection<Long> getChangesetIds() {
201        final Set<Long> ids = new HashSet<>();
202        for (Collection<HistoryOsmPrimitive> i : data.values()) {
203            for (HistoryOsmPrimitive j : i) {
204                ids.add(j.getChangesetId());
205            }
206        }
207        return ids;
208    }
209
210    /* ------------------------------------------------------------------------------ */
211    /* interface LayerChangeListener                                                  */
212    /* ------------------------------------------------------------------------------ */
213    @Override
214    public void layerOrderChanged(LayerOrderChangeEvent e) {
215        /* irrelevant in this context */
216    }
217
218    @Override
219    public void layerAdded(LayerAddEvent e) {
220        /* irrelevant in this context */
221    }
222
223    @Override
224    public void layerRemoving(LayerRemoveEvent e) {
225        if (!MainApplication.isDisplayingMapView()) return;
226        if (MainApplication.getLayerManager().getLayers().isEmpty()) {
227            data.clear();
228            fireCacheCleared();
229        }
230    }
231}