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