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