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