001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm.history; 003 004import java.text.MessageFormat; 005import java.time.Instant; 006import java.util.ArrayList; 007import java.util.Comparator; 008import java.util.List; 009import java.util.Objects; 010import java.util.stream.Collectors; 011 012import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 013import org.openstreetmap.josm.data.osm.PrimitiveId; 014import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 015import org.openstreetmap.josm.tools.CheckParameterUtil; 016 017/** 018 * Represents the history of an OSM primitive. The history consists 019 * of a list of object snapshots with a specific version. 020 * @since 1670 021 */ 022public class History { 023 024 @FunctionalInterface 025 private interface FilterPredicate { 026 boolean matches(HistoryOsmPrimitive primitive); 027 } 028 029 private static History filter(History history, FilterPredicate predicate) { 030 List<HistoryOsmPrimitive> out = history.versions.stream() 031 .filter(predicate::matches) 032 .collect(Collectors.toList()); 033 return new History(history.id, history.type, out); 034 } 035 036 /** the list of object snapshots */ 037 private final List<HistoryOsmPrimitive> versions; 038 /** the object id */ 039 private final long id; 040 /** the object type */ 041 private final OsmPrimitiveType type; 042 043 /** 044 * Creates a new history for an OSM primitive. 045 * 046 * @param id the id. > 0 required. 047 * @param type the primitive type. Must not be null. 048 * @param versions a list of versions. Can be null. 049 * @throws IllegalArgumentException if id <= 0 050 * @throws IllegalArgumentException if type is null 051 */ 052 protected History(long id, OsmPrimitiveType type, List<HistoryOsmPrimitive> versions) { 053 if (id <= 0) 054 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected, got {1}", "id", id)); 055 CheckParameterUtil.ensureParameterNotNull(type, "type"); 056 this.id = id; 057 this.type = type; 058 this.versions = new ArrayList<>(); 059 if (versions != null) { 060 this.versions.addAll(versions); 061 } 062 } 063 064 /** 065 * Returns a new copy of this history, sorted in ascending order. 066 * @return a new copy of this history, sorted in ascending order 067 */ 068 public History sortAscending() { 069 List<HistoryOsmPrimitive> copy = new ArrayList<>(versions); 070 copy.sort(Comparator.naturalOrder()); 071 return new History(id, type, copy); 072 } 073 074 /** 075 * Returns a new copy of this history, sorted in descending order. 076 * @return a new copy of this history, sorted in descending order 077 */ 078 public History sortDescending() { 079 List<HistoryOsmPrimitive> copy = new ArrayList<>(versions); 080 copy.sort(Comparator.reverseOrder()); 081 return new History(id, type, copy); 082 } 083 084 /** 085 * Returns a new partial copy of this history, from the given date 086 * @param fromDate the starting date 087 * @return a new partial copy of this history, from the given date 088 */ 089 public History from(final Instant fromDate) { 090 return filter(this, primitive -> primitive.getInstant().compareTo(fromDate) >= 0); 091 } 092 093 /** 094 * Returns a new partial copy of this history, until the given date 095 * @param untilDate the end date 096 * @return a new partial copy of this history, until the given date 097 */ 098 public History until(final Instant untilDate) { 099 return filter(this, primitive -> primitive.getInstant().compareTo(untilDate) <= 0); 100 } 101 102 /** 103 * Returns a new partial copy of this history, between the given dates 104 * @param fromDate the starting date 105 * @param untilDate the end date 106 * @return a new partial copy of this history, between the given dates 107 */ 108 public History between(Instant fromDate, Instant untilDate) { 109 return this.from(fromDate).until(untilDate); 110 } 111 112 /** 113 * Returns a new partial copy of this history, from the given version number 114 * @param fromVersion the starting version number 115 * @return a new partial copy of this history, from the given version number 116 */ 117 public History from(final long fromVersion) { 118 return filter(this, primitive -> primitive.getVersion() >= fromVersion); 119 } 120 121 /** 122 * Returns a new partial copy of this history, to the given version number 123 * @param untilVersion the ending version number 124 * @return a new partial copy of this history, to the given version number 125 */ 126 public History until(final long untilVersion) { 127 return filter(this, primitive -> primitive.getVersion() <= untilVersion); 128 } 129 130 /** 131 * Returns a new partial copy of this history, between the given version numbers 132 * @param fromVersion the starting version number 133 * @param untilVersion the ending version number 134 * @return a new partial copy of this history, between the given version numbers 135 */ 136 public History between(long fromVersion, long untilVersion) { 137 return this.from(fromVersion).until(untilVersion); 138 } 139 140 /** 141 * Returns a new partial copy of this history, for the given user id 142 * @param uid the user id 143 * @return a new partial copy of this history, for the given user id 144 */ 145 public History forUserId(final long uid) { 146 return filter(this, primitive -> primitive.getUser() != null && primitive.getUser().getId() == uid); 147 } 148 149 /** 150 * Replies the primitive id for this history. 151 * 152 * @return the primitive id 153 * @see #getPrimitiveId 154 * @see #getType 155 */ 156 public long getId() { 157 return id; 158 } 159 160 /** 161 * Replies the primitive id for this history. 162 * 163 * @return the primitive id 164 * @see #getId 165 */ 166 public PrimitiveId getPrimitiveId() { 167 return new SimplePrimitiveId(id, type); 168 } 169 170 /** 171 * Determines if this history contains a specific version number. 172 * @param version the version number to look for 173 * @return {@code true} if this history contains {@code version}, {@code false} otherwise 174 */ 175 public boolean contains(long version) { 176 return versions.stream().anyMatch(primitive -> primitive.matches(id, version)); 177 } 178 179 /** 180 * Replies the history primitive with version <code>version</code>. null, 181 * if no such primitive exists. 182 * 183 * @param version the version 184 * @return the history primitive with version <code>version</code> 185 */ 186 public HistoryOsmPrimitive getByVersion(long version) { 187 return versions.stream() 188 .filter(primitive -> primitive.matches(id, version)) 189 .findFirst().orElse(null); 190 } 191 192 /** 193 * Replies the history primitive which changed the given key. 194 * @param primitive the reference primitive (the history up to and including this primitive is considered) 195 * @param key the OSM key 196 * @param isLatest whether this relates to a not yet committed changeset 197 * @return the history primitive which changed the given key 198 */ 199 public HistoryOsmPrimitive getWhichChangedTag(HistoryOsmPrimitive primitive, String key, boolean isLatest) { 200 if (primitive == null) { 201 return null; 202 } else if (isLatest && !Objects.equals(getLatest().get(key), primitive.get(key))) { 203 return primitive; 204 } 205 for (int i = versions.indexOf(primitive); i > 0; i--) { 206 if (!Objects.equals(versions.get(i).get(key), versions.get(i - 1).get(key))) { 207 return versions.get(i); 208 } 209 } 210 return versions.get(0); 211 } 212 213 /** 214 * Replies the history primitive at given <code>date</code>. null, 215 * if no such primitive exists. 216 * 217 * @param date the date 218 * @return the history primitive at given <code>date</code> 219 */ 220 public HistoryOsmPrimitive getByDate(Instant date) { 221 History h = sortAscending(); 222 223 if (h.versions.isEmpty()) 224 return null; 225 if (h.get(0).getInstant().compareTo(date) > 0) 226 return null; 227 for (int i = 1; i < h.versions.size(); i++) { 228 if (h.get(i-1).getInstant().compareTo(date) <= 0 229 && h.get(i).getInstant().compareTo(date) >= 0) 230 return h.get(i); 231 } 232 return h.getLatest(); 233 } 234 235 /** 236 * Replies the history primitive at index <code>idx</code>. 237 * 238 * @param idx the index 239 * @return the history primitive at index <code>idx</code> 240 * @throws IndexOutOfBoundsException if index out or range 241 */ 242 public HistoryOsmPrimitive get(int idx) { 243 if (idx < 0 || idx >= versions.size()) 244 throw new IndexOutOfBoundsException(MessageFormat.format( 245 "Parameter ''{0}'' in range 0..{1} expected. Got ''{2}''.", "idx", versions.size()-1, idx)); 246 return versions.get(idx); 247 } 248 249 /** 250 * Replies the earliest entry of this history. 251 * @return the earliest entry of this history 252 */ 253 public HistoryOsmPrimitive getEarliest() { 254 if (isEmpty()) 255 return null; 256 return sortAscending().versions.get(0); 257 } 258 259 /** 260 * Replies the latest entry of this history. 261 * @return the latest entry of this history 262 */ 263 public HistoryOsmPrimitive getLatest() { 264 if (isEmpty()) 265 return null; 266 return sortDescending().versions.get(0); 267 } 268 269 /** 270 * Replies the number of versions. 271 * @return the number of versions 272 */ 273 public int getNumVersions() { 274 return versions.size(); 275 } 276 277 /** 278 * Returns true if this history contains no version. 279 * @return {@code true} if this history contains no version, {@code false} otherwise 280 */ 281 public final boolean isEmpty() { 282 return versions.isEmpty(); 283 } 284 285 /** 286 * Replies the primitive type for this history. 287 * @return the primitive type 288 * @see #getId 289 */ 290 public OsmPrimitiveType getType() { 291 return type; 292 } 293 294 @Override 295 public String toString() { 296 StringBuilder result = new StringBuilder("History [" 297 + (type != null ? ("type=" + type + ", ") : "") + "id=" + id); 298 if (versions != null) { 299 result.append(", versions=\n"); 300 for (HistoryOsmPrimitive v : versions) { 301 result.append('\t').append(v).append(",\n"); 302 } 303 } 304 result.append(']'); 305 return result.toString(); 306 } 307}