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 */ 352 public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) { 353 return Utils.filter(getMembers(), new Predicate<RelationMember>() { 354 @Override 355 public boolean evaluate(RelationMember member) { 356 return primitives.contains(member.getMember()); 357 } 358 }); 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 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 @Override 382 public String getDisplayName(NameFormatter formatter) { 383 return formatter.format(this); 384 } 385 386 /** 387 * Replies the set of {@link OsmPrimitive}s referred to by at least one 388 * member of this relation 389 * 390 * @return the set of {@link OsmPrimitive}s referred to by at least one 391 * member of this relation 392 */ 393 public Set<OsmPrimitive> getMemberPrimitives() { 394 Set<OsmPrimitive> ret = new HashSet<>(); 395 RelationMember[] members = this.members; 396 for (RelationMember m: members) { 397 if (m.getMember() != null) { 398 ret.add(m.getMember()); 399 } 400 } 401 return ret; 402 } 403 404 public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) { 405 return Utils.filteredCollection(getMemberPrimitives(), tClass); 406 } 407 408 public List<OsmPrimitive> getMemberPrimitivesList() { 409 return Utils.transform(getMembers(), new Utils.Function<RelationMember, OsmPrimitive>() { 410 @Override 411 public OsmPrimitive apply(RelationMember x) { 412 return x.getMember(); 413 } 414 }); 415 } 416 417 @Override 418 public OsmPrimitiveType getType() { 419 return OsmPrimitiveType.RELATION; 420 } 421 422 @Override 423 public OsmPrimitiveType getDisplayType() { 424 return isMultipolygon() ? OsmPrimitiveType.MULTIPOLYGON 425 : OsmPrimitiveType.RELATION; 426 } 427 428 public boolean isMultipolygon() { 429 return "multipolygon".equals(get("type")) || "boundary".equals(get("type")); 430 } 431 432 @Override 433 public BBox getBBox() { 434 RelationMember[] members = this.members; 435 436 if (members.length == 0) 437 return new BBox(0, 0, 0, 0); 438 if (getDataSet() == null) 439 return calculateBBox(new HashSet<PrimitiveId>()); 440 else { 441 if (bbox == null) { 442 bbox = calculateBBox(new HashSet<PrimitiveId>()); 443 } 444 if (bbox == null) 445 return new BBox(0, 0, 0, 0); // No real members 446 else 447 return new BBox(bbox); 448 } 449 } 450 451 private BBox calculateBBox(Set<PrimitiveId> visitedRelations) { 452 if (visitedRelations.contains(this)) 453 return null; 454 visitedRelations.add(this); 455 456 RelationMember[] members = this.members; 457 if (members.length == 0) 458 return null; 459 else { 460 BBox result = null; 461 for (RelationMember rm:members) { 462 BBox box = rm.isRelation() ? rm.getRelation().calculateBBox(visitedRelations) : rm.getMember().getBBox(); 463 if (box != null) { 464 if (result == null) { 465 result = box; 466 } else { 467 result.add(box); 468 } 469 } 470 } 471 return result; 472 } 473 } 474 475 @Override 476 public void updatePosition() { 477 bbox = calculateBBox(new HashSet<PrimitiveId>()); 478 } 479 480 @Override 481 void setDataset(DataSet dataSet) { 482 super.setDataset(dataSet); 483 checkMembers(); 484 bbox = null; // bbox might have changed if relation was in ds, was removed, modified, added back to dataset 485 } 486 487 private void checkMembers() throws DataIntegrityProblemException { 488 DataSet dataSet = getDataSet(); 489 if (dataSet != null) { 490 RelationMember[] members = this.members; 491 for (RelationMember rm: members) { 492 if (rm.getMember().getDataSet() != dataSet) 493 throw new DataIntegrityProblemException( 494 String.format("Relation member must be part of the same dataset as relation(%s, %s)", 495 getPrimitiveId(), rm.getMember().getPrimitiveId())); 496 } 497 if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) { 498 for (RelationMember rm: members) { 499 if (rm.getMember().isDeleted()) 500 throw new DataIntegrityProblemException("Deleted member referenced: " + toString()); 501 } 502 } 503 } 504 } 505 506 private void fireMembersChanged() throws DataIntegrityProblemException { 507 checkMembers(); 508 if (getDataSet() != null) { 509 getDataSet().fireRelationMembersChanged(this); 510 } 511 } 512 513 /** 514 * Determines if at least one child primitive is incomplete. 515 * 516 * @return true if at least one child primitive is incomplete 517 */ 518 public boolean hasIncompleteMembers() { 519 RelationMember[] members = this.members; 520 for (RelationMember rm: members) { 521 if (rm.getMember().isIncomplete()) return true; 522 } 523 return false; 524 } 525 526 /** 527 * Replies a collection with the incomplete children this relation refers to. 528 * 529 * @return the incomplete children. Empty collection if no children are incomplete. 530 */ 531 public Collection<OsmPrimitive> getIncompleteMembers() { 532 Set<OsmPrimitive> ret = new HashSet<>(); 533 RelationMember[] members = this.members; 534 for (RelationMember rm: members) { 535 if (!rm.getMember().isIncomplete()) { 536 continue; 537 } 538 ret.add(rm.getMember()); 539 } 540 return ret; 541 } 542 543 @Override 544 protected void keysChangedImpl(Map<String, String> originalKeys) { 545 super.keysChangedImpl(originalKeys); 546 for (OsmPrimitive member : getMemberPrimitives()) { 547 member.clearCachedStyle(); 548 } 549 } 550 551 @Override 552 public boolean concernsArea() { 553 return isMultipolygon() && hasAreaTags(); 554 } 555 556 @Override 557 public boolean isOutsideDownloadArea() { 558 return false; 559 } 560 561 /** 562 * Returns the set of roles used in this relation. 563 * @return the set of roles used in this relation. Can be empty but never null 564 * @since 7556 565 */ 566 public Set<String> getMemberRoles() { 567 Set<String> result = new HashSet<>(); 568 for (RelationMember rm : members) { 569 String role = rm.getRole(); 570 if (!role.isEmpty()) { 571 result.add(role); 572 } 573 } 574 return result; 575 } 576}