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 * Sets members of the relation. 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 /** 321 * Returns the first member. 322 * @return first member, or {@code null} 323 */ 324 public RelationMember firstMember() { 325 return (isIncomplete() || members.length == 0) ? null : members[0]; 326 } 327 328 /** 329 * Returns the last member. 330 * @return last member, or {@code null} 331 */ 332 public RelationMember lastMember() { 333 return (isIncomplete() || members.length == 0) ? null : members[members.length - 1]; 334 } 335 336 /** 337 * removes all members with member.member == primitive 338 * 339 * @param primitive the primitive to check for 340 */ 341 public void removeMembersFor(OsmPrimitive primitive) { 342 removeMembersFor(Collections.singleton(primitive)); 343 } 344 345 @Override 346 public void setDeleted(boolean deleted) { 347 boolean locked = writeLock(); 348 try { 349 for (RelationMember rm:members) { 350 if (deleted) { 351 rm.getMember().removeReferrer(this); 352 } else { 353 rm.getMember().addReferrer(this); 354 } 355 } 356 super.setDeleted(deleted); 357 } finally { 358 writeUnlock(locked); 359 } 360 } 361 362 /** 363 * Obtains all members with member.member == primitive 364 * @param primitives the primitives to check for 365 * @return all relation members for the given primitives 366 */ 367 public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) { 368 return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember())); 369 } 370 371 /** 372 * removes all members with member.member == primitive 373 * 374 * @param primitives the primitives to check for 375 * @since 5613 376 */ 377 public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) { 378 checkDatasetNotReadOnly(); 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 /** 393 * Replies the set of {@link OsmPrimitive}s referred to by at least one 394 * member of this relation 395 * 396 * @return the set of {@link OsmPrimitive}s referred to by at least one 397 * member of this relation 398 * @see #getMemberPrimitivesList() 399 */ 400 public Set<OsmPrimitive> getMemberPrimitives() { 401 return getMembers().stream().map(RelationMember::getMember).collect(Collectors.toSet()); 402 } 403 404 /** 405 * Returns the {@link OsmPrimitive}s of the specified type referred to by at least one member of this relation. 406 * @param tClass the type of the primitive 407 * @param <T> the type of the primitive 408 * @return the primitives 409 */ 410 public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) { 411 return Utils.filteredCollection(getMemberPrimitivesList(), tClass); 412 } 413 414 /** 415 * Returns an unmodifiable list of the {@link OsmPrimitive}s referred to by at least one member of this relation. 416 * @return an unmodifiable list of the primitives 417 */ 418 public List<OsmPrimitive> getMemberPrimitivesList() { 419 return Utils.transform(getMembers(), RelationMember::getMember); 420 } 421 422 @Override 423 public OsmPrimitiveType getType() { 424 return OsmPrimitiveType.RELATION; 425 } 426 427 @Override 428 public OsmPrimitiveType getDisplayType() { 429 return isMultipolygon() && !isBoundary() ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION; 430 } 431 432 @Override 433 public BBox getBBox() { 434 if (getDataSet() != null && bbox != null) 435 return new BBox(bbox); // use cached value 436 437 BBox box = new BBox(); 438 addToBBox(box, new HashSet<PrimitiveId>()); 439 if (getDataSet() != null) 440 setBBox(box); // set cache 441 return new BBox(box); 442 } 443 444 private void setBBox(BBox bbox) { 445 this.bbox = bbox; 446 } 447 448 @Override 449 protected void addToBBox(BBox box, Set<PrimitiveId> visited) { 450 for (RelationMember rm : members) { 451 if (visited.add(rm.getMember())) 452 rm.getMember().addToBBox(box, visited); 453 } 454 } 455 456 @Override 457 public void updatePosition() { 458 setBBox(null); // make sure that it is recalculated 459 setBBox(getBBox()); 460 } 461 462 @Override 463 void setDataset(DataSet dataSet) { 464 super.setDataset(dataSet); 465 checkMembers(); 466 setBBox(null); // bbox might have changed if relation was in ds, was removed, modified, added back to dataset 467 } 468 469 /** 470 * Checks that members are part of the same dataset, and that they're not deleted. 471 * @throws DataIntegrityProblemException if one the above conditions is not met 472 */ 473 private void checkMembers() { 474 DataSet dataSet = getDataSet(); 475 if (dataSet != null) { 476 RelationMember[] members = this.members; 477 for (RelationMember rm: members) { 478 if (rm.getMember().getDataSet() != dataSet) 479 throw new DataIntegrityProblemException( 480 String.format("Relation member must be part of the same dataset as relation(%s, %s)", 481 getPrimitiveId(), rm.getMember().getPrimitiveId())); 482 } 483 if (Config.getPref().getBoolean("debug.checkDeleteReferenced", true)) { 484 for (RelationMember rm: members) { 485 if (rm.getMember().isDeleted()) 486 throw new DataIntegrityProblemException("Deleted member referenced: " + toString()); 487 } 488 } 489 } 490 } 491 492 /** 493 * Fires the {@code RelationMembersChangedEvent} to listeners. 494 * @throws DataIntegrityProblemException if members are not valid 495 * @see #checkMembers 496 */ 497 private void fireMembersChanged() { 498 checkMembers(); 499 if (getDataSet() != null) { 500 getDataSet().fireRelationMembersChanged(this); 501 } 502 } 503 504 @Override 505 public boolean hasIncompleteMembers() { 506 RelationMember[] members = this.members; 507 for (RelationMember rm: members) { 508 if (rm.getMember().isIncomplete()) return true; 509 } 510 return false; 511 } 512 513 /** 514 * Replies a collection with the incomplete children this relation refers to. 515 * 516 * @return the incomplete children. Empty collection if no children are incomplete. 517 */ 518 public Collection<OsmPrimitive> getIncompleteMembers() { 519 Set<OsmPrimitive> ret = new HashSet<>(); 520 RelationMember[] members = this.members; 521 for (RelationMember rm: members) { 522 if (!rm.getMember().isIncomplete()) { 523 continue; 524 } 525 ret.add(rm.getMember()); 526 } 527 return ret; 528 } 529 530 @Override 531 protected void keysChangedImpl(Map<String, String> originalKeys) { 532 super.keysChangedImpl(originalKeys); 533 for (OsmPrimitive member : getMemberPrimitivesList()) { 534 member.clearCachedStyle(); 535 } 536 } 537 538 @Override 539 public boolean concernsArea() { 540 return isMultipolygon() && hasAreaTags(); 541 } 542 543 @Override 544 public boolean isOutsideDownloadArea() { 545 return false; 546 } 547 548 /** 549 * Returns the set of roles used in this relation. 550 * @return the set of roles used in this relation. Can be empty but never null 551 * @since 7556 552 */ 553 public Set<String> getMemberRoles() { 554 Set<String> result = new HashSet<>(); 555 for (RelationMember rm : members) { 556 String role = rm.getRole(); 557 if (!role.isEmpty()) { 558 result.add(role); 559 } 560 } 561 return result; 562 } 563}