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