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