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.Optional; 012import java.util.Set; 013import java.util.stream.Collectors; 014 015import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 016import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 017import org.openstreetmap.josm.spi.preferences.Config; 018import org.openstreetmap.josm.tools.CopyList; 019import org.openstreetmap.josm.tools.SubclassFilteredCollection; 020import org.openstreetmap.josm.tools.Utils; 021 022/** 023 * A relation, having a set of tags and any number (0...n) of members. 024 * 025 * @author Frederik Ramm 026 */ 027public final class Relation extends OsmPrimitive implements IRelation { 028 029 private RelationMember[] members = new RelationMember[0]; 030 031 private BBox bbox; 032 033 /** 034 * @return Members of the relation. Changes made in returned list are not mapped 035 * back to the primitive, use setMembers() to modify the members 036 * @since 1925 037 */ 038 public List<RelationMember> getMembers() { 039 return new CopyList<>(members); 040 } 041 042 /** 043 * 044 * @param members Can be null, in that case all members are removed 045 * @since 1925 046 */ 047 public void setMembers(List<RelationMember> members) { 048 checkDatasetNotReadOnly(); 049 boolean locked = writeLock(); 050 try { 051 for (RelationMember rm : this.members) { 052 rm.getMember().removeReferrer(this); 053 rm.getMember().clearCachedStyle(); 054 } 055 056 if (members != null) { 057 this.members = members.toArray(new RelationMember[0]); 058 } else { 059 this.members = new RelationMember[0]; 060 } 061 for (RelationMember rm : this.members) { 062 rm.getMember().addReferrer(this); 063 rm.getMember().clearCachedStyle(); 064 } 065 066 fireMembersChanged(); 067 } finally { 068 writeUnlock(locked); 069 } 070 } 071 072 @Override 073 public int getMembersCount() { 074 return members.length; 075 } 076 077 /** 078 * Returns the relation member at the specified index. 079 * @param index the index of the relation member 080 * @return relation member at the specified index 081 */ 082 public RelationMember getMember(int index) { 083 return members[index]; 084 } 085 086 /** 087 * Adds the specified relation member at the last position. 088 * @param member the member to add 089 */ 090 public void addMember(RelationMember member) { 091 checkDatasetNotReadOnly(); 092 boolean locked = writeLock(); 093 try { 094 members = Utils.addInArrayCopy(members, member); 095 member.getMember().addReferrer(this); 096 member.getMember().clearCachedStyle(); 097 fireMembersChanged(); 098 } finally { 099 writeUnlock(locked); 100 } 101 } 102 103 /** 104 * Adds the specified relation member at the specified index. 105 * @param member the member to add 106 * @param index the index at which the specified element is to be inserted 107 */ 108 public void addMember(int index, RelationMember member) { 109 checkDatasetNotReadOnly(); 110 boolean locked = writeLock(); 111 try { 112 RelationMember[] newMembers = new RelationMember[members.length + 1]; 113 System.arraycopy(members, 0, newMembers, 0, index); 114 System.arraycopy(members, index, newMembers, index + 1, members.length - index); 115 newMembers[index] = member; 116 members = newMembers; 117 member.getMember().addReferrer(this); 118 member.getMember().clearCachedStyle(); 119 fireMembersChanged(); 120 } finally { 121 writeUnlock(locked); 122 } 123 } 124 125 /** 126 * Replace member at position specified by index. 127 * @param index index (positive integer) 128 * @param member relation member to set 129 * @return Member that was at the position 130 */ 131 public RelationMember setMember(int index, RelationMember member) { 132 checkDatasetNotReadOnly(); 133 boolean locked = writeLock(); 134 try { 135 RelationMember originalMember = members[index]; 136 members[index] = member; 137 if (originalMember.getMember() != member.getMember()) { 138 member.getMember().addReferrer(this); 139 member.getMember().clearCachedStyle(); 140 originalMember.getMember().removeReferrer(this); 141 originalMember.getMember().clearCachedStyle(); 142 fireMembersChanged(); 143 } 144 return originalMember; 145 } finally { 146 writeUnlock(locked); 147 } 148 } 149 150 /** 151 * Removes member at specified position. 152 * @param index index (positive integer) 153 * @return Member that was at the position 154 */ 155 public RelationMember removeMember(int index) { 156 checkDatasetNotReadOnly(); 157 boolean locked = writeLock(); 158 try { 159 List<RelationMember> members = getMembers(); 160 RelationMember result = members.remove(index); 161 setMembers(members); 162 return result; 163 } finally { 164 writeUnlock(locked); 165 } 166 } 167 168 @Override 169 public long getMemberId(int idx) { 170 return members[idx].getUniqueId(); 171 } 172 173 @Override 174 public String getRole(int idx) { 175 return members[idx].getRole(); 176 } 177 178 @Override 179 public OsmPrimitiveType getMemberType(int idx) { 180 return members[idx].getType(); 181 } 182 183 @Override 184 public void accept(OsmPrimitiveVisitor visitor) { 185 visitor.visit(this); 186 } 187 188 @Override 189 public void accept(PrimitiveVisitor visitor) { 190 visitor.visit(this); 191 } 192 193 protected Relation(long id, boolean allowNegative) { 194 super(id, allowNegative); 195 } 196 197 /** 198 * Create a new relation with id 0 199 */ 200 public Relation() { 201 super(0, false); 202 } 203 204 /** 205 * Constructs an identical clone of the argument. 206 * @param clone The relation to clone 207 * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}. 208 * If {@code false}, does nothing 209 */ 210 public Relation(Relation clone, boolean clearMetadata) { 211 super(clone.getUniqueId(), true); 212 cloneFrom(clone); 213 if (clearMetadata) { 214 clearOsmMetadata(); 215 } 216 } 217 218 /** 219 * Create an identical clone of the argument (including the id) 220 * @param clone The relation to clone, including its id 221 */ 222 public Relation(Relation clone) { 223 this(clone, false); 224 } 225 226 /** 227 * Creates a new relation for the given id. If the id > 0, the way is marked 228 * as incomplete. 229 * 230 * @param id the id. > 0 required 231 * @throws IllegalArgumentException if id < 0 232 */ 233 public Relation(long id) { 234 super(id, false); 235 } 236 237 /** 238 * Creates new relation 239 * @param id the id 240 * @param version version number (positive integer) 241 */ 242 public Relation(long id, int version) { 243 super(id, version, false); 244 } 245 246 @Override 247 public void cloneFrom(OsmPrimitive osm) { 248 if (!(osm instanceof Relation)) 249 throw new IllegalArgumentException("Not a relation: " + osm); 250 boolean locked = writeLock(); 251 try { 252 super.cloneFrom(osm); 253 // It's not necessary to clone members as RelationMember class is immutable 254 setMembers(((Relation) osm).getMembers()); 255 } finally { 256 writeUnlock(locked); 257 } 258 } 259 260 @Override 261 public void load(PrimitiveData data) { 262 if (!(data instanceof RelationData)) 263 throw new IllegalArgumentException("Not a relation data: " + data); 264 boolean locked = writeLock(); 265 try { 266 super.load(data); 267 268 RelationData relationData = (RelationData) data; 269 270 List<RelationMember> newMembers = new ArrayList<>(); 271 for (RelationMemberData member : relationData.getMembers()) { 272 newMembers.add(new RelationMember(member.getRole(), Optional.ofNullable(getDataSet().getPrimitiveById(member)) 273 .orElseThrow(() -> new AssertionError("Data consistency problem - relation with missing member detected")))); 274 } 275 setMembers(newMembers); 276 } finally { 277 writeUnlock(locked); 278 } 279 } 280 281 @Override 282 public RelationData save() { 283 RelationData data = new RelationData(); 284 saveCommonAttributes(data); 285 for (RelationMember member:getMembers()) { 286 data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember())); 287 } 288 return data; 289 } 290 291 @Override 292 public String toString() { 293 StringBuilder result = new StringBuilder(32); 294 result.append("{Relation id=") 295 .append(getUniqueId()) 296 .append(" version=") 297 .append(getVersion()) 298 .append(' ') 299 .append(getFlagsAsString()) 300 .append(" ["); 301 for (RelationMember rm:getMembers()) { 302 result.append(OsmPrimitiveType.from(rm.getMember())) 303 .append(' ') 304 .append(rm.getMember().getUniqueId()) 305 .append(", "); 306 } 307 result.delete(result.length()-2, result.length()) 308 .append("]}"); 309 return result.toString(); 310 } 311 312 @Override 313 public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) { 314 return (other instanceof Relation) 315 && hasEqualSemanticFlags(other) 316 && Arrays.equals(members, ((Relation) other).members) 317 && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly); 318 } 319 320 @Override 321 public int compareTo(OsmPrimitive o) { 322 return o instanceof Relation ? Long.compare(getUniqueId(), o.getUniqueId()) : -1; 323 } 324 325 /** 326 * Returns the first member. 327 * @return first member, or {@code null} 328 */ 329 public RelationMember firstMember() { 330 return (isIncomplete() || members.length == 0) ? null : members[0]; 331 } 332 333 /** 334 * Returns the last member. 335 * @return last member, or {@code null} 336 */ 337 public RelationMember lastMember() { 338 return (isIncomplete() || members.length == 0) ? null : members[members.length - 1]; 339 } 340 341 /** 342 * removes all members with member.member == primitive 343 * 344 * @param primitive the primitive to check for 345 */ 346 public void removeMembersFor(OsmPrimitive primitive) { 347 removeMembersFor(Collections.singleton(primitive)); 348 } 349 350 @Override 351 public void setDeleted(boolean deleted) { 352 boolean locked = writeLock(); 353 try { 354 for (RelationMember rm:members) { 355 if (deleted) { 356 rm.getMember().removeReferrer(this); 357 } else { 358 rm.getMember().addReferrer(this); 359 } 360 } 361 super.setDeleted(deleted); 362 } finally { 363 writeUnlock(locked); 364 } 365 } 366 367 /** 368 * Obtains all members with member.member == primitive 369 * @param primitives the primitives to check for 370 * @return all relation members for the given primitives 371 */ 372 public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) { 373 return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember())); 374 } 375 376 /** 377 * removes all members with member.member == primitive 378 * 379 * @param primitives the primitives to check for 380 * @since 5613 381 */ 382 public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) { 383 checkDatasetNotReadOnly(); 384 if (primitives == null || primitives.isEmpty()) 385 return; 386 387 boolean locked = writeLock(); 388 try { 389 List<RelationMember> members = getMembers(); 390 members.removeAll(getMembersFor(primitives)); 391 setMembers(members); 392 } finally { 393 writeUnlock(locked); 394 } 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 (Config.getPref().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 @Override 523 public boolean hasIncompleteMembers() { 524 RelationMember[] members = this.members; 525 for (RelationMember rm: members) { 526 if (rm.getMember().isIncomplete()) return true; 527 } 528 return false; 529 } 530 531 /** 532 * Replies a collection with the incomplete children this relation refers to. 533 * 534 * @return the incomplete children. Empty collection if no children are incomplete. 535 */ 536 public Collection<OsmPrimitive> getIncompleteMembers() { 537 Set<OsmPrimitive> ret = new HashSet<>(); 538 RelationMember[] members = this.members; 539 for (RelationMember rm: members) { 540 if (!rm.getMember().isIncomplete()) { 541 continue; 542 } 543 ret.add(rm.getMember()); 544 } 545 return ret; 546 } 547 548 @Override 549 protected void keysChangedImpl(Map<String, String> originalKeys) { 550 super.keysChangedImpl(originalKeys); 551 for (OsmPrimitive member : getMemberPrimitivesList()) { 552 member.clearCachedStyle(); 553 } 554 } 555 556 @Override 557 public boolean concernsArea() { 558 return isMultipolygon() && hasAreaTags(); 559 } 560 561 @Override 562 public boolean isOutsideDownloadArea() { 563 return false; 564 } 565 566 /** 567 * Returns the set of roles used in this relation. 568 * @return the set of roles used in this relation. Can be empty but never null 569 * @since 7556 570 */ 571 public Set<String> getMemberRoles() { 572 Set<String> result = new HashSet<>(); 573 for (RelationMember rm : members) { 574 String role = rm.getRole(); 575 if (!role.isEmpty()) { 576 result.add(role); 577 } 578 } 579 return result; 580 } 581}