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