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 if (!(osm instanceof Relation)) 243 throw new IllegalArgumentException("Not a relation: " + osm); 244 boolean locked = writeLock(); 245 try { 246 super.cloneFrom(osm); 247 // It's not necessary to clone members as RelationMember class is immutable 248 setMembers(((Relation) osm).getMembers()); 249 } finally { 250 writeUnlock(locked); 251 } 252 } 253 254 @Override 255 public void load(PrimitiveData data) { 256 if (!(data instanceof RelationData)) 257 throw new IllegalArgumentException("Not a relation data: " + data); 258 boolean locked = writeLock(); 259 try { 260 super.load(data); 261 262 RelationData relationData = (RelationData) data; 263 264 List<RelationMember> newMembers = new ArrayList<>(); 265 for (RelationMemberData member : relationData.getMembers()) { 266 OsmPrimitive primitive = getDataSet().getPrimitiveById(member); 267 if (primitive == null) 268 throw new AssertionError("Data consistency problem - relation with missing member detected"); 269 newMembers.add(new RelationMember(member.getRole(), primitive)); 270 } 271 setMembers(newMembers); 272 } finally { 273 writeUnlock(locked); 274 } 275 } 276 277 @Override 278 public RelationData save() { 279 RelationData data = new RelationData(); 280 saveCommonAttributes(data); 281 for (RelationMember member:getMembers()) { 282 data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember())); 283 } 284 return data; 285 } 286 287 @Override 288 public String toString() { 289 StringBuilder result = new StringBuilder(32); 290 result.append("{Relation id=") 291 .append(getUniqueId()) 292 .append(" version=") 293 .append(getVersion()) 294 .append(' ') 295 .append(getFlagsAsString()) 296 .append(" ["); 297 for (RelationMember rm:getMembers()) { 298 result.append(OsmPrimitiveType.from(rm.getMember())) 299 .append(' ') 300 .append(rm.getMember().getUniqueId()) 301 .append(", "); 302 } 303 result.delete(result.length()-2, result.length()) 304 .append("]}"); 305 return result.toString(); 306 } 307 308 @Override 309 public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) { 310 return (other instanceof Relation) 311 && hasEqualSemanticFlags(other) 312 && Arrays.equals(members, ((Relation) other).members) 313 && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly); 314 } 315 316 @Override 317 public int compareTo(OsmPrimitive o) { 318 return o instanceof Relation ? Long.compare(getUniqueId(), o.getUniqueId()) : -1; 319 } 320 321 /** 322 * Returns the first member. 323 * @return first member, or {@code null} 324 */ 325 public RelationMember firstMember() { 326 return (isIncomplete() || members.length == 0) ? null : members[0]; 327 } 328 329 /** 330 * Returns the last member. 331 * @return last member, or {@code null} 332 */ 333 public RelationMember lastMember() { 334 return (isIncomplete() || members.length == 0) ? null : members[members.length - 1]; 335 } 336 337 /** 338 * removes all members with member.member == primitive 339 * 340 * @param primitive the primitive to check for 341 */ 342 public void removeMembersFor(OsmPrimitive primitive) { 343 removeMembersFor(Collections.singleton(primitive)); 344 } 345 346 @Override 347 public void setDeleted(boolean deleted) { 348 boolean locked = writeLock(); 349 try { 350 for (RelationMember rm:members) { 351 if (deleted) { 352 rm.getMember().removeReferrer(this); 353 } else { 354 rm.getMember().addReferrer(this); 355 } 356 } 357 super.setDeleted(deleted); 358 } finally { 359 writeUnlock(locked); 360 } 361 } 362 363 /** 364 * Obtains all members with member.member == primitive 365 * @param primitives the primitives to check for 366 * @return all relation members for the given primitives 367 */ 368 public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) { 369 return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember())); 370 } 371 372 /** 373 * removes all members with member.member == primitive 374 * 375 * @param primitives the primitives to check for 376 * @since 5613 377 */ 378 public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) { 379 if (primitives == null || primitives.isEmpty()) 380 return; 381 382 boolean locked = writeLock(); 383 try { 384 List<RelationMember> members = getMembers(); 385 members.removeAll(getMembersFor(primitives)); 386 setMembers(members); 387 } finally { 388 writeUnlock(locked); 389 } 390 } 391 392 @Override 393 public String getDisplayName(NameFormatter formatter) { 394 return formatter.format(this); 395 } 396 397 /** 398 * Replies the set of {@link OsmPrimitive}s referred to by at least one 399 * member of this relation 400 * 401 * @return the set of {@link OsmPrimitive}s referred to by at least one 402 * member of this relation 403 * @see #getMemberPrimitivesList() 404 */ 405 public Set<OsmPrimitive> getMemberPrimitives() { 406 return getMembers().stream().map(RelationMember::getMember).collect(Collectors.toSet()); 407 } 408 409 /** 410 * Returns the {@link OsmPrimitive}s of the specified type referred to by at least one member of this relation. 411 * @param tClass the type of the primitive 412 * @param <T> the type of the primitive 413 * @return the primitives 414 */ 415 public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) { 416 return Utils.filteredCollection(getMemberPrimitivesList(), tClass); 417 } 418 419 /** 420 * Returns an unmodifiable list of the {@link OsmPrimitive}s referred to by at least one member of this relation. 421 * @return an unmodifiable list of the primitives 422 */ 423 public List<OsmPrimitive> getMemberPrimitivesList() { 424 return Utils.transform(getMembers(), RelationMember::getMember); 425 } 426 427 @Override 428 public OsmPrimitiveType getType() { 429 return OsmPrimitiveType.RELATION; 430 } 431 432 @Override 433 public OsmPrimitiveType getDisplayType() { 434 return isMultipolygon() && !isBoundary() ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION; 435 } 436 437 /** 438 * Determines if this relation is a boundary. 439 * @return {@code true} if a boundary relation 440 */ 441 public boolean isBoundary() { 442 return "boundary".equals(get("type")); 443 } 444 445 @Override 446 public boolean isMultipolygon() { 447 return "multipolygon".equals(get("type")) || isBoundary(); 448 } 449 450 @Override 451 public BBox getBBox() { 452 if (getDataSet() != null && bbox != null) 453 return new BBox(bbox); // use cached value 454 455 BBox box = new BBox(); 456 addToBBox(box, new HashSet<PrimitiveId>()); 457 if (getDataSet() != null) 458 setBBox(box); // set cache 459 return new BBox(box); 460 } 461 462 private void setBBox(BBox bbox) { 463 this.bbox = bbox; 464 } 465 466 @Override 467 protected void addToBBox(BBox box, Set<PrimitiveId> visited) { 468 for (RelationMember rm : members) { 469 if (visited.add(rm.getMember())) 470 rm.getMember().addToBBox(box, visited); 471 } 472 } 473 474 @Override 475 public void updatePosition() { 476 setBBox(null); // make sure that it is recalculated 477 setBBox(getBBox()); 478 } 479 480 @Override 481 void setDataset(DataSet dataSet) { 482 super.setDataset(dataSet); 483 checkMembers(); 484 setBBox(null); // bbox might have changed if relation was in ds, was removed, modified, added back to dataset 485 } 486 487 /** 488 * Checks that members are part of the same dataset, and that they're not deleted. 489 * @throws DataIntegrityProblemException if one the above conditions is not met 490 */ 491 private void checkMembers() { 492 DataSet dataSet = getDataSet(); 493 if (dataSet != null) { 494 RelationMember[] members = this.members; 495 for (RelationMember rm: members) { 496 if (rm.getMember().getDataSet() != dataSet) 497 throw new DataIntegrityProblemException( 498 String.format("Relation member must be part of the same dataset as relation(%s, %s)", 499 getPrimitiveId(), rm.getMember().getPrimitiveId())); 500 } 501 if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) { 502 for (RelationMember rm: members) { 503 if (rm.getMember().isDeleted()) 504 throw new DataIntegrityProblemException("Deleted member referenced: " + toString()); 505 } 506 } 507 } 508 } 509 510 /** 511 * Fires the {@code RelationMembersChangedEvent} to listeners. 512 * @throws DataIntegrityProblemException if members are not valid 513 * @see #checkMembers 514 */ 515 private void fireMembersChanged() { 516 checkMembers(); 517 if (getDataSet() != null) { 518 getDataSet().fireRelationMembersChanged(this); 519 } 520 } 521 522 /** 523 * Determines if at least one child primitive is incomplete. 524 * 525 * @return true if at least one child primitive is incomplete 526 */ 527 public boolean hasIncompleteMembers() { 528 RelationMember[] members = this.members; 529 for (RelationMember rm: members) { 530 if (rm.getMember().isIncomplete()) return true; 531 } 532 return false; 533 } 534 535 /** 536 * Replies a collection with the incomplete children this relation refers to. 537 * 538 * @return the incomplete children. Empty collection if no children are incomplete. 539 */ 540 public Collection<OsmPrimitive> getIncompleteMembers() { 541 Set<OsmPrimitive> ret = new HashSet<>(); 542 RelationMember[] members = this.members; 543 for (RelationMember rm: members) { 544 if (!rm.getMember().isIncomplete()) { 545 continue; 546 } 547 ret.add(rm.getMember()); 548 } 549 return ret; 550 } 551 552 @Override 553 protected void keysChangedImpl(Map<String, String> originalKeys) { 554 super.keysChangedImpl(originalKeys); 555 for (OsmPrimitive member : getMemberPrimitivesList()) { 556 member.clearCachedStyle(); 557 } 558 } 559 560 @Override 561 public boolean concernsArea() { 562 return isMultipolygon() && hasAreaTags(); 563 } 564 565 @Override 566 public boolean isOutsideDownloadArea() { 567 return false; 568 } 569 570 /** 571 * Returns the set of roles used in this relation. 572 * @return the set of roles used in this relation. Can be empty but never null 573 * @since 7556 574 */ 575 public Set<String> getMemberRoles() { 576 Set<String> result = new HashSet<>(); 577 for (RelationMember rm : members) { 578 String role = rm.getRole(); 579 if (!role.isEmpty()) { 580 result.add(role); 581 } 582 } 583 return result; 584 } 585}