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