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