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.Comparator; 007import java.util.Date; 008import java.util.List; 009 010import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 011import org.openstreetmap.josm.data.osm.PrimitiveId; 012import org.openstreetmap.josm.data.osm.SimplePrimitiveId; 013import org.openstreetmap.josm.tools.CheckParameterUtil; 014 015/** 016 * Represents the history of an OSM primitive. The history consists 017 * of a list of object snapshots with a specific version. 018 * @since 1670 019 */ 020public class History { 021 022 @FunctionalInterface 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 final 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 copy.sort(Comparator.naturalOrder()); 072 return new History(id, type, copy); 073 } 074 075 /** 076 * Returns a new copy of this history, sorted in descending order. 077 * @return a new copy of this history, sorted in descending order 078 */ 079 public History sortDescending() { 080 List<HistoryOsmPrimitive> copy = new ArrayList<>(versions); 081 copy.sort(Comparator.reverseOrder()); 082 return new History(id, type, copy); 083 } 084 085 /** 086 * Returns a new partial copy of this history, from the given date 087 * @param fromDate the starting date 088 * @return a new partial copy of this history, from the given date 089 */ 090 public History from(final Date fromDate) { 091 return filter(this, primitive -> primitive.getTimestamp().compareTo(fromDate) >= 0); 092 } 093 094 /** 095 * Returns a new partial copy of this history, until the given date 096 * @param untilDate the end date 097 * @return a new partial copy of this history, until the given date 098 */ 099 public History until(final Date untilDate) { 100 return filter(this, primitive -> primitive.getTimestamp().compareTo(untilDate) <= 0); 101 } 102 103 /** 104 * Returns a new partial copy of this history, between the given dates 105 * @param fromDate the starting date 106 * @param untilDate the end date 107 * @return a new partial copy of this history, between the given dates 108 */ 109 public History between(Date fromDate, Date untilDate) { 110 return this.from(fromDate).until(untilDate); 111 } 112 113 /** 114 * Returns a new partial copy of this history, from the given version number 115 * @param fromVersion the starting version number 116 * @return a new partial copy of this history, from the given version number 117 */ 118 public History from(final long fromVersion) { 119 return filter(this, primitive -> primitive.getVersion() >= fromVersion); 120 } 121 122 /** 123 * Returns a new partial copy of this history, to the given version number 124 * @param untilVersion the ending version number 125 * @return a new partial copy of this history, to the given version number 126 */ 127 public History until(final long untilVersion) { 128 return filter(this, primitive -> primitive.getVersion() <= untilVersion); 129 } 130 131 /** 132 * Returns a new partial copy of this history, betwwen the given version numbers 133 * @param fromVersion the starting version number 134 * @param untilVersion the ending version number 135 * @return a new partial copy of this history, between the given version numbers 136 */ 137 public History between(long fromVersion, long untilVersion) { 138 return this.from(fromVersion).until(untilVersion); 139 } 140 141 /** 142 * Returns a new partial copy of this history, for the given user id 143 * @param uid the user id 144 * @return a new partial copy of this history, for the given user id 145 */ 146 public History forUserId(final long uid) { 147 return filter(this, primitive -> primitive.getUser() != null && primitive.getUser().getId() == uid); 148 } 149 150 /** 151 * Replies the primitive id for this history. 152 * 153 * @return the primitive id 154 * @see #getPrimitiveId 155 * @see #getType 156 */ 157 public long getId() { 158 return id; 159 } 160 161 /** 162 * Replies the primitive id for this history. 163 * 164 * @return the primitive id 165 * @see #getId 166 */ 167 public PrimitiveId getPrimitiveId() { 168 return new SimplePrimitiveId(id, type); 169 } 170 171 /** 172 * Determines if this history contains a specific version number. 173 * @param version the version number to look for 174 * @return {@code true} if this history contains {@code version}, {@code false} otherwise 175 */ 176 public boolean contains(long version) { 177 for (HistoryOsmPrimitive primitive: versions) { 178 if (primitive.matches(id, version)) 179 return true; 180 } 181 return false; 182 } 183 184 /** 185 * Replies the history primitive with version <code>version</code>. null, 186 * if no such primitive exists. 187 * 188 * @param version the version 189 * @return the history primitive with version <code>version</code> 190 */ 191 public HistoryOsmPrimitive getByVersion(long version) { 192 for (HistoryOsmPrimitive primitive: versions) { 193 if (primitive.matches(id, version)) 194 return primitive; 195 } 196 return null; 197 } 198 199 /** 200 * Replies the history primitive at given <code>date</code>. null, 201 * if no such primitive exists. 202 * 203 * @param date the date 204 * @return the history primitive at given <code>date</code> 205 */ 206 public HistoryOsmPrimitive getByDate(Date date) { 207 History h = sortAscending(); 208 209 if (h.versions.isEmpty()) 210 return null; 211 if (h.get(0).getTimestamp().compareTo(date) > 0) 212 return null; 213 for (int i = 1; i < h.versions.size(); i++) { 214 if (h.get(i-1).getTimestamp().compareTo(date) <= 0 215 && h.get(i).getTimestamp().compareTo(date) >= 0) 216 return h.get(i); 217 } 218 return h.getLatest(); 219 } 220 221 /** 222 * Replies the history primitive at index <code>idx</code>. 223 * 224 * @param idx the index 225 * @return the history primitive at index <code>idx</code> 226 * @throws IndexOutOfBoundsException if index out or range 227 */ 228 public HistoryOsmPrimitive get(int idx) { 229 if (idx < 0 || idx >= versions.size()) 230 throw new IndexOutOfBoundsException(MessageFormat.format( 231 "Parameter ''{0}'' in range 0..{1} expected. Got ''{2}''.", "idx", versions.size()-1, idx)); 232 return versions.get(idx); 233 } 234 235 /** 236 * Replies the earliest entry of this history. 237 * @return the earliest entry of this history 238 */ 239 public HistoryOsmPrimitive getEarliest() { 240 if (isEmpty()) 241 return null; 242 return sortAscending().versions.get(0); 243 } 244 245 /** 246 * Replies the latest entry of this history. 247 * @return the latest entry of this history 248 */ 249 public HistoryOsmPrimitive getLatest() { 250 if (isEmpty()) 251 return null; 252 return sortDescending().versions.get(0); 253 } 254 255 /** 256 * Replies the number of versions. 257 * @return the number of versions 258 */ 259 public int getNumVersions() { 260 return versions.size(); 261 } 262 263 /** 264 * Returns true if this history contains no version. 265 * @return {@code true} if this history contains no version, {@code false} otherwise 266 */ 267 public final boolean isEmpty() { 268 return versions.isEmpty(); 269 } 270 271 /** 272 * Replies the primitive type for this history. 273 * @return the primitive type 274 * @see #getId 275 */ 276 public OsmPrimitiveType getType() { 277 return type; 278 } 279 280 @Override 281 public String toString() { 282 StringBuilder result = new StringBuilder("History [" 283 + (type != null ? ("type=" + type + ", ") : "") + "id=" + id); 284 if (versions != null) { 285 result.append(", versions=\n"); 286 for (HistoryOsmPrimitive v : versions) { 287 result.append('\t').append(v).append(",\n"); 288 } 289 } 290 result.append(']'); 291 return result.toString(); 292 } 293}