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