001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.text.MessageFormat; 007import java.util.Arrays; 008import java.util.Collection; 009import java.util.Collections; 010import java.util.Date; 011import java.util.List; 012import java.util.Locale; 013import java.util.Map; 014import java.util.Objects; 015import java.util.Set; 016import java.util.function.Consumer; 017import java.util.stream.Collectors; 018import java.util.stream.IntStream; 019import java.util.stream.Stream; 020 021import org.openstreetmap.josm.data.osm.search.SearchCompiler; 022import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match; 023import org.openstreetmap.josm.data.osm.search.SearchParseError; 024import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor; 025import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor; 026import org.openstreetmap.josm.gui.mappaint.StyleCache; 027import org.openstreetmap.josm.spi.preferences.Config; 028import org.openstreetmap.josm.tools.CheckParameterUtil; 029import org.openstreetmap.josm.tools.Logging; 030import org.openstreetmap.josm.tools.Utils; 031import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider; 032 033/** 034 * The base class for OSM objects ({@link Node}, {@link Way}, {@link Relation}). 035 * 036 * It can be created, deleted and uploaded to the OSM-Server. 037 * 038 * Although OsmPrimitive is designed as a base class, it is not to be meant to subclass 039 * it by any other than from the package {@link org.openstreetmap.josm.data.osm}. The available primitives are a fixed 040 * set that are given by the server environment and not an extendable data stuff. 041 * 042 * @author imi 043 */ 044public abstract class OsmPrimitive extends AbstractPrimitive implements TemplateEngineDataProvider { 045 private static final String SPECIAL_VALUE_ID = "id"; 046 private static final String SPECIAL_VALUE_LOCAL_NAME = "localname"; 047 048 /** 049 * A tagged way that matches this pattern has a direction. 050 * @see #FLAG_HAS_DIRECTIONS 051 */ 052 static volatile Match directionKeys; 053 054 /** 055 * A tagged way that matches this pattern has a direction that is reversed. 056 * <p> 057 * This pattern should be a subset of {@link #directionKeys} 058 * @see #FLAG_DIRECTION_REVERSED 059 */ 060 private static final Match reversedDirectionKeys; 061 062 static { 063 String reversedDirectionDefault = "oneway=\"-1\""; 064 065 String directionDefault = "oneway? | "+ 066 "(aerialway=chair_lift & -oneway=no) | "+ 067 "(aerialway=rope_tow & -oneway=no) | "+ 068 "(aerialway=magic_carpet & -oneway=no) | "+ 069 "(aerialway=zip_line & -oneway=no) | "+ 070 "(aerialway=drag_lift & -oneway=no) | "+ 071 "(aerialway=t-bar & -oneway=no) | "+ 072 "(aerialway=j-bar & -oneway=no) | "+ 073 "(aerialway=platter & -oneway=no) | "+ 074 "waterway=stream | waterway=river | waterway=ditch | waterway=drain | waterway=tidal_channel | "+ 075 "(\"piste:type\"=downhill & -area=yes) | (\"piste:type\"=sled & -area=yes) | (man_made=\"piste:halfpipe\" & -area=yes) | "+ 076 "(junction=circular & -oneway=no) | junction=roundabout | (highway=motorway & -oneway=no & -oneway=reversible) | "+ 077 "(highway=motorway_link & -oneway=no & -oneway=reversible)"; 078 079 reversedDirectionKeys = compileDirectionKeys("tags.reversed_direction", reversedDirectionDefault); 080 directionKeys = compileDirectionKeys("tags.direction", directionDefault); 081 } 082 083 /** 084 * Replies the collection of referring primitives for the primitives in <code>primitives</code>. 085 * 086 * @param primitives the collection of primitives. 087 * @return the collection of referring primitives for the primitives in <code>primitives</code>; 088 * empty set if primitives is null or if there are no referring primitives 089 */ 090 public static Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) { 091 return (primitives != null ? primitives.stream() : Stream.<OsmPrimitive>empty()) 092 .flatMap(p -> p.referrers(OsmPrimitive.class)) 093 .collect(Collectors.toSet()); 094 } 095 096 /** 097 * Creates a new primitive for the given id. 098 * 099 * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. 100 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or 101 * positive number. 102 * 103 * @param id the id 104 * @param allowNegativeId {@code true} to allow negative id 105 * @throws IllegalArgumentException if id < 0 and allowNegativeId is false 106 */ 107 protected OsmPrimitive(long id, boolean allowNegativeId) { 108 if (allowNegativeId) { 109 this.id = id; 110 } else { 111 if (id < 0) 112 throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id)); 113 else if (id == 0) { 114 this.id = getIdGenerator().generateUniqueId(); 115 } else { 116 this.id = id; 117 } 118 119 } 120 this.version = 0; 121 this.setIncomplete(id > 0); 122 } 123 124 /** 125 * Creates a new primitive for the given id and version. 126 * 127 * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. 128 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or 129 * positive number. 130 * 131 * If id is not > 0 version is ignored and set to 0. 132 * 133 * @param id the id 134 * @param version the version (positive integer) 135 * @param allowNegativeId {@code true} to allow negative id 136 * @throws IllegalArgumentException if id < 0 and allowNegativeId is false 137 */ 138 protected OsmPrimitive(long id, int version, boolean allowNegativeId) { 139 this(id, allowNegativeId); 140 this.version = id > 0 ? version : 0; 141 setIncomplete(id > 0 && version == 0); 142 } 143 144 /*---------- 145 * MAPPAINT 146 *--------*/ 147 private StyleCache mappaintStyle; 148 149 @Override 150 public final StyleCache getCachedStyle() { 151 return mappaintStyle; 152 } 153 154 @Override 155 public final void setCachedStyle(StyleCache mappaintStyle) { 156 this.mappaintStyle = mappaintStyle; 157 } 158 159 @Override 160 public final boolean isCachedStyleUpToDate() { 161 return mappaintStyle != null && mappaintCacheIdx == dataSet.getMappaintCacheIndex(); 162 } 163 164 @Override 165 public final void declareCachedStyleUpToDate() { 166 this.mappaintCacheIdx = dataSet.getMappaintCacheIndex(); 167 } 168 169 /* end of mappaint data */ 170 171 /*--------- 172 * DATASET 173 *---------*/ 174 175 /** the parent dataset */ 176 private DataSet dataSet; 177 178 /** 179 * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods 180 * @param dataSet the parent dataset 181 */ 182 void setDataset(DataSet dataSet) { 183 if (this.dataSet != null && dataSet != null && this.dataSet != dataSet) 184 throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset"); 185 this.dataSet = dataSet; 186 } 187 188 @Override 189 public DataSet getDataSet() { 190 return dataSet; 191 } 192 193 /** 194 * Throws exception if primitive is not part of the dataset 195 */ 196 public void checkDataset() { 197 if (dataSet == null) 198 throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString()); 199 } 200 201 /** 202 * Throws exception if primitive is in a read-only dataset 203 */ 204 protected final void checkDatasetNotReadOnly() { 205 if (dataSet != null && dataSet.isLocked()) 206 throw new DataIntegrityProblemException("Primitive cannot be modified in read-only dataset: " + toString()); 207 } 208 209 protected boolean writeLock() { 210 if (dataSet != null) { 211 dataSet.beginUpdate(); 212 return true; 213 } else 214 return false; 215 } 216 217 protected void writeUnlock(boolean locked) { 218 if (locked && dataSet != null) { 219 // It shouldn't be possible for dataset to become null because 220 // method calling setDataset would need write lock which is owned by this thread 221 dataSet.endUpdate(); 222 } 223 } 224 225 /** 226 * Sets the id and the version of this primitive if it is known to the OSM API. 227 * 228 * Since we know the id and its version it can't be incomplete anymore. incomplete 229 * is set to false. 230 * 231 * @param id the id. > 0 required 232 * @param version the version > 0 required 233 * @throws IllegalArgumentException if id <= 0 234 * @throws IllegalArgumentException if version <= 0 235 * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset 236 */ 237 @Override 238 public void setOsmId(long id, int version) { 239 checkDatasetNotReadOnly(); 240 boolean locked = writeLock(); 241 try { 242 if (id <= 0) 243 throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id)); 244 if (version <= 0) 245 throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version)); 246 if (dataSet != null && id != this.id) { 247 DataSet datasetCopy = dataSet; 248 // Reindex primitive 249 datasetCopy.removePrimitive(this); 250 this.id = id; 251 datasetCopy.addPrimitive(this); 252 } 253 super.setOsmId(id, version); 254 } finally { 255 writeUnlock(locked); 256 } 257 } 258 259 /** 260 * Clears the metadata, including id and version known to the OSM API. 261 * The id is a new unique id. The version, changeset and timestamp are set to 0. 262 * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead 263 * 264 * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@link DataSet}. 265 * 266 * @throws DataIntegrityProblemException If primitive was already added to the dataset 267 * @since 6140 268 */ 269 @Override 270 public void clearOsmMetadata() { 271 if (dataSet != null) 272 throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset"); 273 super.clearOsmMetadata(); 274 } 275 276 @Override 277 public void setUser(User user) { 278 checkDatasetNotReadOnly(); 279 boolean locked = writeLock(); 280 try { 281 super.setUser(user); 282 } finally { 283 writeUnlock(locked); 284 } 285 } 286 287 @Override 288 public void setChangesetId(int changesetId) { 289 checkDatasetNotReadOnly(); 290 boolean locked = writeLock(); 291 try { 292 int old = this.changesetId; 293 super.setChangesetId(changesetId); 294 if (dataSet != null) { 295 dataSet.fireChangesetIdChanged(this, old, changesetId); 296 } 297 } finally { 298 writeUnlock(locked); 299 } 300 } 301 302 @Deprecated 303 @Override 304 public void setTimestamp(Date timestamp) { 305 checkDatasetNotReadOnly(); 306 boolean locked = writeLock(); 307 try { 308 super.setTimestamp(timestamp); 309 } finally { 310 writeUnlock(locked); 311 } 312 } 313 314 /* ------- 315 /* FLAGS 316 /* ------*/ 317 318 private void updateFlagsNoLock(short flag, boolean value) { 319 super.updateFlags(flag, value); 320 } 321 322 @Override 323 protected final void updateFlags(short flag, boolean value) { 324 boolean locked = writeLock(); 325 try { 326 updateFlagsNoLock(flag, value); 327 } finally { 328 writeUnlock(locked); 329 } 330 } 331 332 @Override 333 public boolean setDisabledState(boolean hidden) { 334 boolean locked = writeLock(); 335 try { 336 return super.setDisabledState(hidden); 337 } finally { 338 writeUnlock(locked); 339 } 340 } 341 342 /** 343 * Remove the disabled flag from the primitive. 344 * Afterwards, the primitive is displayed normally and can be selected again. 345 * @return {@code true} if a change occurred 346 */ 347 @Override 348 public boolean unsetDisabledState() { 349 boolean locked = writeLock(); 350 try { 351 return super.unsetDisabledState(); 352 } finally { 353 writeUnlock(locked); 354 } 355 } 356 357 /** 358 * Set binary property used internally by the filter mechanism. 359 * @param isPreserved new "preserved" flag value 360 * @since 13309 361 */ 362 public void setPreserved(boolean isPreserved) { 363 updateFlags(FLAG_PRESERVED, isPreserved); 364 } 365 366 @Override 367 public boolean isDisabled() { 368 return (flags & FLAG_DISABLED) != 0; 369 } 370 371 @Override 372 public boolean isDisabledAndHidden() { 373 return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0); 374 } 375 376 @Override 377 public boolean isPreserved() { 378 return (flags & FLAG_PRESERVED) != 0; 379 } 380 381 @Override 382 public boolean isSelectable() { 383 // not synchronized -> check disabled twice just to be sure we did not have a race condition. 384 return !isDisabled() && isDrawable() && !isDisabled(); 385 } 386 387 @Override 388 public boolean isDrawable() { 389 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0; 390 } 391 392 @Override 393 public void setModified(boolean modified) { 394 checkDatasetNotReadOnly(); 395 boolean locked = writeLock(); 396 try { 397 super.setModified(modified); 398 if (dataSet != null) { 399 dataSet.firePrimitiveFlagsChanged(this); 400 } 401 clearCachedStyle(); 402 } finally { 403 writeUnlock(locked); 404 } 405 } 406 407 @Override 408 public void setVisible(boolean visible) { 409 checkDatasetNotReadOnly(); 410 boolean locked = writeLock(); 411 try { 412 super.setVisible(visible); 413 clearCachedStyle(); 414 } finally { 415 writeUnlock(locked); 416 } 417 } 418 419 @Override 420 public void setDeleted(boolean deleted) { 421 checkDatasetNotReadOnly(); 422 boolean locked = writeLock(); 423 try { 424 super.setDeleted(deleted); 425 if (dataSet != null) { 426 if (deleted) { 427 dataSet.firePrimitivesRemoved(Collections.singleton(this), false); 428 } else { 429 dataSet.firePrimitivesAdded(Collections.singleton(this), false); 430 } 431 } 432 clearCachedStyle(); 433 } finally { 434 writeUnlock(locked); 435 } 436 } 437 438 @Override 439 protected final void setIncomplete(boolean incomplete) { 440 checkDatasetNotReadOnly(); 441 boolean locked = writeLock(); 442 try { 443 if (dataSet != null && incomplete != this.isIncomplete()) { 444 if (incomplete) { 445 dataSet.firePrimitivesRemoved(Collections.singletonList(this), true); 446 } else { 447 dataSet.firePrimitivesAdded(Collections.singletonList(this), true); 448 } 449 } 450 super.setIncomplete(incomplete); 451 } finally { 452 writeUnlock(locked); 453 } 454 } 455 456 @Override 457 public boolean isSelected() { 458 return dataSet != null && dataSet.isSelected(this); 459 } 460 461 @Override 462 public boolean isMemberOfSelected() { 463 if (referrers == null) 464 return false; 465 if (referrers instanceof OsmPrimitive) 466 return referrers instanceof Relation && ((OsmPrimitive) referrers).isSelected(); 467 return Arrays.stream((OsmPrimitive[]) referrers) 468 .anyMatch(ref -> ref instanceof Relation && ref.isSelected()); 469 } 470 471 @Override 472 public boolean isOuterMemberOfSelected() { 473 if (referrers == null) 474 return false; 475 if (referrers instanceof OsmPrimitive) { 476 return isOuterMemberOfMultipolygon((OsmPrimitive) referrers); 477 } 478 return Arrays.stream((OsmPrimitive[]) referrers) 479 .anyMatch(this::isOuterMemberOfMultipolygon); 480 } 481 482 private boolean isOuterMemberOfMultipolygon(OsmPrimitive ref) { 483 return ref instanceof Relation 484 && ref.isSelected() 485 && ((Relation) ref).isMultipolygon() 486 && ((Relation) ref).getMembersFor(Collections.singleton(this)).stream() 487 .anyMatch(rm -> "outer".equals(rm.getRole())); 488 } 489 490 @Override 491 public void setHighlighted(boolean highlighted) { 492 if (isHighlighted() != highlighted) { 493 updateFlags(FLAG_HIGHLIGHTED, highlighted); 494 if (dataSet != null) { 495 dataSet.fireHighlightingChanged(); 496 } 497 } 498 } 499 500 @Override 501 public boolean isHighlighted() { 502 return (flags & FLAG_HIGHLIGHTED) != 0; 503 } 504 505 /*--------------- 506 * DIRECTION KEYS 507 *---------------*/ 508 509 private static Match compileDirectionKeys(String prefName, String defaultValue) throws AssertionError { 510 try { 511 return SearchCompiler.compile(Config.getPref().get(prefName, defaultValue)); 512 } catch (SearchParseError e) { 513 Logging.log(Logging.LEVEL_ERROR, "Unable to compile pattern for " + prefName + ", trying default pattern:", e); 514 } 515 516 try { 517 return SearchCompiler.compile(defaultValue); 518 } catch (SearchParseError e2) { 519 throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2); 520 } 521 } 522 523 private void updateTagged() { 524 // 'area' is not really uninteresting (putting it in that list may have unpredictable side effects) 525 // but it's clearly not enough to consider an object as tagged (see #9261) 526 updateFlagsNoLock(FLAG_TAGGED, hasKeys() && keys() 527 .anyMatch(key -> !isUninterestingKey(key) && !"area".equals(key))); 528 } 529 530 private void updateAnnotated() { 531 updateFlagsNoLock(FLAG_ANNOTATED, hasKeys() && getWorkInProgressKeys().stream().anyMatch(this::hasKey)); 532 } 533 534 @Override 535 public boolean isTagged() { 536 return (flags & FLAG_TAGGED) != 0; 537 } 538 539 @Override 540 public boolean isAnnotated() { 541 return (flags & FLAG_ANNOTATED) != 0; 542 } 543 544 protected void updateDirectionFlags() { 545 boolean hasDirections = false; 546 boolean directionReversed = false; 547 if (reversedDirectionKeys.match(this)) { 548 hasDirections = true; 549 directionReversed = true; 550 } 551 if (directionKeys.match(this)) { 552 hasDirections = true; 553 } 554 555 updateFlagsNoLock(FLAG_DIRECTION_REVERSED, directionReversed); 556 updateFlagsNoLock(FLAG_HAS_DIRECTIONS, hasDirections); 557 } 558 559 @Override 560 public boolean hasDirectionKeys() { 561 return (flags & FLAG_HAS_DIRECTIONS) != 0; 562 } 563 564 @Override 565 public boolean reversedDirection() { 566 return (flags & FLAG_DIRECTION_REVERSED) != 0; 567 } 568 569 /*------------ 570 * Keys handling 571 ------------*/ 572 573 @Override 574 public final void setKeys(TagMap keys) { 575 checkDatasetNotReadOnly(); 576 boolean locked = writeLock(); 577 try { 578 super.setKeys(keys); 579 } finally { 580 writeUnlock(locked); 581 } 582 } 583 584 @Override 585 public final void setKeys(Map<String, String> keys) { 586 checkDatasetNotReadOnly(); 587 boolean locked = writeLock(); 588 try { 589 super.setKeys(keys); 590 } finally { 591 writeUnlock(locked); 592 } 593 } 594 595 @Override 596 public final void put(String key, String value) { 597 checkDatasetNotReadOnly(); 598 boolean locked = writeLock(); 599 try { 600 super.put(key, value); 601 } finally { 602 writeUnlock(locked); 603 } 604 } 605 606 @Override 607 public final void remove(String key) { 608 checkDatasetNotReadOnly(); 609 boolean locked = writeLock(); 610 try { 611 super.remove(key); 612 } finally { 613 writeUnlock(locked); 614 } 615 } 616 617 @Override 618 public final void removeAll() { 619 checkDatasetNotReadOnly(); 620 boolean locked = writeLock(); 621 try { 622 super.removeAll(); 623 } finally { 624 writeUnlock(locked); 625 } 626 } 627 628 @Override 629 protected void keysChangedImpl(Map<String, String> originalKeys) { 630 clearCachedStyle(); 631 if (dataSet != null) { 632 for (OsmPrimitive ref : getReferrers()) { 633 ref.clearCachedStyle(); 634 } 635 } 636 updateDirectionFlags(); 637 updateTagged(); 638 updateAnnotated(); 639 if (dataSet != null) { 640 dataSet.fireTagsChanged(this, originalKeys); 641 } 642 } 643 644 /*------------ 645 * Referrers 646 ------------*/ 647 648 private Object referrers; 649 650 /** 651 * Add new referrer. If referrer is already included then no action is taken 652 * @param referrer The referrer to add 653 */ 654 protected void addReferrer(OsmPrimitive referrer) { 655 checkDatasetNotReadOnly(); 656 if (referrers == null) { 657 referrers = referrer; 658 } else if (referrers instanceof OsmPrimitive) { 659 if (referrers != referrer) { 660 referrers = new OsmPrimitive[] {(OsmPrimitive) referrers, referrer}; 661 } 662 } else { 663 for (OsmPrimitive primitive:(OsmPrimitive[]) referrers) { 664 if (primitive == referrer) 665 return; 666 } 667 referrers = Utils.addInArrayCopy((OsmPrimitive[]) referrers, referrer); 668 } 669 } 670 671 /** 672 * Remove referrer. No action is taken if referrer is not registered 673 * @param referrer The referrer to remove 674 */ 675 protected void removeReferrer(OsmPrimitive referrer) { 676 checkDatasetNotReadOnly(); 677 if (referrers instanceof OsmPrimitive) { 678 if (referrers == referrer) { 679 referrers = null; 680 } 681 } else if (referrers instanceof OsmPrimitive[]) { 682 OsmPrimitive[] orig = (OsmPrimitive[]) referrers; 683 int idx = IntStream.range(0, orig.length) 684 .filter(i -> orig[i] == referrer) 685 .findFirst().orElse(-1); 686 if (idx == -1) 687 return; 688 689 if (orig.length == 2) { 690 referrers = orig[1-idx]; // idx is either 0 or 1, take the other 691 } else { // downsize the array 692 OsmPrimitive[] smaller = new OsmPrimitive[orig.length-1]; 693 System.arraycopy(orig, 0, smaller, 0, idx); 694 System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx); 695 referrers = smaller; 696 } 697 } 698 } 699 700 private <T extends OsmPrimitive> Stream<T> referrers(boolean allowWithoutDataset, Class<T> filter) { 701 // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example 702 // when way is cloned 703 704 if (dataSet == null && allowWithoutDataset) { 705 return Stream.empty(); 706 } 707 checkDataset(); 708 if (referrers == null) { 709 return Stream.empty(); 710 } 711 final Stream<OsmPrimitive> stream = referrers instanceof OsmPrimitive // NOPMD 712 ? Stream.of((OsmPrimitive) referrers) 713 : Arrays.stream((OsmPrimitive[]) referrers); 714 return stream 715 .filter(p -> p.dataSet == dataSet) 716 .filter(filter::isInstance) 717 .map(filter::cast); 718 } 719 720 /** 721 * Gets all primitives in the current dataset that reference this primitive. 722 * @param filter restrict primitives to subclasses 723 * @param <T> type of primitives 724 * @return the referrers as Stream 725 * @since 14654 726 */ 727 public final <T extends OsmPrimitive> Stream<T> referrers(Class<T> filter) { 728 return referrers(false, filter); 729 } 730 731 @Override 732 public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) { 733 return referrers(allowWithoutDataset, OsmPrimitive.class) 734 .collect(Collectors.toList()); 735 } 736 737 @Override 738 public final List<OsmPrimitive> getReferrers() { 739 return getReferrers(false); 740 } 741 742 /** 743 * <p>Visits {@code visitor} for all referrers.</p> 744 * 745 * @param visitor the visitor. Ignored, if null. 746 * @since 12809 747 */ 748 public void visitReferrers(OsmPrimitiveVisitor visitor) { 749 if (visitor != null) 750 doVisitReferrers(o -> o.accept(visitor)); 751 } 752 753 @Override 754 public void visitReferrers(PrimitiveVisitor visitor) { 755 if (visitor != null) 756 doVisitReferrers(o -> o.accept(visitor)); 757 } 758 759 private void doVisitReferrers(Consumer<OsmPrimitive> visitor) { 760 if (this.referrers == null) 761 return; 762 else if (this.referrers instanceof OsmPrimitive) { 763 OsmPrimitive ref = (OsmPrimitive) this.referrers; 764 if (ref.dataSet == dataSet) { 765 visitor.accept(ref); 766 } 767 } else if (this.referrers instanceof OsmPrimitive[]) { 768 OsmPrimitive[] refs = (OsmPrimitive[]) this.referrers; 769 for (OsmPrimitive ref: refs) { 770 if (ref.dataSet == dataSet) { 771 visitor.accept(ref); 772 } 773 } 774 } 775 } 776 777 /** 778 * Return true, if this primitive is a node referred by at least n ways 779 * @param n Minimal number of ways to return true. Must be positive 780 * @return {@code true} if this primitive is referred by at least n ways 781 */ 782 protected final boolean isNodeReferredByWays(int n) { 783 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example 784 // when way is cloned 785 if (referrers == null) return false; 786 checkDataset(); 787 if (referrers instanceof OsmPrimitive) 788 return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet; 789 else { 790 int counter = 0; 791 for (OsmPrimitive o : (OsmPrimitive[]) referrers) { 792 if (dataSet == o.dataSet && o instanceof Way && ++counter >= n) 793 return true; 794 } 795 return false; 796 } 797 } 798 799 /*----------------- 800 * OTHER METHODS 801 *----------------*/ 802 803 /** 804 * Implementation of the visitor scheme. Subclasses have to call the correct 805 * visitor function. 806 * @param visitor The visitor from which the visit() function must be called. 807 * @since 12809 808 */ 809 public abstract void accept(OsmPrimitiveVisitor visitor); 810 811 /** 812 * Get and write all attributes from the parameter. Does not fire any listener, so 813 * use this only in the data initializing phase 814 * @param other other primitive 815 */ 816 public final void cloneFrom(OsmPrimitive other) { 817 cloneFrom(other, true); 818 } 819 820 /** 821 * Get and write all attributes from the parameter. Does not fire any listener, so 822 * use this only in the data initializing phase 823 * @param other other primitive 824 * @param copyChildren whether to copy child primitives too 825 * @since 16212 826 */ 827 protected void cloneFrom(OsmPrimitive other, boolean copyChildren) { 828 // write lock is provided by subclasses 829 if (id != other.id && dataSet != null) 830 throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset"); 831 832 super.cloneFrom(other); 833 clearCachedStyle(); 834 } 835 836 /** 837 * Merges the technical and semantic attributes from <code>other</code> onto this. 838 * 839 * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code> 840 * have an assigned OSM id, the IDs have to be the same. 841 * 842 * @param other the other primitive. Must not be null. 843 * @throws IllegalArgumentException if other is null. 844 * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not 845 * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId() 846 */ 847 public void mergeFrom(OsmPrimitive other) { 848 checkDatasetNotReadOnly(); 849 boolean locked = writeLock(); 850 try { 851 CheckParameterUtil.ensureParameterNotNull(other, "other"); 852 if (other.isNew() ^ isNew()) 853 throw new DataIntegrityProblemException( 854 tr("Cannot merge because either of the participating primitives is new and the other is not")); 855 if (!other.isNew() && other.getId() != id) 856 throw new DataIntegrityProblemException( 857 tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId())); 858 859 setKeys(other.hasKeys() ? other.getKeys() : null); 860 timestamp = other.timestamp; 861 version = other.version; 862 setIncomplete(other.isIncomplete()); 863 flags = other.flags; 864 user = other.user; 865 changesetId = other.changesetId; 866 } finally { 867 writeUnlock(locked); 868 } 869 } 870 871 /** 872 * Replies true if this primitive and other are equal with respect to their semantic attributes. 873 * <ol> 874 * <li>equal id</li> 875 * <li>both are complete or both are incomplete</li> 876 * <li>both have the same tags</li> 877 * </ol> 878 * @param other other primitive to compare 879 * @return true if this primitive and other are equal with respect to their semantic attributes. 880 */ 881 public final boolean hasEqualSemanticAttributes(OsmPrimitive other) { 882 return hasEqualSemanticAttributes(other, false); 883 } 884 885 boolean hasEqualSemanticFlags(final OsmPrimitive other) { 886 if (!isNew() && id != other.id) 887 return false; 888 return !(isIncomplete() ^ other.isIncomplete()); // exclusive or operator for performance (see #7159) 889 } 890 891 boolean hasEqualSemanticAttributes(final OsmPrimitive other, final boolean testInterestingTagsOnly) { 892 return hasEqualSemanticFlags(other) 893 && (testInterestingTagsOnly ? hasSameInterestingTags(other) : getKeys().equals(other.getKeys())); 894 } 895 896 /** 897 * Replies true if this primitive and other are equal with respect to their technical attributes. 898 * The attributes: 899 * <ol> 900 * <li>deleted</li> 901 * <li>modified</li> 902 * <li>timestamp</li> 903 * <li>version</li> 904 * <li>visible</li> 905 * <li>user</li> 906 * </ol> 907 * have to be equal 908 * @param other the other primitive 909 * @return true if this primitive and other are equal with respect to their technical attributes 910 */ 911 public boolean hasEqualTechnicalAttributes(OsmPrimitive other) { 912 // CHECKSTYLE.OFF: BooleanExpressionComplexity 913 return other != null 914 && timestamp == other.timestamp 915 && version == other.version 916 && changesetId == other.changesetId 917 && isDeleted() == other.isDeleted() 918 && isModified() == other.isModified() 919 && isVisible() == other.isVisible() 920 && Objects.equals(user, other.user); 921 // CHECKSTYLE.ON: BooleanExpressionComplexity 922 } 923 924 /** 925 * Loads (clone) this primitive from provided PrimitiveData 926 * @param data The object which should be cloned 927 */ 928 public void load(PrimitiveData data) { 929 checkDatasetNotReadOnly(); 930 // Write lock is provided by subclasses 931 setKeys(data.hasKeys() ? data.getKeys() : null); 932 setRawTimestamp(data.getRawTimestamp()); 933 user = data.getUser(); 934 setChangesetId(data.getChangesetId()); 935 setDeleted(data.isDeleted()); 936 setModified(data.isModified()); 937 setVisible(data.isVisible()); 938 setIncomplete(data.isIncomplete()); 939 version = data.getVersion(); 940 } 941 942 /** 943 * Save parameters of this primitive to the transport object 944 * @return The saved object data 945 */ 946 public abstract PrimitiveData save(); 947 948 /** 949 * Save common parameters of primitives to the transport object 950 * @param data The object to save the data into 951 */ 952 protected void saveCommonAttributes(PrimitiveData data) { 953 data.setId(id); 954 data.setKeys(hasKeys() ? getKeys() : null); 955 data.setRawTimestamp(getRawTimestamp()); 956 data.setUser(user); 957 data.setDeleted(isDeleted()); 958 data.setModified(isModified()); 959 data.setVisible(isVisible()); 960 data.setIncomplete(isIncomplete()); 961 data.setChangesetId(changesetId); 962 data.setVersion(version); 963 } 964 965 /** 966 * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...) 967 */ 968 public abstract void updatePosition(); 969 970 /*---------------- 971 * OBJECT METHODS 972 *---------------*/ 973 974 @Override 975 protected String getFlagsAsString() { 976 StringBuilder builder = new StringBuilder(super.getFlagsAsString()); 977 978 if (isDisabled()) { 979 if (isDisabledAndHidden()) { 980 builder.append('h'); 981 } else { 982 builder.append('d'); 983 } 984 } 985 if (isTagged()) { 986 builder.append('T'); 987 } 988 if (hasDirectionKeys()) { 989 if (reversedDirection()) { 990 builder.append('<'); 991 } else { 992 builder.append('>'); 993 } 994 } 995 return builder.toString(); 996 } 997 998 /** 999 * Equal, if the id (and class) is equal. 1000 * 1001 * An primitive is equal to its incomplete counter part. 1002 */ 1003 @Override 1004 public boolean equals(Object obj) { 1005 if (this == obj) { 1006 return true; 1007 } else if (obj == null || getClass() != obj.getClass()) { 1008 return false; 1009 } else { 1010 OsmPrimitive that = (OsmPrimitive) obj; 1011 return id == that.id; 1012 } 1013 } 1014 1015 /** 1016 * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0. 1017 * 1018 * An primitive has the same hashcode as its incomplete counterpart. 1019 */ 1020 @Override 1021 public int hashCode() { 1022 return Long.hashCode(id); 1023 } 1024 1025 @Override 1026 public Collection<String> getTemplateKeys() { 1027 return Stream.concat(Stream.of(SPECIAL_VALUE_ID, SPECIAL_VALUE_LOCAL_NAME), keys()) 1028 .collect(Collectors.toList()); 1029 } 1030 1031 @Override 1032 public Object getTemplateValue(String name, boolean special) { 1033 if (special) { 1034 String lc = name.toLowerCase(Locale.ENGLISH); 1035 if (SPECIAL_VALUE_ID.equals(lc)) 1036 return getId(); 1037 else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc)) 1038 return getLocalName(); 1039 else 1040 return null; 1041 1042 } else 1043 return getIgnoreCase(name); 1044 } 1045 1046 @Override 1047 public boolean evaluateCondition(Match condition) { 1048 return condition.match(this); 1049 } 1050 1051 /** 1052 * Replies the set of referring relations 1053 * @param primitives primitives to fetch relations from 1054 * 1055 * @return the set of referring relations 1056 */ 1057 public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) { 1058 return primitives.stream() 1059 .flatMap(p -> p.referrers(Relation.class)) 1060 .collect(Collectors.toSet()); 1061 } 1062 1063 /** 1064 * Determines if this primitive has tags denoting an area. 1065 * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise. 1066 * @since 6491 1067 */ 1068 public final boolean hasAreaTags() { 1069 return hasKey("building", "landuse", "amenity", "shop", "building:part", "boundary", "historic", "place", "area:highway") 1070 || hasTag("area", OsmUtils.TRUE_VALUE) 1071 || hasTag("waterway", "riverbank") 1072 || hasTag("highway", "rest_area", "services", "platform") 1073 || hasTag("railway", "platform") 1074 || hasTagDifferent("leisure", "picnic_table", "slipway", "firepit") 1075 || hasTag("natural", "water", "wood", "scrub", "wetland", "grassland", "heath", "rock", "bare_rock", 1076 "sand", "beach", "scree", "bay", "glacier", "shingle", "fell", "reef", "stone", 1077 "mud", "landslide", "sinkhole", "crevasse", "desert") 1078 || hasTag("aeroway", "aerodrome"); 1079 } 1080 1081 /** 1082 * Determines if this primitive semantically concerns an area. 1083 * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise. 1084 * @since 6491 1085 */ 1086 public abstract boolean concernsArea(); 1087 1088 /** 1089 * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}. 1090 * @return {@code true} if this primitive lies outside of the downloaded area 1091 */ 1092 public abstract boolean isOutsideDownloadArea(); 1093 1094 /** 1095 * If necessary, extend the bbox to contain this primitive 1096 * @param box a bbox instance 1097 * @param visited a set of visited members or null 1098 * @since 11269 1099 */ 1100 protected abstract void addToBBox(BBox box, Set<PrimitiveId> visited); 1101}