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