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 112 * @param member 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 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}. If {@code false}, does nothing 190 */ 191 public Relation(Relation clone, boolean clearMetadata) { 192 super(clone.getUniqueId(), true); 193 cloneFrom(clone); 194 if (clearMetadata) { 195 clearOsmMetadata(); 196 } 197 } 198 199 /** 200 * Create an identical clone of the argument (including the id) 201 * @param clone The relation to clone, including its id 202 */ 203 public Relation(Relation clone) { 204 this(clone, false); 205 } 206 207 /** 208 * Creates a new relation for the given id. If the id > 0, the way is marked 209 * as incomplete. 210 * 211 * @param id the id. > 0 required 212 * @throws IllegalArgumentException thrown if id < 0 213 */ 214 public Relation(long id) throws IllegalArgumentException { 215 super(id, false); 216 } 217 218 /** 219 * Creates new relation 220 * @param id 221 * @param version 222 */ 223 public Relation(long id, int version) { 224 super(id, version, false); 225 } 226 227 @Override 228 public void cloneFrom(OsmPrimitive osm) { 229 boolean locked = writeLock(); 230 try { 231 super.cloneFrom(osm); 232 // It's not necessary to clone members as RelationMember class is immutable 233 setMembers(((Relation)osm).getMembers()); 234 } finally { 235 writeUnlock(locked); 236 } 237 } 238 239 @Override 240 public void load(PrimitiveData data) { 241 boolean locked = writeLock(); 242 try { 243 super.load(data); 244 245 RelationData relationData = (RelationData) data; 246 247 List<RelationMember> newMembers = new ArrayList<>(); 248 for (RelationMemberData member : relationData.getMembers()) { 249 OsmPrimitive primitive = getDataSet().getPrimitiveById(member); 250 if (primitive == null) 251 throw new AssertionError("Data consistency problem - relation with missing member detected"); 252 newMembers.add(new RelationMember(member.getRole(), primitive)); 253 } 254 setMembers(newMembers); 255 } finally { 256 writeUnlock(locked); 257 } 258 } 259 260 @Override public RelationData save() { 261 RelationData data = new RelationData(); 262 saveCommonAttributes(data); 263 for (RelationMember member:getMembers()) { 264 data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember())); 265 } 266 return data; 267 } 268 269 @Override public String toString() { 270 StringBuilder result = new StringBuilder(); 271 result.append("{Relation id="); 272 result.append(getUniqueId()); 273 result.append(" version="); 274 result.append(getVersion()); 275 result.append(" "); 276 result.append(getFlagsAsString()); 277 result.append(" ["); 278 for (RelationMember rm:getMembers()) { 279 result.append(OsmPrimitiveType.from(rm.getMember())); 280 result.append(" "); 281 result.append(rm.getMember().getUniqueId()); 282 result.append(", "); 283 } 284 result.delete(result.length()-2, result.length()); 285 result.append("]"); 286 result.append("}"); 287 return result.toString(); 288 } 289 290 @Override 291 public boolean hasEqualSemanticAttributes(OsmPrimitive other) { 292 if (!(other instanceof Relation)) 293 return false; 294 if (! super.hasEqualSemanticAttributes(other)) 295 return false; 296 Relation r = (Relation)other; 297 return Arrays.equals(members, r.members); 298 } 299 300 @Override 301 public int compareTo(OsmPrimitive o) { 302 return o instanceof Relation ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : -1; 303 } 304 305 public RelationMember firstMember() { 306 if (isIncomplete()) return null; 307 308 RelationMember[] members = this.members; 309 return (members.length == 0) ? null : members[0]; 310 } 311 public RelationMember lastMember() { 312 if (isIncomplete()) return null; 313 314 RelationMember[] members = this.members; 315 return (members.length == 0) ? null : members[members.length - 1]; 316 } 317 318 /** 319 * removes all members with member.member == primitive 320 * 321 * @param primitive the primitive to check for 322 */ 323 public void removeMembersFor(OsmPrimitive primitive) { 324 removeMembersFor(Collections.singleton(primitive)); 325 } 326 327 @Override 328 public void setDeleted(boolean deleted) { 329 boolean locked = writeLock(); 330 try { 331 for (RelationMember rm:members) { 332 if (deleted) { 333 rm.getMember().removeReferrer(this); 334 } else { 335 rm.getMember().addReferrer(this); 336 } 337 } 338 super.setDeleted(deleted); 339 } finally { 340 writeUnlock(locked); 341 } 342 } 343 344 /** 345 * Obtains all members with member.member == primitive 346 * @param primitives the primitives to check for 347 */ 348 public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) { 349 return Utils.filter(getMembers(), new Predicate<RelationMember>() { 350 @Override 351 public boolean evaluate(RelationMember member) { 352 return primitives.contains(member.getMember()); 353 } 354 }); 355 } 356 357 /** 358 * removes all members with member.member == primitive 359 * 360 * @param primitives the primitives to check for 361 * @since 5613 362 */ 363 public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) { 364 if (primitives == null || primitives.isEmpty()) 365 return; 366 367 boolean locked = writeLock(); 368 try { 369 List<RelationMember> members = getMembers(); 370 members.removeAll(getMembersFor(primitives)); 371 setMembers(members); 372 } finally { 373 writeUnlock(locked); 374 } 375 } 376 377 @Override 378 public String getDisplayName(NameFormatter formatter) { 379 return formatter.format(this); 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 */ 389 public Set<OsmPrimitive> getMemberPrimitives() { 390 HashSet<OsmPrimitive> ret = new HashSet<>(); 391 RelationMember[] members = this.members; 392 for (RelationMember m: members) { 393 if (m.getMember() != null) { 394 ret.add(m.getMember()); 395 } 396 } 397 return ret; 398 } 399 400 public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) { 401 return Utils.filteredCollection(getMemberPrimitives(), tClass); 402 } 403 404 public List<OsmPrimitive> getMemberPrimitivesList() { 405 return Utils.transform(getMembers(), new Utils.Function<RelationMember, OsmPrimitive>() { 406 @Override 407 public OsmPrimitive apply(RelationMember x) { 408 return x.getMember(); 409 } 410 }); 411 } 412 413 @Override 414 public OsmPrimitiveType getType() { 415 return OsmPrimitiveType.RELATION; 416 } 417 418 @Override 419 public OsmPrimitiveType getDisplayType() { 420 return isMultipolygon() ? OsmPrimitiveType.MULTIPOLYGON 421 : OsmPrimitiveType.RELATION; 422 } 423 424 public boolean isMultipolygon() { 425 return "multipolygon".equals(get("type")) || "boundary".equals(get("type")); 426 } 427 428 @Override 429 public BBox getBBox() { 430 RelationMember[] members = this.members; 431 432 if (members.length == 0) 433 return new BBox(0, 0, 0, 0); 434 if (getDataSet() == null) 435 return calculateBBox(new HashSet<PrimitiveId>()); 436 else { 437 if (bbox == null) { 438 bbox = calculateBBox(new HashSet<PrimitiveId>()); 439 } 440 if (bbox == null) 441 return new BBox(0, 0, 0, 0); // No real members 442 else 443 return new BBox(bbox); 444 } 445 } 446 447 private BBox calculateBBox(Set<PrimitiveId> visitedRelations) { 448 if (visitedRelations.contains(this)) 449 return null; 450 visitedRelations.add(this); 451 452 RelationMember[] members = this.members; 453 if (members.length == 0) 454 return null; 455 else { 456 BBox result = null; 457 for (RelationMember rm:members) { 458 BBox box = rm.isRelation()?rm.getRelation().calculateBBox(visitedRelations):rm.getMember().getBBox(); 459 if (box != null) { 460 if (result == null) { 461 result = box; 462 } else { 463 result.add(box); 464 } 465 } 466 } 467 return result; 468 } 469 } 470 471 @Override 472 public void updatePosition() { 473 bbox = calculateBBox(new HashSet<PrimitiveId>()); 474 } 475 476 @Override 477 public void setDataset(DataSet dataSet) { 478 super.setDataset(dataSet); 479 checkMembers(); 480 bbox = null; // bbox might have changed if relation was in ds, was removed, modified, added back to dataset 481 } 482 483 private void checkMembers() throws DataIntegrityProblemException { 484 DataSet dataSet = getDataSet(); 485 if (dataSet != null) { 486 RelationMember[] members = this.members; 487 for (RelationMember rm: members) { 488 if (rm.getMember().getDataSet() != dataSet) 489 throw new DataIntegrityProblemException(String.format("Relation member must be part of the same dataset as relation(%s, %s)", getPrimitiveId(), rm.getMember().getPrimitiveId())); 490 } 491 if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) { 492 for (RelationMember rm: members) { 493 if (rm.getMember().isDeleted()) 494 throw new DataIntegrityProblemException("Deleted member referenced: " + toString()); 495 } 496 } 497 } 498 } 499 500 private void fireMembersChanged() throws DataIntegrityProblemException { 501 checkMembers(); 502 if (getDataSet() != null) { 503 getDataSet().fireRelationMembersChanged(this); 504 } 505 } 506 507 /** 508 * Determines if at least one child primitive is incomplete. 509 * 510 * @return true if at least one child primitive is incomplete 511 */ 512 public boolean hasIncompleteMembers() { 513 RelationMember[] members = this.members; 514 for (RelationMember rm: members) { 515 if (rm.getMember().isIncomplete()) return true; 516 } 517 return false; 518 } 519 520 /** 521 * Replies a collection with the incomplete children this relation refers to. 522 * 523 * @return the incomplete children. Empty collection if no children are incomplete. 524 */ 525 public Collection<OsmPrimitive> getIncompleteMembers() { 526 Set<OsmPrimitive> ret = new HashSet<>(); 527 RelationMember[] members = this.members; 528 for (RelationMember rm: members) { 529 if (!rm.getMember().isIncomplete()) { 530 continue; 531 } 532 ret.add(rm.getMember()); 533 } 534 return ret; 535 } 536 537 @Override 538 protected void keysChangedImpl(Map<String, String> originalKeys) { 539 super.keysChangedImpl(originalKeys); 540 for (OsmPrimitive member : getMemberPrimitives()) { 541 member.clearCachedStyle(); 542 } 543 } 544 545 @Override 546 public boolean concernsArea() { 547 return isMultipolygon() && hasAreaTags(); 548 } 549 550 @Override 551 public boolean isOutsideDownloadArea() { 552 return false; 553 } 554 555 /** 556 * Returns the set of roles used in this relation. 557 * @return the set of roles used in this relation. Can be empty but never null 558 * @since 7556 559 */ 560 public Set<String> getMemberRoles() { 561 Set<String> result = new HashSet<>(); 562 for (RelationMember rm : members) { 563 String role = rm.getRole(); 564 if (!role.isEmpty()) { 565 result.add(role); 566 } 567 } 568 return result; 569 } 570}