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