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 > 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}