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. &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        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}