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. > 0 required. 096 * @param type the primitive type. Must not be null. 097 * @param version the version of the primitive. > 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. > 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 <= 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}