001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.ArrayList;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.Date;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011import java.util.Objects;
012
013import org.openstreetmap.josm.data.Bounds;
014import org.openstreetmap.josm.data.coor.LatLon;
015import org.openstreetmap.josm.data.osm.visitor.Visitor;
016import org.openstreetmap.josm.tools.CheckParameterUtil;
017
018/**
019 * Represents a single changeset in JOSM. For now its only used during
020 * upload but in the future we may do more.
021 * @since 625
022 */
023public final class Changeset implements Tagged {
024
025    /** The maximum changeset tag length allowed by API 0.6 **/
026    public static final int MAX_CHANGESET_TAG_LENGTH = 255;
027
028    /** the changeset id */
029    private int id;
030    /** the user who owns the changeset */
031    private User user;
032    /** date this changeset was created at */
033    private Date createdAt;
034    /** the date this changeset was closed at*/
035    private Date closedAt;
036    /** indicates whether this changeset is still open or not */
037    private boolean open;
038    /** the min. coordinates of the bounding box of this changeset */
039    private LatLon min;
040    /** the max. coordinates of the bounding box of this changeset */
041    private LatLon max;
042    /** the number of comments for this changeset */
043    private int commentsCount;
044    /** the map of tags */
045    private Map<String, String> tags;
046    /** indicates whether this changeset is incomplete. For an incomplete changeset we only know its id */
047    private boolean incomplete;
048    /** the changeset content */
049    private ChangesetDataSet content;
050    /** the changeset discussion */
051    private List<ChangesetDiscussionComment> discussion;
052
053    /**
054     * Creates a new changeset with id 0.
055     */
056    public Changeset() {
057        this(0);
058    }
059
060    /**
061     * Creates a changeset with id <code>id</code>. If id &gt; 0, sets incomplete to true.
062     *
063     * @param id the id
064     */
065    public Changeset(int id) {
066        this.id = id;
067        this.incomplete = id > 0;
068        this.tags = new HashMap<>();
069    }
070
071    /**
072     * Creates a clone of <code>other</code>
073     *
074     * @param other the other changeset. If null, creates a new changeset with id 0.
075     */
076    public Changeset(Changeset other) {
077        if (other == null) {
078            this.id = 0;
079            this.tags = new HashMap<>();
080        } else if (other.isIncomplete()) {
081            setId(other.getId());
082            this.incomplete = true;
083            this.tags = new HashMap<>();
084        } else {
085            this.id = other.id;
086            mergeFrom(other);
087            this.incomplete = false;
088        }
089    }
090
091    /**
092     * Creates a changeset with the data obtained from the given preset, i.e.,
093     * the {@link AbstractPrimitive#getChangesetId() changeset id}, {@link AbstractPrimitive#getUser() user}, and
094     * {@link AbstractPrimitive#getTimestamp() timestamp}.
095     * @param primitive the primitive to use
096     * @return the created changeset
097     */
098    public static Changeset fromPrimitive(final OsmPrimitive primitive) {
099        final Changeset changeset = new Changeset(primitive.getChangesetId());
100        changeset.setUser(primitive.getUser());
101        changeset.setCreatedAt(primitive.getTimestamp()); // not accurate in all cases
102        return changeset;
103    }
104
105    public void visit(Visitor v) {
106        v.visit(this);
107    }
108
109    public int compareTo(Changeset other) {
110        return Integer.compare(getId(), other.getId());
111    }
112
113    public String getName() {
114        // no translation
115        return "changeset " + getId();
116    }
117
118    public String getDisplayName(NameFormatter formatter) {
119        return formatter.format(this);
120    }
121
122    public int getId() {
123        return id;
124    }
125
126    public void setId(int id) {
127        this.id = id;
128    }
129
130    public User getUser() {
131        return user;
132    }
133
134    public void setUser(User user) {
135        this.user = user;
136    }
137
138    public Date getCreatedAt() {
139        return createdAt;
140    }
141
142    public void setCreatedAt(Date createdAt) {
143        this.createdAt = createdAt;
144    }
145
146    public Date getClosedAt() {
147        return closedAt;
148    }
149
150    public void setClosedAt(Date closedAt) {
151        this.closedAt = closedAt;
152    }
153
154    public boolean isOpen() {
155        return open;
156    }
157
158    public void setOpen(boolean open) {
159        this.open = open;
160    }
161
162    public LatLon getMin() {
163        return min;
164    }
165
166    public void setMin(LatLon min) {
167        this.min = min;
168    }
169
170    public LatLon getMax() {
171        return max;
172    }
173
174    public Bounds getBounds() {
175        if (min != null && max != null)
176            return new Bounds(min, max);
177        return null;
178    }
179
180    public void setMax(LatLon max) {
181        this.max = max;
182    }
183
184    /**
185     * Replies the number of comments for this changeset.
186     * @return the number of comments for this changeset
187     * @since 7700
188     */
189    public int getCommentsCount() {
190        return commentsCount;
191    }
192
193    /**
194     * Sets the number of comments for this changeset.
195     * @param commentsCount the number of comments for this changeset
196     * @since 7700
197     */
198    public void setCommentsCount(int commentsCount) {
199        this.commentsCount = commentsCount;
200    }
201
202    @Override
203    public Map<String, String> getKeys() {
204        return tags;
205    }
206
207    @Override
208    public void setKeys(Map<String, String> keys) {
209        CheckParameterUtil.ensureParameterNotNull(keys, "keys");
210        for (String value : keys.values()) {
211            if (value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) {
212                throw new IllegalArgumentException("Changeset tag value is too long: "+value);
213            }
214        }
215        this.tags = keys;
216    }
217
218    public boolean isIncomplete() {
219        return incomplete;
220    }
221
222    public void setIncomplete(boolean incomplete) {
223        this.incomplete = incomplete;
224    }
225
226    @Override
227    public void put(String key, String value) {
228        CheckParameterUtil.ensureParameterNotNull(key, "key");
229        if (value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) {
230            throw new IllegalArgumentException("Changeset tag value is too long: "+value);
231        }
232        this.tags.put(key, value);
233    }
234
235    @Override
236    public String get(String key) {
237        return this.tags.get(key);
238    }
239
240    @Override
241    public void remove(String key) {
242        this.tags.remove(key);
243    }
244
245    @Override
246    public void removeAll() {
247        this.tags.clear();
248    }
249
250    public boolean hasEqualSemanticAttributes(Changeset other) {
251        if (other == null)
252            return false;
253        if (closedAt == null) {
254            if (other.closedAt != null)
255                return false;
256        } else if (!closedAt.equals(other.closedAt))
257            return false;
258        if (createdAt == null) {
259            if (other.createdAt != null)
260                return false;
261        } else if (!createdAt.equals(other.createdAt))
262            return false;
263        if (id != other.id)
264            return false;
265        if (max == null) {
266            if (other.max != null)
267                return false;
268        } else if (!max.equals(other.max))
269            return false;
270        if (min == null) {
271            if (other.min != null)
272                return false;
273        } else if (!min.equals(other.min))
274            return false;
275        if (open != other.open)
276            return false;
277        if (tags == null) {
278            if (other.tags != null)
279                return false;
280        } else if (!tags.equals(other.tags))
281            return false;
282        if (user == null) {
283            if (other.user != null)
284                return false;
285        } else if (!user.equals(other.user))
286            return false;
287        if (commentsCount != other.commentsCount) {
288            return false;
289        }
290        return true;
291    }
292
293    @Override
294    public int hashCode() {
295        return Objects.hash(id);
296    }
297
298    @Override
299    public boolean equals(Object obj) {
300        if (this == obj) return true;
301        if (obj == null || getClass() != obj.getClass()) return false;
302        Changeset changeset = (Changeset) obj;
303        return id == changeset.id;
304    }
305
306    @Override
307    public boolean hasKeys() {
308        return !tags.keySet().isEmpty();
309    }
310
311    @Override
312    public Collection<String> keySet() {
313        return tags.keySet();
314    }
315
316    public boolean isNew() {
317        return id <= 0;
318    }
319
320    public void mergeFrom(Changeset other) {
321        if (other == null)
322            return;
323        if (id != other.id)
324            return;
325        this.user = other.user;
326        this.createdAt = other.createdAt;
327        this.closedAt = other.closedAt;
328        this.open  = other.open;
329        this.min = other.min;
330        this.max = other.max;
331        this.commentsCount = other.commentsCount;
332        this.tags = new HashMap<>(other.tags);
333        this.incomplete = other.incomplete;
334        this.discussion = other.discussion != null ? new ArrayList<>(other.discussion) : null;
335
336        // FIXME: merging of content required?
337        this.content = other.content;
338    }
339
340    public boolean hasContent() {
341        return content != null;
342    }
343
344    public ChangesetDataSet getContent() {
345        return content;
346    }
347
348    public void setContent(ChangesetDataSet content) {
349        this.content = content;
350    }
351
352    /**
353     * Replies the list of comments in the changeset discussion, if any.
354     * @return the list of comments in the changeset discussion. May be empty but never null
355     * @since 7704
356     */
357    public synchronized List<ChangesetDiscussionComment> getDiscussion() {
358        if (discussion == null) {
359            return Collections.emptyList();
360        }
361        return new ArrayList<>(discussion);
362    }
363
364    /**
365     * Adds a comment to the changeset discussion.
366     * @param comment the comment to add. Ignored if null
367     * @since 7704
368     */
369    public synchronized void addDiscussionComment(ChangesetDiscussionComment comment) {
370        if (comment == null) {
371            return;
372        }
373        if (discussion == null) {
374            discussion = new ArrayList<>();
375        }
376        discussion.add(comment);
377    }
378}