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.util.Collections;
008import java.util.Date;
009import java.util.HashMap;
010import java.util.Locale;
011import java.util.Map;
012
013import org.openstreetmap.josm.data.osm.Changeset;
014import org.openstreetmap.josm.data.osm.Node;
015import org.openstreetmap.josm.data.osm.OsmPrimitive;
016import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
017import org.openstreetmap.josm.data.osm.PrimitiveId;
018import org.openstreetmap.josm.data.osm.Relation;
019import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
020import org.openstreetmap.josm.data.osm.User;
021import org.openstreetmap.josm.data.osm.Way;
022import org.openstreetmap.josm.tools.CheckParameterUtil;
023
024/**
025 * Represents an immutable OSM primitive in the context of a historical view on
026 * OSM data.
027 *
028 */
029public abstract class HistoryOsmPrimitive implements Comparable<HistoryOsmPrimitive> {
030
031    private long id;
032    private boolean visible;
033    private User user;
034    private long changesetId;
035    private Changeset changeset;
036    private Date timestamp;
037    private long version;
038    private Map<String, String> tags;
039
040    protected final void ensurePositiveLong(long value, String name) {
041        if (value <= 0) {
042            throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0 expected. Got ''{1}''.", name, value));
043        }
044    }
045
046    /**
047     * Constructs a new {@code HistoryOsmPrimitive}.
048     *
049     * @param id the id (&gt; 0 required)
050     * @param version the version (&gt; 0 required)
051     * @param visible whether the primitive is still visible
052     * @param user the user (!= null required)
053     * @param changesetId the changeset id (&gt; 0 required)
054     * @param timestamp the timestamp (!= null required)
055     *
056     * @throws IllegalArgumentException if preconditions are violated
057     */
058    public HistoryOsmPrimitive(long id, long version, boolean visible, User user, long changesetId, Date timestamp) {
059        this(id, version, visible, user, changesetId, timestamp, true);
060    }
061
062    /**
063     * Constructs a new {@code HistoryOsmPrimitive} with a configurable checking of historic parameters.
064     * This is needed to build virtual HistoryOsmPrimitives for modified primitives, which do not have a timestamp and a changeset id.
065     *
066     * @param id the id (&gt; 0 required)
067     * @param version the version (&gt; 0 required)
068     * @param visible whether the primitive is still visible
069     * @param user the user (!= null required)
070     * @param changesetId the changeset id (&gt; 0 required if {@code checkHistoricParams} is true)
071     * @param timestamp the timestamp (!= null required if {@code checkHistoricParams} is true)
072     * @param checkHistoricParams if true, checks values of {@code changesetId} and {@code timestamp}
073     *
074     * @throws IllegalArgumentException if preconditions are violated
075     * @since 5440
076     */
077    public HistoryOsmPrimitive(long id, long version, boolean visible, User user, long changesetId, Date timestamp,
078            boolean checkHistoricParams) {
079        ensurePositiveLong(id, "id");
080        ensurePositiveLong(version, "version");
081        CheckParameterUtil.ensureParameterNotNull(user, "user");
082        if (checkHistoricParams) {
083            ensurePositiveLong(changesetId, "changesetId");
084            CheckParameterUtil.ensureParameterNotNull(timestamp, "timestamp");
085        }
086        this.id = id;
087        this.version = version;
088        this.visible = visible;
089        this.user = user;
090        this.changesetId  = changesetId;
091        this.timestamp = timestamp;
092        tags = new HashMap<>();
093    }
094
095    /**
096     * Constructs a new {@code HistoryOsmPrimitive} from an existing {@link OsmPrimitive}.
097     * @param p the primitive
098     */
099    public HistoryOsmPrimitive(OsmPrimitive p) {
100        this(p.getId(), p.getVersion(), p.isVisible(), p.getUser(), p.getChangesetId(), p.getTimestamp());
101    }
102
103    /**
104     * Replies a new {@link HistoryNode}, {@link HistoryWay} or {@link HistoryRelation} from an existing {@link OsmPrimitive}.
105     * @param p the primitive
106     * @return a new {@code HistoryNode}, {@code HistoryWay} or {@code HistoryRelation} from {@code p}.
107     */
108    public static HistoryOsmPrimitive forOsmPrimitive(OsmPrimitive p) {
109        if (p instanceof Node) {
110            return new HistoryNode((Node) p);
111        } else if (p instanceof Way) {
112            return new HistoryWay((Way) p);
113        } else if (p instanceof Relation) {
114            return new HistoryRelation((Relation) p);
115        } else {
116            return null;
117        }
118    }
119
120    public long getId() {
121        return id;
122    }
123
124    public PrimitiveId getPrimitiveId() {
125        return new SimplePrimitiveId(id, getType());
126    }
127
128    public boolean isVisible() {
129        return visible;
130    }
131
132    public User getUser() {
133        return user;
134    }
135
136    public long getChangesetId() {
137        return changesetId;
138    }
139
140    public Date getTimestamp() {
141        return timestamp;
142    }
143
144    public long getVersion() {
145        return version;
146    }
147
148    public boolean matches(long id, long version) {
149        return this.id == id && this.version == version;
150    }
151
152    public boolean matches(long id) {
153        return this.id == id;
154    }
155
156    public abstract OsmPrimitiveType getType();
157
158    @Override
159    public int compareTo(HistoryOsmPrimitive o) {
160        if (this.id != o.id)
161            throw new ClassCastException(tr("Cannot compare primitive with ID ''{0}'' to primitive with ID ''{1}''.", o.id, this.id));
162        return Long.compare(this.version, o.version);
163    }
164
165    public void put(String key, String value) {
166        tags.put(key, value);
167    }
168
169    public String get(String key) {
170        return tags.get(key);
171    }
172
173    public boolean hasTag(String key) {
174        return tags.get(key) != null;
175    }
176
177    public Map<String, String> getTags() {
178        return Collections.unmodifiableMap(tags);
179    }
180
181    public Changeset getChangeset() {
182        return changeset;
183    }
184
185    public void setChangeset(Changeset changeset) {
186        this.changeset = changeset;
187    }
188
189    /**
190     * Sets the tags for this history primitive. Removes all
191     * tags if <code>tags</code> is null.
192     *
193     * @param tags the tags. May be null.
194     */
195    public void setTags(Map<String, String> tags) {
196        if (tags == null) {
197            this.tags = new HashMap<>();
198        } else {
199            this.tags = new HashMap<>(tags);
200        }
201    }
202
203    /**
204     * Replies the name of this primitive. The default implementation replies the value
205     * of the tag <tt>name</tt> or null, if this tag is not present.
206     *
207     * @return the name of this primitive
208     */
209    public String getName() {
210        if (get("name") != null)
211            return get("name");
212        return null;
213    }
214
215    /**
216     * Replies the display name of a primitive formatted by <code>formatter</code>
217     * @param formatter The formatter used to generate a display name
218     *
219     * @return the display name
220     */
221    public abstract String getDisplayName(HistoryNameFormatter formatter);
222
223    /**
224     * Replies the a localized name for this primitive given by the value of the tags (in this order)
225     * <ul>
226     *   <li>name:lang_COUNTRY_Variant  of the current locale</li>
227     *   <li>name:lang_COUNTRY of the current locale</li>
228     *   <li>name:lang of the current locale</li>
229     *   <li>name of the current locale</li>
230     * </ul>
231     *
232     * null, if no such tag exists
233     *
234     * @return the name of this primitive
235     */
236    public String getLocalName() {
237        String key = "name:" + Locale.getDefault();
238        if (get(key) != null)
239            return get(key);
240        key = "name:" + Locale.getDefault().getLanguage() + '_' + Locale.getDefault().getCountry();
241        if (get(key) != null)
242            return get(key);
243        key = "name:" + Locale.getDefault().getLanguage();
244        if (get(key) != null)
245            return get(key);
246        return getName();
247    }
248
249    @Override
250    public int hashCode() {
251        final int prime = 31;
252        int result = 1;
253        result = prime * result + (int) (id ^ (id >>> 32));
254        result = prime * result + (int) (version ^ (version >>> 32));
255        return result;
256    }
257
258    @Override
259    public boolean equals(Object obj) {
260        if (this == obj)
261            return true;
262        if (!(obj instanceof HistoryOsmPrimitive))
263            return false;
264        // equal semantics is valid for subclasses like {@link HistoryOsmNode} etc. too.
265        // So, don't enforce equality of class.
266        HistoryOsmPrimitive other = (HistoryOsmPrimitive) obj;
267        if (id != other.id)
268            return false;
269        if (version != other.version)
270            return false;
271        return true;
272    }
273
274    @Override
275    public String toString() {
276        return getClass().getSimpleName() + " [version=" + version + ", id=" + id + ", visible=" + visible + ", "
277                + (timestamp != null ? "timestamp=" + timestamp : "") + ", "
278                + (user != null ? "user=" + user + ", " : "") + "changesetId="
279                + changesetId
280                + ']';
281    }
282}