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 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. &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 if id &lt;= 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}