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 /** 106 * Visitor pattern. 107 * @param v visitor 108 */ 109 public void visit(Visitor v) { 110 v.visit(this); 111 } 112 113 /** 114 * Compares this changeset to another, based on their identifier. 115 * @param other other changeset 116 * @return the value {@code 0} if {@code getId() == other.getId()}; 117 * a value less than {@code 0} if {@code getId() < other.getId()}; and 118 * a value greater than {@code 0} if {@code getId() > other.getId()} 119 */ 120 public int compareTo(Changeset other) { 121 return Integer.compare(getId(), other.getId()); 122 } 123 124 /** 125 * Returns the changeset name. 126 * @return the changeset name (untranslated: "changeset <identifier>") 127 */ 128 public String getName() { 129 // no translation 130 return "changeset " + getId(); 131 } 132 133 /** 134 * Returns the changeset display name, as per given name formatter. 135 * @param formatter name formatter 136 * @return the changeset display name, as per given name formatter 137 */ 138 public String getDisplayName(NameFormatter formatter) { 139 return formatter.format(this); 140 } 141 142 /** 143 * Returns the changeset identifier. 144 * @return the changeset identifier 145 */ 146 public int getId() { 147 return id; 148 } 149 150 /** 151 * Sets the changeset identifier. 152 * @param id changeset identifier 153 */ 154 public void setId(int id) { 155 this.id = id; 156 } 157 158 /** 159 * Returns the changeset user. 160 * @return the changeset user 161 */ 162 public User getUser() { 163 return user; 164 } 165 166 /** 167 * Sets the changeset user. 168 * @param user changeset user 169 */ 170 public void setUser(User user) { 171 this.user = user; 172 } 173 174 /** 175 * Returns the changeset creation date. 176 * @return the changeset creation date 177 */ 178 public Date getCreatedAt() { 179 return createdAt; 180 } 181 182 /** 183 * Sets the changeset creation date. 184 * @param createdAt changeset creation date 185 */ 186 public void setCreatedAt(Date createdAt) { 187 this.createdAt = createdAt; 188 } 189 190 /** 191 * Returns the changeset closure date. 192 * @return the changeset closure date 193 */ 194 public Date getClosedAt() { 195 return closedAt; 196 } 197 198 /** 199 * Sets the changeset closure date. 200 * @param closedAt changeset closure date 201 */ 202 public void setClosedAt(Date closedAt) { 203 this.closedAt = closedAt; 204 } 205 206 /** 207 * Determines if this changeset is open. 208 * @return {@code true} if this changeset is open 209 */ 210 public boolean isOpen() { 211 return open; 212 } 213 214 /** 215 * Sets whether this changeset is open. 216 * @param open {@code true} if this changeset is open 217 */ 218 public void setOpen(boolean open) { 219 this.open = open; 220 } 221 222 /** 223 * Returns the min lat/lon of the changeset bounding box. 224 * @return the min lat/lon of the changeset bounding box 225 */ 226 public LatLon getMin() { 227 return min; 228 } 229 230 /** 231 * Sets the min lat/lon of the changeset bounding box. 232 * @param min min lat/lon of the changeset bounding box 233 */ 234 public void setMin(LatLon min) { 235 this.min = min; 236 } 237 238 /** 239 * Returns the max lat/lon of the changeset bounding box. 240 * @return the max lat/lon of the changeset bounding box 241 */ 242 public LatLon getMax() { 243 return max; 244 } 245 246 /** 247 * Sets the max lat/lon of the changeset bounding box. 248 * @param max min lat/lon of the changeset bounding box 249 */ 250 public void setMax(LatLon max) { 251 this.max = max; 252 } 253 254 /** 255 * Returns the changeset bounding box. 256 * @return the changeset bounding box 257 */ 258 public Bounds getBounds() { 259 if (min != null && max != null) 260 return new Bounds(min, max); 261 return null; 262 } 263 264 /** 265 * Replies the number of comments for this changeset. 266 * @return the number of comments for this changeset 267 * @since 7700 268 */ 269 public int getCommentsCount() { 270 return commentsCount; 271 } 272 273 /** 274 * Sets the number of comments for this changeset. 275 * @param commentsCount the number of comments for this changeset 276 * @since 7700 277 */ 278 public void setCommentsCount(int commentsCount) { 279 this.commentsCount = commentsCount; 280 } 281 282 @Override 283 public Map<String, String> getKeys() { 284 return tags; 285 } 286 287 @Override 288 public void setKeys(Map<String, String> keys) { 289 CheckParameterUtil.ensureParameterNotNull(keys, "keys"); 290 keys.values().stream() 291 .filter(value -> value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) 292 .findFirst() 293 .ifPresent(value -> { 294 throw new IllegalArgumentException("Changeset tag value is too long: "+value); 295 }); 296 this.tags = keys; 297 } 298 299 /** 300 * Determines if this changeset is incomplete. 301 * @return {@code true} if this changeset is incomplete 302 */ 303 public boolean isIncomplete() { 304 return incomplete; 305 } 306 307 /** 308 * Sets whether this changeset is incomplete 309 * @param incomplete {@code true} if this changeset is incomplete 310 */ 311 public void setIncomplete(boolean incomplete) { 312 this.incomplete = incomplete; 313 } 314 315 @Override 316 public void put(String key, String value) { 317 CheckParameterUtil.ensureParameterNotNull(key, "key"); 318 if (value != null && value.length() > MAX_CHANGESET_TAG_LENGTH) { 319 throw new IllegalArgumentException("Changeset tag value is too long: "+value); 320 } 321 this.tags.put(key, value); 322 } 323 324 @Override 325 public String get(String key) { 326 return this.tags.get(key); 327 } 328 329 @Override 330 public void remove(String key) { 331 this.tags.remove(key); 332 } 333 334 @Override 335 public void removeAll() { 336 this.tags.clear(); 337 } 338 339 /** 340 * Determines if this changeset has equals semantic attributes with another one. 341 * @param other other changeset 342 * @return {@code true} if this changeset has equals semantic attributes with other changeset 343 */ 344 public boolean hasEqualSemanticAttributes(Changeset other) { 345 if (other == null) 346 return false; 347 if (closedAt == null) { 348 if (other.closedAt != null) 349 return false; 350 } else if (!closedAt.equals(other.closedAt)) 351 return false; 352 if (createdAt == null) { 353 if (other.createdAt != null) 354 return false; 355 } else if (!createdAt.equals(other.createdAt)) 356 return false; 357 if (id != other.id) 358 return false; 359 if (max == null) { 360 if (other.max != null) 361 return false; 362 } else if (!max.equals(other.max)) 363 return false; 364 if (min == null) { 365 if (other.min != null) 366 return false; 367 } else if (!min.equals(other.min)) 368 return false; 369 if (open != other.open) 370 return false; 371 if (tags == null) { 372 if (other.tags != null) 373 return false; 374 } else if (!tags.equals(other.tags)) 375 return false; 376 if (user == null) { 377 if (other.user != null) 378 return false; 379 } else if (!user.equals(other.user)) 380 return false; 381 if (commentsCount != other.commentsCount) { 382 return false; 383 } 384 return true; 385 } 386 387 @Override 388 public int hashCode() { 389 return Objects.hash(id); 390 } 391 392 @Override 393 public boolean equals(Object obj) { 394 if (this == obj) return true; 395 if (obj == null || getClass() != obj.getClass()) return false; 396 Changeset changeset = (Changeset) obj; 397 return id == changeset.id; 398 } 399 400 @Override 401 public boolean hasKeys() { 402 return !tags.keySet().isEmpty(); 403 } 404 405 @Override 406 public Collection<String> keySet() { 407 return tags.keySet(); 408 } 409 410 /** 411 * Determines if this changeset is new. 412 * @return {@code true} if this changeset is new ({@code id <= 0}) 413 */ 414 public boolean isNew() { 415 return id <= 0; 416 } 417 418 /** 419 * Merges changeset metadata from another changeset. 420 * @param other other changeset 421 */ 422 public void mergeFrom(Changeset other) { 423 if (other == null) 424 return; 425 if (id != other.id) 426 return; 427 this.user = other.user; 428 this.createdAt = other.createdAt; 429 this.closedAt = other.closedAt; 430 this.open = other.open; 431 this.min = other.min; 432 this.max = other.max; 433 this.commentsCount = other.commentsCount; 434 this.tags = new HashMap<>(other.tags); 435 this.incomplete = other.incomplete; 436 this.discussion = other.discussion != null ? new ArrayList<>(other.discussion) : null; 437 438 // FIXME: merging of content required? 439 this.content = other.content; 440 } 441 442 /** 443 * Determines if this changeset has contents. 444 * @return {@code true} if this changeset has contents 445 */ 446 public boolean hasContent() { 447 return content != null; 448 } 449 450 /** 451 * Returns the changeset contents. 452 * @return the changeset contents, can be null 453 */ 454 public ChangesetDataSet getContent() { 455 return content; 456 } 457 458 /** 459 * Sets the changeset contents. 460 * @param content changeset contents, can be null 461 */ 462 public void setContent(ChangesetDataSet content) { 463 this.content = content; 464 } 465 466 /** 467 * Replies the list of comments in the changeset discussion, if any. 468 * @return the list of comments in the changeset discussion. May be empty but never null 469 * @since 7704 470 */ 471 public synchronized List<ChangesetDiscussionComment> getDiscussion() { 472 if (discussion == null) { 473 return Collections.emptyList(); 474 } 475 return new ArrayList<>(discussion); 476 } 477 478 /** 479 * Adds a comment to the changeset discussion. 480 * @param comment the comment to add. Ignored if null 481 * @since 7704 482 */ 483 public synchronized void addDiscussionComment(ChangesetDiscussionComment comment) { 484 if (comment == null) { 485 return; 486 } 487 if (discussion == null) { 488 discussion = new ArrayList<>(); 489 } 490 discussion.add(comment); 491 } 492}