001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm.history;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.text.MessageFormat;
007import java.time.Instant;
008import java.util.Collection;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashMap;
012import java.util.Locale;
013import java.util.Map;
014import java.util.Objects;
015
016import org.openstreetmap.josm.data.osm.Changeset;
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.PrimitiveData;
020import org.openstreetmap.josm.data.osm.PrimitiveId;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
023import org.openstreetmap.josm.data.osm.Tagged;
024import org.openstreetmap.josm.data.osm.User;
025import org.openstreetmap.josm.data.osm.Way;
026import org.openstreetmap.josm.tools.CheckParameterUtil;
027import org.openstreetmap.josm.tools.Logging;
028
029/**
030 * Represents an immutable OSM primitive in the context of a historical view on OSM data.
031 * @since 1670
032 */
033public abstract class HistoryOsmPrimitive implements Tagged, Comparable<HistoryOsmPrimitive>, PrimitiveId {
034
035    private final long id;
036    private final boolean visible;
037    private final User user;
038    private final long changesetId;
039    private Changeset changeset;
040    private final Instant timestamp;
041    private final long version;
042    private Map<String, String> tags;
043
044    /**
045     * Constructs a new {@code HistoryOsmPrimitive}.
046     *
047     * @param id the id (&gt; 0 required)
048     * @param version the version (&gt; 0 required)
049     * @param visible whether the primitive is still visible
050     * @param user the user (!= null required)
051     * @param changesetId the changeset id (&gt; 0 required)
052     * @param timestamp the timestamp (!= null required)
053     *
054     * @throws IllegalArgumentException if preconditions are violated
055     */
056    protected HistoryOsmPrimitive(long id, long version, boolean visible, User user, long changesetId, Instant timestamp) {
057        this(id, version, visible, user, changesetId, timestamp, true);
058    }
059
060    /**
061     * Constructs a new {@code HistoryOsmPrimitive} with a configurable checking of historic parameters.
062     * This is needed to build virtual HistoryOsmPrimitives for modified primitives, which do not have a timestamp and a changeset id.
063     *
064     * @param id the id (&gt; 0 required)
065     * @param version the version (&gt; 0 required)
066     * @param visible whether the primitive is still visible
067     * @param user the user (!= null required)
068     * @param changesetId the changeset id (&gt; 0 required if {@code checkHistoricParams} is true)
069     * @param timestamp the timestamp (!= null required if {@code checkHistoricParams} is true)
070     * @param checkHistoricParams if true, checks values of {@code changesetId} and {@code timestamp}
071     *
072     * @throws IllegalArgumentException if preconditions are violated
073     * @since 5440
074     */
075    protected HistoryOsmPrimitive(long id, long version, boolean visible, User user, long changesetId, Instant timestamp,
076            boolean checkHistoricParams) {
077        ensurePositiveLong(id, "id");
078        ensurePositiveLong(version, "version");
079        CheckParameterUtil.ensureParameterNotNull(user, "user");
080        if (checkHistoricParams) {
081            ensurePositiveLong(changesetId, "changesetId");
082            CheckParameterUtil.ensureParameterNotNull(timestamp, "timestamp");
083        }
084        this.id = id;
085        this.version = version;
086        this.visible = visible;
087        this.user = user;
088        this.changesetId = changesetId;
089        this.timestamp = timestamp;
090        this.tags = new HashMap<>();
091    }
092
093    /**
094     * Constructs a new {@code HistoryOsmPrimitive} from an existing {@link OsmPrimitive}.
095     * @param p the primitive
096     */
097    protected HistoryOsmPrimitive(OsmPrimitive p) {
098        this(p.getId(), p.getVersion(), p.isVisible(), p.getUser(), p.getChangesetId(), p.getInstant());
099    }
100
101    /**
102     * Replies a new {@link HistoryNode}, {@link HistoryWay} or {@link HistoryRelation} from an existing {@link OsmPrimitive}.
103     * @param p the primitive
104     * @return a new {@code HistoryNode}, {@code HistoryWay} or {@code HistoryRelation} from {@code p}.
105     */
106    public static HistoryOsmPrimitive forOsmPrimitive(OsmPrimitive p) {
107        if (p instanceof Node) {
108            return new HistoryNode((Node) p);
109        } else if (p instanceof Way) {
110            return new HistoryWay((Way) p);
111        } else if (p instanceof Relation) {
112            return new HistoryRelation((Relation) p);
113        } else {
114            return null;
115        }
116    }
117
118    /**
119     * Returns the id.
120     * @return the id
121     */
122    public long getId() {
123        return id;
124    }
125
126    /**
127     * Returns the primitive id.
128     * @return the primitive id
129     */
130    public PrimitiveId getPrimitiveId() {
131        return new SimplePrimitiveId(id, getType());
132    }
133
134    /**
135     * Determines if the primitive is still visible.
136     * @return {@code true} if the primitive is still visible
137     */
138    public boolean isVisible() {
139        return visible;
140    }
141
142    /**
143     * Returns the user.
144     * @return the user
145     */
146    public User getUser() {
147        return user;
148    }
149
150    /**
151     * Returns the changeset id.
152     * @return the changeset id
153     */
154    public long getChangesetId() {
155        return changesetId;
156    }
157
158    /**
159     * Returns the timestamp.
160     * @return the timestamp
161     * @deprecated Use {@link #getInstant()}
162     */
163    @Deprecated
164    public Date getTimestamp() {
165        return Date.from(timestamp);
166    }
167
168    /**
169     * Returns the timestamp.
170     * @return the timestamp
171     */
172    public Instant getInstant() {
173        return timestamp;
174    }
175
176    /**
177     * Returns the version.
178     * @return the version
179     */
180    public long getVersion() {
181        return version;
182    }
183
184    /**
185     * Checks that value is positive.
186     * @param value value to check
187     * @param name parameter name for error message
188     * @throws IllegalArgumentException if {@code value <= 0}
189     */
190    protected final void ensurePositiveLong(long value, String name) {
191        if (value <= 0) {
192            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected. Got ''{1}''.", name, value));
193        }
194    }
195
196    /**
197     * Determines if this history matches given id and version.
198     * @param id Primitive identifier
199     * @param version Primitive version
200     * @return {@code true} if this history matches given id and version
201     */
202    public boolean matches(long id, long version) {
203        return this.id == id && this.version == version;
204    }
205
206    /**
207     * Determines if this history matches given id.
208     * @param id Primitive identifier
209     * @return {@code true} if this history matches given id
210     */
211    public boolean matches(long id) {
212        return this.id == id;
213    }
214
215    @Override
216    public final long getUniqueId() {
217        return getId();
218    }
219
220    @Override
221    public final boolean isNew() {
222        return false;
223    }
224
225    @Override
226    public int compareTo(HistoryOsmPrimitive o) {
227        if (this.id != o.id)
228            throw new ClassCastException(tr("Cannot compare primitive with ID ''{0}'' to primitive with ID ''{1}''.", o.id, this.id));
229        return Long.compare(this.version, o.version);
230    }
231
232    @Override
233    public final void put(String key, String value) {
234        tags.put(key, value);
235    }
236
237    @Override
238    public final String get(String key) {
239        return tags.get(key);
240    }
241
242    @Override
243    public final boolean hasKey(String key) {
244        return tags.containsKey(key);
245    }
246
247    @Override
248    public final Map<String, String> getKeys() {
249        return getTags();
250    }
251
252    @Override
253    public final void setKeys(Map<String, String> keys) {
254        throw new UnsupportedOperationException();
255    }
256
257    @Override
258    public final void remove(String key) {
259        throw new UnsupportedOperationException();
260    }
261
262    @Override
263    public final void removeAll() {
264        throw new UnsupportedOperationException();
265    }
266
267    @Override
268    public final boolean hasKeys() {
269        return !tags.isEmpty();
270    }
271
272    @Override
273    public final Collection<String> keySet() {
274        return Collections.unmodifiableSet(tags.keySet());
275    }
276
277    @Override
278    public int getNumKeys() {
279        return tags.size();
280    }
281
282    /**
283     * Replies the key/value map.
284     * @return the key/value map
285     */
286    public Map<String, String> getTags() {
287        return Collections.unmodifiableMap(tags);
288    }
289
290    /**
291     * Returns the changeset for this history primitive.
292     * @return the changeset for this history primitive
293     */
294    public Changeset getChangeset() {
295        return changeset;
296    }
297
298    /**
299     * Sets the changeset for this history primitive.
300     * @param changeset the changeset for this history primitive
301     */
302    public void setChangeset(Changeset changeset) {
303        this.changeset = changeset;
304    }
305
306    /**
307     * Sets the tags for this history primitive. Removes all tags if <code>tags</code> is null.
308     *
309     * @param tags the tags. May be null.
310     */
311    public void setTags(Map<String, String> tags) {
312        if (tags == null) {
313            this.tags = new HashMap<>();
314        } else {
315            this.tags = new HashMap<>(tags);
316        }
317    }
318
319    /**
320     * Replies the name of this primitive. The default implementation replies the value
321     * of the tag <code>name</code> or null, if this tag is not present.
322     *
323     * @return the name of this primitive
324     */
325    public String getName() {
326        if (get("name") != null)
327            return get("name");
328        return null;
329    }
330
331    /**
332     * Replies the display name of a primitive formatted by <code>formatter</code>
333     * @param formatter The formatter used to generate a display name
334     *
335     * @return the display name
336     */
337    public abstract String getDisplayName(HistoryNameFormatter formatter);
338
339    /**
340     * Replies the a localized name for this primitive given by the value of the tags (in this order)
341     * <ul>
342     *   <li>name:lang_COUNTRY_Variant  of the current locale</li>
343     *   <li>name:lang_COUNTRY of the current locale</li>
344     *   <li>name:lang of the current locale</li>
345     *   <li>name of the current locale</li>
346     * </ul>
347     *
348     * null, if no such tag exists
349     *
350     * @return the name of this primitive
351     */
352    public String getLocalName() {
353        String key = "name:" + Locale.getDefault();
354        if (get(key) != null)
355            return get(key);
356        key = "name:" + Locale.getDefault().getLanguage() + '_' + Locale.getDefault().getCountry();
357        if (get(key) != null)
358            return get(key);
359        key = "name:" + Locale.getDefault().getLanguage();
360        if (get(key) != null)
361            return get(key);
362        return getName();
363    }
364
365    /**
366     * Fills the attributes common to all primitives with values from this history.
367     * @param data primitive data to fill
368     */
369    protected void fillPrimitiveCommonData(PrimitiveData data) {
370        data.setUser(user);
371        try {
372            data.setVisible(visible);
373        } catch (IllegalStateException e) {
374            Logging.log(Logging.LEVEL_ERROR, "Cannot change visibility for "+data+':', e);
375        }
376        data.setInstant(timestamp);
377        data.setKeys(tags);
378        data.setOsmId(id, (int) version);
379    }
380
381    @Override
382    public int hashCode() {
383        return Objects.hash(id, version);
384    }
385
386    @Override
387    public boolean equals(Object obj) {
388        if (this == obj) return true;
389        if (obj == null || getClass() != obj.getClass()) return false;
390        HistoryOsmPrimitive that = (HistoryOsmPrimitive) obj;
391        return id == that.id && version == that.version;
392    }
393
394    @Override
395    public String toString() {
396        return getClass().getSimpleName() + " [version=" + version + ", id=" + id + ", visible=" + visible + ", "
397                + (timestamp != null ? ("timestamp=" + timestamp) : "") + ", "
398                + (user != null ? ("user=" + user + ", ") : "") + "changesetId="
399                + changesetId
400                + ']';
401    }
402}