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.Main;
015import org.openstreetmap.josm.data.osm.Changeset;
016import org.openstreetmap.josm.data.osm.IPrimitive;
017import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
018import org.openstreetmap.josm.data.osm.PrimitiveId;
019import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
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            Main.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    public void addHistoryDataSetListener(HistoryDataSetListener listener) {
063        if (listener != null) {
064            listeners.addIfAbsent(listener);
065        }
066    }
067
068    public void removeHistoryDataSetListener(HistoryDataSetListener listener) {
069        listeners.remove(listener);
070    }
071
072    protected void fireHistoryUpdated(PrimitiveId id) {
073        for (HistoryDataSetListener l : listeners) {
074            l.historyUpdated(this, id);
075        }
076    }
077
078    protected void fireCacheCleared() {
079        for (HistoryDataSetListener l : listeners) {
080            l.historyDataSetCleared(this);
081        }
082    }
083
084    /**
085     * Replies the history primitive for the primitive with id <code>id</code>
086     * and version <code>version</code>. null, if no such primitive exists.
087     *
088     * @param id the id of the primitive. &gt; 0 required.
089     * @param type the primitive type. Must not be null.
090     * @param version the version of the primitive. &gt; 0 required
091     * @return the history primitive for the primitive with id <code>id</code>,
092     * type <code>type</code>, and version <code>version</code>
093     */
094    public HistoryOsmPrimitive get(long id, OsmPrimitiveType type, long version) {
095        if (id <= 0)
096            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "id", id));
097        CheckParameterUtil.ensureParameterNotNull(type, "type");
098        if (version <= 0)
099            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "version", version));
100
101        SimplePrimitiveId pid = new SimplePrimitiveId(id, type);
102        List<HistoryOsmPrimitive> versions = data.get(pid);
103        if (versions == null)
104            return null;
105        for (HistoryOsmPrimitive primitive: versions) {
106            if (primitive.matches(id, version))
107                return primitive;
108        }
109        return null;
110    }
111
112    /**
113     * Adds a history primitive to the data set
114     *
115     * @param primitive  the history primitive to add
116     */
117    public void put(HistoryOsmPrimitive primitive) {
118        PrimitiveId id = new SimplePrimitiveId(primitive.getId(), primitive.getType());
119        if (data.get(id) == null) {
120            data.put(id, new ArrayList<HistoryOsmPrimitive>());
121        }
122        data.get(id).add(primitive);
123        fireHistoryUpdated(id);
124    }
125
126    /**
127     * Adds a changeset to the data set
128     *
129     * @param changeset the changeset to add
130     */
131    public void putChangeset(Changeset changeset) {
132        changesets.put((long) changeset.getId(), changeset);
133        fireHistoryUpdated(null);
134    }
135
136    /**
137     * Replies the history for a given primitive with id <code>id</code>
138     * and type <code>type</code>.
139     *
140     * @param id the id the if of the primitive. &gt; 0 required
141     * @param type the type of the primitive. Must not be null.
142     * @return the history. null, if there isn't a history for <code>id</code> and
143     * <code>type</code>.
144     * @throws IllegalArgumentException if id &lt;= 0
145     * @throws IllegalArgumentException if type is null
146     */
147    public History getHistory(long id, OsmPrimitiveType type) {
148        if (id <= 0)
149            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "id", id));
150        CheckParameterUtil.ensureParameterNotNull(type, "type");
151        SimplePrimitiveId pid = new SimplePrimitiveId(id, type);
152        return getHistory(pid);
153    }
154
155    /**
156     * Replies the history for a primitive with id <code>id</code>. null, if no
157     * such history exists.
158     *
159     * @param pid the primitive id. Must not be null.
160     * @return the history for a primitive with id <code>id</code>. null, if no
161     * such history exists
162     * @throws IllegalArgumentException if pid is null
163     */
164    public History getHistory(PrimitiveId pid) {
165        CheckParameterUtil.ensureParameterNotNull(pid, "pid");
166        List<HistoryOsmPrimitive> versions = data.get(pid);
167        if (versions == null && pid instanceof IPrimitive) {
168            versions = data.get(((IPrimitive) pid).getPrimitiveId());
169        }
170        if (versions == null)
171            return null;
172        for (HistoryOsmPrimitive i : versions) {
173            i.setChangeset(changesets.get(i.getChangesetId()));
174        }
175        return new History(pid.getUniqueId(), pid.getType(), versions);
176    }
177
178    /**
179     * merges the histories from the {@link HistoryDataSet} other in this history data set
180     *
181     * @param other the other history data set. Ignored if null.
182     */
183    public void mergeInto(HistoryDataSet other) {
184        if (other == null)
185            return;
186        this.data.putAll(other.data);
187        this.changesets.putAll(other.changesets);
188        fireHistoryUpdated(null);
189    }
190
191    public Collection<Long> getChangesetIds() {
192        final Set<Long> ids = new HashSet<>();
193        for (Collection<HistoryOsmPrimitive> i : data.values()) {
194            for (HistoryOsmPrimitive j : i) {
195                ids.add(j.getChangesetId());
196            }
197        }
198        return ids;
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 (!Main.isDisplayingMapView()) return;
217        if (Main.getLayerManager().getLayers().isEmpty()) {
218            data.clear();
219            fireCacheCleared();
220        }
221    }
222}