001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Map; 011import java.util.Set; 012import java.util.stream.Collectors; 013 014import org.openstreetmap.josm.Main; 015import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 016import org.openstreetmap.josm.data.osm.visitor.Visitor; 017import org.openstreetmap.josm.tools.CopyList; 018import org.openstreetmap.josm.tools.SubclassFilteredCollection; 019import org.openstreetmap.josm.tools.Utils; 020 021/** 022 * A relation, having a set of tags and any number (0...n) of members. 023 * 024 * @author Frederik Ramm 025 */ 026public final class Relation extends OsmPrimitive implements IRelation { 027 028 private RelationMember[] members = new RelationMember[0]; 029 030 private BBox bbox; 031 032 /** 033 * @return Members of the relation. Changes made in returned list are not mapped 034 * back to the primitive, use setMembers() to modify the members 035 * @since 1925 036 */ 037 public List<RelationMember> getMembers() { 038 return new CopyList<>(members); 039 } 040 041 /** 042 * 043 * @param members Can be null, in that case all members are removed 044 * @since 1925 045 */ 046 public void setMembers(List<RelationMember> members) { 047 boolean locked = writeLock(); 048 try { 049 for (RelationMember rm : this.members) { 050 rm.getMember().removeReferrer(this); 051 rm.getMember().clearCachedStyle(); 052 } 053 054 if (members != null) { 055 this.members = members.toArray(new RelationMember[members.size()]); 056 } else { 057 this.members = new RelationMember[0]; 058 } 059 for (RelationMember rm : this.members) { 060 rm.getMember().addReferrer(this); 061 rm.getMember().clearCachedStyle(); 062 } 063 064 fireMembersChanged(); 065 } finally { 066 writeUnlock(locked); 067 } 068 } 069 070 @Override 071 public int getMembersCount() { 072 return members.length; 073 } 074 075 /** 076 * Returns the relation member at the specified index. 077 * @param index the index of the relation member 078 * @return relation member at the specified index 079 */ 080 public RelationMember getMember(int index) { 081 return members[index]; 082 } 083 084 /** 085 * Adds the specified relation member at the last position. 086 * @param member the member to add 087 */ 088 public void addMember(RelationMember member) { 089 boolean locked = writeLock(); 090 try { 091 members = Utils.addInArrayCopy(members, member); 092 member.getMember().addReferrer(this); 093 member.getMember().clearCachedStyle(); 094 fireMembersChanged(); 095 } finally { 096 writeUnlock(locked); 097 } 098 } 099 100 /** 101 * Adds the specified relation member at the specified index. 102 * @param member the member to add 103 * @param index the index at which the specified element is to be inserted 104 */ 105 public void addMember(int index, RelationMember member) { 106 boolean locked = writeLock(); 107 try { 108 RelationMember[] newMembers = new RelationMember[members.length + 1]; 109 System.arraycopy(members, 0, newMembers, 0, index); 110 System.arraycopy(members, index, newMembers, index + 1, members.length - index); 111 newMembers[index] = member; 112 members = newMembers; 113 member.getMember().addReferrer(this); 114 member.getMember().clearCachedStyle(); 115 fireMembersChanged(); 116 } finally { 117 writeUnlock(locked); 118 } 119 } 120 121 /** 122 * Replace member at position specified by index. 123 * @param index index (positive integer) 124 * @param member relation member to set 125 * @return Member that was at the position 126 */ 127 public RelationMember setMember(int index, RelationMember member) { 128 boolean locked = writeLock(); 129 try { 130 RelationMember originalMember = members[index]; 131 members[index] = member; 132 if (originalMember.getMember() != member.getMember()) { 133 member.getMember().addReferrer(this); 134 member.getMember().clearCachedStyle(); 135 originalMember.getMember().removeReferrer(this); 136 originalMember.getMember().clearCachedStyle(); 137 fireMembersChanged(); 138 } 139 return originalMember; 140 } finally { 141 writeUnlock(locked); 142 } 143 } 144 145 /** 146 * Removes member at specified position. 147 * @param index index (positive integer) 148 * @return Member that was at the position 149 */ 150 public RelationMember removeMember(int index) { 151 boolean locked = writeLock(); 152 try { 153 List<RelationMember> members = getMembers(); 154 RelationMember result = members.remove(index); 155 setMembers(members); 156 return result; 157 } finally { 158 writeUnlock(locked); 159 } 160 } 161 162 @Override 163 public long getMemberId(int idx) { 164 return members[idx].getUniqueId(); 165 } 166 167 @Override 168 public String getRole(int idx) { 169 return members[idx].getRole(); 170 } 171 172 @Override 173 public OsmPrimitiveType getMemberType(int idx) { 174 return members[idx].getType(); 175 } 176 177 @Override 178 public void accept(Visitor visitor) { 179 visitor.visit(this); 180 } 181 182 @Override 183 public void accept(PrimitiveVisitor visitor) { 184 visitor.visit(this); 185 } 186 187 protected Relation(long id, boolean allowNegative) { 188 super(id, allowNegative); 189 } 190 191 /** 192 * Create a new relation with id 0 193 */ 194 public Relation() { 195 super(0, false); 196 } 197 198 /** 199 * Constructs an identical clone of the argument. 200 * @param clone The relation to clone 201 * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}. 202 * If {@code false}, does nothing 203 */ 204 public Relation(Relation clone, boolean clearMetadata) { 205 super(clone.getUniqueId(), true); 206 cloneFrom(clone); 207 if (clearMetadata) { 208 clearOsmMetadata(); 209 } 210 } 211 212 /** 213 * Create an identical clone of the argument (including the id) 214 * @param clone The relation to clone, including its id 215 */ 216 public Relation(Relation clone) { 217 this(clone, false); 218 } 219 220 /** 221 * Creates a new relation for the given id. If the id > 0, the way is marked 222 * as incomplete. 223 * 224 * @param id the id. > 0 required 225 * @throws IllegalArgumentException if id < 0 226 */ 227 public Relation(long id) { 228 super(id, false); 229 } 230 231 /** 232 * Creates new relation 233 * @param id the id 234 * @param version version number (positive integer) 235 */ 236 public Relation(long id, int version) { 237 super(id, version, false); 238 } 239 240 @Override 241 public void cloneFrom(OsmPrimitive osm) { 242 boolean locked = writeLock(); 243 try { 244 super.cloneFrom(osm); 245 // It's not necessary to clone members as RelationMember class is immutable 246 setMembers(((Relation) osm).getMembers()); 247 } finally { 248 writeUnlock(locked); 249 } 250 } 251 252 @Override 253 public void load(PrimitiveData data) { 254 boolean locked = writeLock(); 255 try { 256 super.load(data); 257 258 RelationData relationData = (RelationData) data; 259 260 List<RelationMember> newMembers = new ArrayList<>(); 261 for (RelationMemberData member : relationData.getMembers()) { 262 OsmPrimitive primitive = getDataSet().getPrimitiveById(member); 263 if (primitive == null) 264 throw new AssertionError("Data consistency problem - relation with missing member detected"); 265 newMembers.add(new RelationMember(member.getRole(), primitive)); 266 } 267 setMembers(newMembers); 268 } finally { 269 writeUnlock(locked); 270 } 271 } 272 273 @Override public RelationData save() { 274 RelationData data = new RelationData(); 275 saveCommonAttributes(data); 276 for (RelationMember member:getMembers()) { 277 data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember())); 278 } 279 return data; 280 } 281 282 @Override 283 public String toString() { 284 StringBuilder result = new StringBuilder(32); 285 result.append("{Relation id=") 286 .append(getUniqueId()) 287 .append(" version=") 288 .append(getVersion()) 289 .append(' ') 290 .append(getFlagsAsString()) 291 .append(" ["); 292 for (RelationMember rm:getMembers()) { 293 result.append(OsmPrimitiveType.from(rm.getMember())) 294 .append(' ') 295 .append(rm.getMember().getUniqueId()) 296 .append(", "); 297 } 298 result.delete(result.length()-2, result.length()) 299 .append("]}"); 300 return result.toString(); 301 } 302 303 @Override 304 public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) { 305 if (!(other instanceof Relation)) 306 return false; 307 if (!super.hasEqualSemanticAttributes(other, testInterestingTagsOnly)) 308 return false; 309 Relation r = (Relation) other; 310 return Arrays.equals(members, r.members); 311 } 312 313 @Override 314 public int compareTo(OsmPrimitive o) { 315 return o instanceof Relation ? Long.compare(getUniqueId(), o.getUniqueId()) : -1; 316 } 317 318 /** 319 * Returns the first member. 320 * @return first member, or {@code null} 321 */ 322 public RelationMember firstMember() { 323 return (isIncomplete() || members.length == 0) ? null : members[0]; 324 } 325 326 /** 327 * Returns the last member. 328 * @return last member, or {@code null} 329 */ 330 public RelationMember lastMember() { 331 return (isIncomplete() || members.length == 0) ? null : members[members.length - 1]; 332 } 333 334 /** 335 * removes all members with member.member == primitive 336 * 337 * @param primitive the primitive to check for 338 */ 339 public void removeMembersFor(OsmPrimitive primitive) { 340 removeMembersFor(Collections.singleton(primitive)); 341 } 342 343 @Override 344 public void setDeleted(boolean deleted) { 345 boolean locked = writeLock(); 346 try { 347 for (RelationMember rm:members) { 348 if (deleted) { 349 rm.getMember().removeReferrer(this); 350 } else { 351 rm.getMember().addReferrer(this); 352 } 353 } 354 super.setDeleted(deleted); 355 } finally { 356 writeUnlock(locked); 357 } 358 } 359 360 /** 361 * Obtains all members with member.member == primitive 362 * @param primitives the primitives to check for 363 * @return all relation members for the given primitives 364 */ 365 public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) { 366 return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember())); 367 } 368 369 /** 370 * removes all members with member.member == primitive 371 * 372 * @param primitives the primitives to check for 373 * @since 5613 374 */ 375 public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) { 376 if (primitives == null || primitives.isEmpty()) 377 return; 378 379 boolean locked = writeLock(); 380 try { 381 List<RelationMember> members = getMembers(); 382 members.removeAll(getMembersFor(primitives)); 383 setMembers(members); 384 } finally { 385 writeUnlock(locked); 386 } 387 } 388 389 @Override 390 public String getDisplayName(NameFormatter formatter) { 391 return formatter.format(this); 392 } 393 394 /** 395 * Replies the set of {@link OsmPrimitive}s referred to by at least one 396 * member of this relation 397 * 398 * @return the set of {@link OsmPrimitive}s referred to by at least one 399 * member of this relation 400 * @see #getMemberPrimitivesList() 401 */ 402 public Set<OsmPrimitive> getMemberPrimitives() { 403 return getMembers().stream().map(RelationMember::getMember).collect(Collectors.toSet()); 404 } 405 406 /** 407 * Returns the {@link OsmPrimitive}s of the specified type referred to by at least one member of this relation. 408 * @param tClass the type of the primitive 409 * @param <T> the type of the primitive 410 * @return the primitives 411 */ 412 public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) { 413 return Utils.filteredCollection(getMemberPrimitivesList(), tClass); 414 } 415 416 /** 417 * Returns an unmodifiable list of the {@link OsmPrimitive}s referred to by at least one member of this relation. 418 * @return an unmodifiable list of the primitives 419 */ 420 public List<OsmPrimitive> getMemberPrimitivesList() { 421 return Utils.transform(getMembers(), RelationMember::getMember); 422 } 423 424 @Override 425 public OsmPrimitiveType getType() { 426 return OsmPrimitiveType.RELATION; 427 } 428 429 @Override 430 public OsmPrimitiveType getDisplayType() { 431 return isMultipolygon() && !isBoundary() ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION; 432 } 433 434 /** 435 * Determines if this relation is a boundary. 436 * @return {@code true} if a boundary relation 437 */ 438 public boolean isBoundary() { 439 return "boundary".equals(get("type")); 440 } 441 442 @Override 443 public boolean isMultipolygon() { 444 return "multipolygon".equals(get("type")) || isBoundary(); 445 } 446 447 @Override 448 public BBox getBBox() { 449 RelationMember[] members = this.members; 450 451 if (members.length == 0) 452 return new BBox(0, 0, 0, 0); 453 if (getDataSet() == null) 454 return calculateBBox(new HashSet<PrimitiveId>()); 455 else { 456 if (bbox == null) { 457 bbox = calculateBBox(new HashSet<PrimitiveId>()); 458 } 459 if (bbox == null) 460 return new BBox(0, 0, 0, 0); // No real members 461 else 462 return new BBox(bbox); 463 } 464 } 465 466 private BBox calculateBBox(Set<PrimitiveId> visitedRelations) { 467 if (visitedRelations.contains(this)) 468 return null; 469 visitedRelations.add(this); 470 471 RelationMember[] members = this.members; 472 if (members.length == 0) 473 return null; 474 else { 475 BBox result = null; 476 for (RelationMember rm:members) { 477 BBox box = rm.isRelation() ? rm.getRelation().calculateBBox(visitedRelations) : rm.getMember().getBBox(); 478 if (box != null) { 479 if (result == null) { 480 result = box; 481 } else { 482 result.add(box); 483 } 484 } 485 } 486 return result; 487 } 488 } 489 490 @Override 491 public void updatePosition() { 492 bbox = calculateBBox(new HashSet<PrimitiveId>()); 493 } 494 495 @Override 496 void setDataset(DataSet dataSet) { 497 super.setDataset(dataSet); 498 checkMembers(); 499 bbox = null; // bbox might have changed if relation was in ds, was removed, modified, added back to dataset 500 } 501 502 /** 503 * Checks that members are part of the same dataset, and that they're not deleted. 504 * @throws DataIntegrityProblemException if one the above conditions is not met 505 */ 506 private void checkMembers() { 507 DataSet dataSet = getDataSet(); 508 if (dataSet != null) { 509 RelationMember[] members = this.members; 510 for (RelationMember rm: members) { 511 if (rm.getMember().getDataSet() != dataSet) 512 throw new DataIntegrityProblemException( 513 String.format("Relation member must be part of the same dataset as relation(%s, %s)", 514 getPrimitiveId(), rm.getMember().getPrimitiveId())); 515 } 516 if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) { 517 for (RelationMember rm: members) { 518 if (rm.getMember().isDeleted()) 519 throw new DataIntegrityProblemException("Deleted member referenced: " + toString()); 520 } 521 } 522 } 523 } 524 525 /** 526 * Fires the {@code RelationMembersChangedEvent} to listeners. 527 * @throws DataIntegrityProblemException if members are not valid 528 * @see #checkMembers 529 */ 530 private void fireMembersChanged() { 531 checkMembers(); 532 if (getDataSet() != null) { 533 getDataSet().fireRelationMembersChanged(this); 534 } 535 } 536 537 /** 538 * Determines if at least one child primitive is incomplete. 539 * 540 * @return true if at least one child primitive is incomplete 541 */ 542 public boolean hasIncompleteMembers() { 543 RelationMember[] members = this.members; 544 for (RelationMember rm: members) { 545 if (rm.getMember().isIncomplete()) return true; 546 } 547 return false; 548 } 549 550 /** 551 * Replies a collection with the incomplete children this relation refers to. 552 * 553 * @return the incomplete children. Empty collection if no children are incomplete. 554 */ 555 public Collection<OsmPrimitive> getIncompleteMembers() { 556 Set<OsmPrimitive> ret = new HashSet<>(); 557 RelationMember[] members = this.members; 558 for (RelationMember rm: members) { 559 if (!rm.getMember().isIncomplete()) { 560 continue; 561 } 562 ret.add(rm.getMember()); 563 } 564 return ret; 565 } 566 567 @Override 568 protected void keysChangedImpl(Map<String, String> originalKeys) { 569 super.keysChangedImpl(originalKeys); 570 for (OsmPrimitive member : getMemberPrimitivesList()) { 571 member.clearCachedStyle(); 572 } 573 } 574 575 @Override 576 public boolean concernsArea() { 577 return isMultipolygon() && hasAreaTags(); 578 } 579 580 @Override 581 public boolean isOutsideDownloadArea() { 582 return false; 583 } 584 585 /** 586 * Returns the set of roles used in this relation. 587 * @return the set of roles used in this relation. Can be empty but never null 588 * @since 7556 589 */ 590 public Set<String> getMemberRoles() { 591 Set<String> result = new HashSet<>(); 592 for (RelationMember rm : members) { 593 String role = rm.getRole(); 594 if (!role.isEmpty()) { 595 result.add(role); 596 } 597 } 598 return result; 599 } 600}