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