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