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.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.Date; 012import java.util.HashMap; 013import java.util.HashSet; 014import java.util.LinkedHashSet; 015import java.util.LinkedList; 016import java.util.List; 017import java.util.Locale; 018import java.util.Map; 019import java.util.Objects; 020import java.util.Set; 021 022import org.openstreetmap.josm.Main; 023import org.openstreetmap.josm.actions.search.SearchCompiler; 024import org.openstreetmap.josm.actions.search.SearchCompiler.Match; 025import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; 026import org.openstreetmap.josm.data.osm.visitor.Visitor; 027import org.openstreetmap.josm.gui.mappaint.StyleCache; 028import org.openstreetmap.josm.tools.CheckParameterUtil; 029import org.openstreetmap.josm.tools.Predicate; 030import org.openstreetmap.josm.tools.Predicates; 031import org.openstreetmap.josm.tools.Utils; 032import org.openstreetmap.josm.tools.template_engine.TemplateEngineDataProvider; 033 034/** 035 * The base class for OSM objects ({@link Node}, {@link Way}, {@link Relation}). 036 * 037 * It can be created, deleted and uploaded to the OSM-Server. 038 * 039 * Although OsmPrimitive is designed as a base class, it is not to be meant to subclass 040 * it by any other than from the package {@link org.openstreetmap.josm.data.osm}. The available primitives are a fixed set that are given 041 * by the server environment and not an extendible data stuff. 042 * 043 * @author imi 044 */ 045public abstract class OsmPrimitive extends AbstractPrimitive implements Comparable<OsmPrimitive>, TemplateEngineDataProvider { 046 private static final String SPECIAL_VALUE_ID = "id"; 047 private static final String SPECIAL_VALUE_LOCAL_NAME = "localname"; 048 049 /** 050 * An object can be disabled by the filter mechanism. 051 * Then it will show in a shade of gray on the map or it is completely 052 * hidden from the view. 053 * Disabled objects usually cannot be selected or modified 054 * while the filter is active. 055 */ 056 protected static final int FLAG_DISABLED = 1 << 4; 057 058 /** 059 * This flag is only relevant if an object is disabled by the 060 * filter mechanism (i.e. FLAG_DISABLED is set). 061 * Then it indicates, whether it is completely hidden or 062 * just shown in gray color. 063 * 064 * When the primitive is not disabled, this flag should be 065 * unset as well (for efficient access). 066 */ 067 protected static final int FLAG_HIDE_IF_DISABLED = 1 << 5; 068 069 /** 070 * Flag used internally by the filter mechanism. 071 */ 072 protected static final int FLAG_DISABLED_TYPE = 1 << 6; 073 074 /** 075 * Flag used internally by the filter mechanism. 076 */ 077 protected static final int FLAG_HIDDEN_TYPE = 1 << 7; 078 079 /** 080 * This flag is set if the primitive is a way and 081 * according to the tags, the direction of the way is important. 082 * (e.g. one way street.) 083 */ 084 protected static final int FLAG_HAS_DIRECTIONS = 1 << 8; 085 086 /** 087 * If the primitive is tagged. 088 * Some trivial tags like source=* are ignored here. 089 */ 090 protected static final int FLAG_TAGGED = 1 << 9; 091 092 /** 093 * This flag is only relevant if FLAG_HAS_DIRECTIONS is set. 094 * It shows, that direction of the arrows should be reversed. 095 * (E.g. oneway=-1.) 096 */ 097 protected static final int FLAG_DIRECTION_REVERSED = 1 << 10; 098 099 /** 100 * When hovering over ways and nodes in add mode, the 101 * "target" objects are visually highlighted. This flag indicates 102 * that the primitive is currently highlighted. 103 */ 104 protected static final int FLAG_HIGHLIGHTED = 1 << 11; 105 106 /** 107 * If the primitive is annotated with a tag such as note, fixme, etc. 108 * Match the "work in progress" tags in default map style. 109 */ 110 protected static final int FLAG_ANNOTATED = 1 << 12; 111 112 /** 113 * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in 114 * another collection of {@link OsmPrimitive}s. The result collection is a list. 115 * 116 * If <code>list</code> is null, replies an empty list. 117 * 118 * @param <T> type of data (must be one of the {@link OsmPrimitive} types 119 * @param list the original list 120 * @param type the type to filter for 121 * @return the sub-list of OSM primitives of type <code>type</code> 122 */ 123 public static <T extends OsmPrimitive> List<T> getFilteredList(Collection<OsmPrimitive> list, Class<T> type) { 124 if (list == null) return Collections.emptyList(); 125 List<T> ret = new LinkedList<>(); 126 for (OsmPrimitive p: list) { 127 if (type.isInstance(p)) { 128 ret.add(type.cast(p)); 129 } 130 } 131 return ret; 132 } 133 134 /** 135 * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in 136 * another collection of {@link OsmPrimitive}s. The result collection is a set. 137 * 138 * If <code>list</code> is null, replies an empty set. 139 * 140 * @param <T> type of data (must be one of the {@link OsmPrimitive} types 141 * @param set the original collection 142 * @param type the type to filter for 143 * @return the sub-set of OSM primitives of type <code>type</code> 144 */ 145 public static <T extends OsmPrimitive> Set<T> getFilteredSet(Collection<OsmPrimitive> set, Class<T> type) { 146 Set<T> ret = new LinkedHashSet<>(); 147 if (set != null) { 148 for (OsmPrimitive p: set) { 149 if (type.isInstance(p)) { 150 ret.add(type.cast(p)); 151 } 152 } 153 } 154 return ret; 155 } 156 157 /** 158 * Replies the collection of referring primitives for the primitives in <code>primitives</code>. 159 * 160 * @param primitives the collection of primitives. 161 * @return the collection of referring primitives for the primitives in <code>primitives</code>; 162 * empty set if primitives is null or if there are no referring primitives 163 */ 164 public static Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) { 165 Set<OsmPrimitive> ret = new HashSet<>(); 166 if (primitives == null || primitives.isEmpty()) return ret; 167 for (OsmPrimitive p: primitives) { 168 ret.addAll(p.getReferrers()); 169 } 170 return ret; 171 } 172 173 /** 174 * A predicate that filters primitives that are usable. 175 * @see OsmPrimitive#isUsable() 176 */ 177 public static final Predicate<OsmPrimitive> isUsablePredicate = new Predicate<OsmPrimitive>() { 178 @Override 179 public boolean evaluate(OsmPrimitive primitive) { 180 return primitive.isUsable(); 181 } 182 }; 183 184 /** 185 * A predicate filtering primitives that are selectable. 186 */ 187 public static final Predicate<OsmPrimitive> isSelectablePredicate = new Predicate<OsmPrimitive>() { 188 @Override 189 public boolean evaluate(OsmPrimitive primitive) { 190 return primitive.isSelectable(); 191 } 192 }; 193 194 /** 195 * A predicate filtering primitives that are not deleted. 196 */ 197 public static final Predicate<OsmPrimitive> nonDeletedPredicate = new Predicate<OsmPrimitive>() { 198 @Override public boolean evaluate(OsmPrimitive primitive) { 199 return !primitive.isDeleted(); 200 } 201 }; 202 203 /** 204 * A predicate filtering primitives that are not deleted and not incomplete. 205 */ 206 public static final Predicate<OsmPrimitive> nonDeletedCompletePredicate = new Predicate<OsmPrimitive>() { 207 @Override public boolean evaluate(OsmPrimitive primitive) { 208 return !primitive.isDeleted() && !primitive.isIncomplete(); 209 } 210 }; 211 212 /** 213 * A predicate filtering primitives that are not deleted and not incomplete and that are not a relation. 214 */ 215 public static final Predicate<OsmPrimitive> nonDeletedPhysicalPredicate = new Predicate<OsmPrimitive>() { 216 @Override public boolean evaluate(OsmPrimitive primitive) { 217 return !primitive.isDeleted() && !primitive.isIncomplete() && !(primitive instanceof Relation); 218 } 219 }; 220 221 /** 222 * A predicate filtering primitives that are modified 223 */ 224 public static final Predicate<OsmPrimitive> modifiedPredicate = new Predicate<OsmPrimitive>() { 225 @Override public boolean evaluate(OsmPrimitive primitive) { 226 return primitive.isModified(); 227 } 228 }; 229 230 /** 231 * A predicate filtering nodes. 232 */ 233 public static final Predicate<OsmPrimitive> nodePredicate = Predicates.<OsmPrimitive>isOfClass(Node.class); 234 235 /** 236 * A predicate filtering ways. 237 */ 238 public static final Predicate<OsmPrimitive> wayPredicate = Predicates.<OsmPrimitive>isOfClass(Way.class); 239 240 /** 241 * A predicate filtering relations. 242 */ 243 public static final Predicate<OsmPrimitive> relationPredicate = Predicates.<OsmPrimitive>isOfClass(Relation.class); 244 245 /** 246 * A predicate filtering multipolygon relations. 247 */ 248 public static final Predicate<OsmPrimitive> multipolygonPredicate = new Predicate<OsmPrimitive>() { 249 @Override public boolean evaluate(OsmPrimitive primitive) { 250 return primitive.getClass() == Relation.class && ((Relation) primitive).isMultipolygon(); 251 } 252 }; 253 254 /** 255 * This matches all ways that have a direction 256 * 257 * @see #FLAG_HAS_DIRECTIONS 258 */ 259 public static final Predicate<Tag> directionalKeyPredicate = new Predicate<Tag>() { 260 @Override 261 public boolean evaluate(Tag tag) { 262 return directionKeys.match(tag); 263 } 264 }; 265 266 /** 267 * Creates a new primitive for the given id. 268 * 269 * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. 270 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or 271 * positive number. 272 * 273 * @param id the id 274 * @param allowNegativeId {@code true} to allow negative id 275 * @throws IllegalArgumentException if id < 0 and allowNegativeId is false 276 */ 277 protected OsmPrimitive(long id, boolean allowNegativeId) { 278 if (allowNegativeId) { 279 this.id = id; 280 } else { 281 if (id < 0) 282 throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id)); 283 else if (id == 0) { 284 this.id = generateUniqueId(); 285 } else { 286 this.id = id; 287 } 288 289 } 290 this.version = 0; 291 this.setIncomplete(id > 0); 292 } 293 294 /** 295 * Creates a new primitive for the given id and version. 296 * 297 * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. 298 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or 299 * positive number. 300 * 301 * If id is not > 0 version is ignored and set to 0. 302 * 303 * @param id the id 304 * @param version the version (positive integer) 305 * @param allowNegativeId {@code true} to allow negative id 306 * @throws IllegalArgumentException if id < 0 and allowNegativeId is false 307 */ 308 protected OsmPrimitive(long id, int version, boolean allowNegativeId) { 309 this(id, allowNegativeId); 310 this.version = id > 0 ? version : 0; 311 setIncomplete(id > 0 && version == 0); 312 } 313 314 /*---------- 315 * MAPPAINT 316 *--------*/ 317 public StyleCache mappaintStyle; 318 public int mappaintCacheIdx; 319 320 /* This should not be called from outside. Fixing the UI to add relevant 321 get/set functions calling this implicitely is preferred, so we can have 322 transparent cache handling in the future. */ 323 public void clearCachedStyle() { 324 mappaintStyle = null; 325 } 326 /* end of mappaint data */ 327 328 /*--------- 329 * DATASET 330 *---------*/ 331 332 /** the parent dataset */ 333 private DataSet dataSet; 334 335 /** 336 * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods 337 * @param dataSet the parent dataset 338 */ 339 void setDataset(DataSet dataSet) { 340 if (this.dataSet != null && dataSet != null && this.dataSet != dataSet) 341 throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset"); 342 this.dataSet = dataSet; 343 } 344 345 /** 346 * 347 * @return DataSet this primitive is part of. 348 */ 349 public DataSet getDataSet() { 350 return dataSet; 351 } 352 353 /** 354 * Throws exception if primitive is not part of the dataset 355 */ 356 public void checkDataset() { 357 if (dataSet == null) 358 throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString()); 359 } 360 361 protected boolean writeLock() { 362 if (dataSet != null) { 363 dataSet.beginUpdate(); 364 return true; 365 } else 366 return false; 367 } 368 369 protected void writeUnlock(boolean locked) { 370 if (locked) { 371 // It shouldn't be possible for dataset to become null because 372 // method calling setDataset would need write lock which is owned by this thread 373 dataSet.endUpdate(); 374 } 375 } 376 377 /** 378 * Sets the id and the version of this primitive if it is known to the OSM API. 379 * 380 * Since we know the id and its version it can't be incomplete anymore. incomplete 381 * is set to false. 382 * 383 * @param id the id. > 0 required 384 * @param version the version > 0 required 385 * @throws IllegalArgumentException if id <= 0 386 * @throws IllegalArgumentException if version <= 0 387 * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset 388 */ 389 @Override 390 public void setOsmId(long id, int version) { 391 boolean locked = writeLock(); 392 try { 393 if (id <= 0) 394 throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id)); 395 if (version <= 0) 396 throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version)); 397 if (dataSet != null && id != this.id) { 398 DataSet datasetCopy = dataSet; 399 // Reindex primitive 400 datasetCopy.removePrimitive(this); 401 this.id = id; 402 datasetCopy.addPrimitive(this); 403 } 404 super.setOsmId(id, version); 405 } finally { 406 writeUnlock(locked); 407 } 408 } 409 410 /** 411 * Clears the metadata, including id and version known to the OSM API. 412 * The id is a new unique id. The version, changeset and timestamp are set to 0. 413 * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead 414 * 415 * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@link DataSet}. 416 * 417 * @throws DataIntegrityProblemException If primitive was already added to the dataset 418 * @since 6140 419 */ 420 @Override 421 public void clearOsmMetadata() { 422 if (dataSet != null) 423 throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset"); 424 super.clearOsmMetadata(); 425 } 426 427 @Override 428 public void setUser(User user) { 429 boolean locked = writeLock(); 430 try { 431 super.setUser(user); 432 } finally { 433 writeUnlock(locked); 434 } 435 } 436 437 @Override 438 public void setChangesetId(int changesetId) { 439 boolean locked = writeLock(); 440 try { 441 int old = this.changesetId; 442 super.setChangesetId(changesetId); 443 if (dataSet != null) { 444 dataSet.fireChangesetIdChanged(this, old, changesetId); 445 } 446 } finally { 447 writeUnlock(locked); 448 } 449 } 450 451 @Override 452 public void setTimestamp(Date timestamp) { 453 boolean locked = writeLock(); 454 try { 455 super.setTimestamp(timestamp); 456 } finally { 457 writeUnlock(locked); 458 } 459 } 460 461 462 /* ------- 463 /* FLAGS 464 /* ------*/ 465 466 private void updateFlagsNoLock(int flag, boolean value) { 467 super.updateFlags(flag, value); 468 } 469 470 @Override 471 protected final void updateFlags(int flag, boolean value) { 472 boolean locked = writeLock(); 473 try { 474 updateFlagsNoLock(flag, value); 475 } finally { 476 writeUnlock(locked); 477 } 478 } 479 480 /** 481 * Make the primitive disabled (e.g. if a filter applies). 482 * 483 * To enable the primitive again, use unsetDisabledState. 484 * @param hidden if the primitive should be completely hidden from view or 485 * just shown in gray color. 486 * @return true, any flag has changed; false if you try to set the disabled 487 * state to the value that is already preset 488 */ 489 public boolean setDisabledState(boolean hidden) { 490 boolean locked = writeLock(); 491 try { 492 int oldFlags = flags; 493 updateFlagsNoLock(FLAG_DISABLED, true); 494 updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, hidden); 495 return oldFlags != flags; 496 } finally { 497 writeUnlock(locked); 498 } 499 } 500 501 /** 502 * Remove the disabled flag from the primitive. 503 * Afterwards, the primitive is displayed normally and can be selected again. 504 * @return {@code true} if a change occurred 505 */ 506 public boolean unsetDisabledState() { 507 boolean locked = writeLock(); 508 try { 509 int oldFlags = flags; 510 updateFlagsNoLock(FLAG_DISABLED + FLAG_HIDE_IF_DISABLED, false); 511 return oldFlags != flags; 512 } finally { 513 writeUnlock(locked); 514 } 515 } 516 517 /** 518 * Set binary property used internally by the filter mechanism. 519 * @param isExplicit new "disabled type" flag value 520 */ 521 public void setDisabledType(boolean isExplicit) { 522 updateFlags(FLAG_DISABLED_TYPE, isExplicit); 523 } 524 525 /** 526 * Set binary property used internally by the filter mechanism. 527 * @param isExplicit new "hidden type" flag value 528 */ 529 public void setHiddenType(boolean isExplicit) { 530 updateFlags(FLAG_HIDDEN_TYPE, isExplicit); 531 } 532 533 /** 534 * Replies true, if this primitive is disabled. (E.g. a filter applies) 535 * @return {@code true} if this object has the "disabled" flag enabled 536 */ 537 public boolean isDisabled() { 538 return (flags & FLAG_DISABLED) != 0; 539 } 540 541 /** 542 * Replies true, if this primitive is disabled and marked as completely hidden on the map. 543 * @return {@code true} if this object has both the "disabled" and "hide if disabled" flags enabled 544 */ 545 public boolean isDisabledAndHidden() { 546 return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0); 547 } 548 549 /** 550 * Get binary property used internally by the filter mechanism. 551 * @return {@code true} if this object has the "hidden type" flag enabled 552 */ 553 public boolean getHiddenType() { 554 return (flags & FLAG_HIDDEN_TYPE) != 0; 555 } 556 557 /** 558 * Get binary property used internally by the filter mechanism. 559 * @return {@code true} if this object has the "disabled type" flag enabled 560 */ 561 public boolean getDisabledType() { 562 return (flags & FLAG_DISABLED_TYPE) != 0; 563 } 564 565 /** 566 * Determines if this object is selectable. 567 * @return {@code true} if this object is selectable 568 */ 569 public boolean isSelectable() { 570 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_DISABLED + FLAG_HIDE_IF_DISABLED)) == 0; 571 } 572 573 /** 574 * Determines if this object is drawable. 575 * @return {@code true} if this object is drawable 576 */ 577 public boolean isDrawable() { 578 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0; 579 } 580 581 @Override 582 public void setModified(boolean modified) { 583 boolean locked = writeLock(); 584 try { 585 super.setModified(modified); 586 if (dataSet != null) { 587 dataSet.firePrimitiveFlagsChanged(this); 588 } 589 clearCachedStyle(); 590 } finally { 591 writeUnlock(locked); 592 } 593 } 594 595 @Override 596 public void setVisible(boolean visible) { 597 boolean locked = writeLock(); 598 try { 599 super.setVisible(visible); 600 clearCachedStyle(); 601 } finally { 602 writeUnlock(locked); 603 } 604 } 605 606 @Override 607 public void setDeleted(boolean deleted) { 608 boolean locked = writeLock(); 609 try { 610 super.setDeleted(deleted); 611 if (dataSet != null) { 612 if (deleted) { 613 dataSet.firePrimitivesRemoved(Collections.singleton(this), false); 614 } else { 615 dataSet.firePrimitivesAdded(Collections.singleton(this), false); 616 } 617 } 618 clearCachedStyle(); 619 } finally { 620 writeUnlock(locked); 621 } 622 } 623 624 @Override 625 protected final void setIncomplete(boolean incomplete) { 626 boolean locked = writeLock(); 627 try { 628 if (dataSet != null && incomplete != this.isIncomplete()) { 629 if (incomplete) { 630 dataSet.firePrimitivesRemoved(Collections.singletonList(this), true); 631 } else { 632 dataSet.firePrimitivesAdded(Collections.singletonList(this), true); 633 } 634 } 635 super.setIncomplete(incomplete); 636 } finally { 637 writeUnlock(locked); 638 } 639 } 640 641 /** 642 * Determines whether the primitive is selected 643 * @return whether the primitive is selected 644 * @see DataSet#isSelected(OsmPrimitive) 645 */ 646 public boolean isSelected() { 647 return dataSet != null && dataSet.isSelected(this); 648 } 649 650 /** 651 * Determines if this primitive is a member of a selected relation. 652 * @return {@code true} if this primitive is a member of a selected relation, {@code false} otherwise 653 */ 654 public boolean isMemberOfSelected() { 655 if (referrers == null) 656 return false; 657 if (referrers instanceof OsmPrimitive) 658 return referrers instanceof Relation && ((OsmPrimitive) referrers).isSelected(); 659 for (OsmPrimitive ref : (OsmPrimitive[]) referrers) { 660 if (ref instanceof Relation && ref.isSelected()) 661 return true; 662 } 663 return false; 664 } 665 666 /** 667 * Determines if this primitive is an outer member of a selected multipolygon relation. 668 * @return {@code true} if this primitive is an outer member of a selected multipolygon relation, {@code false} otherwise 669 * @since 7621 670 */ 671 public boolean isOuterMemberOfSelected() { 672 if (referrers == null) 673 return false; 674 if (referrers instanceof OsmPrimitive) { 675 return isOuterMemberOfMultipolygon((OsmPrimitive) referrers); 676 } 677 for (OsmPrimitive ref : (OsmPrimitive[]) referrers) { 678 if (isOuterMemberOfMultipolygon(ref)) 679 return true; 680 } 681 return false; 682 } 683 684 private boolean isOuterMemberOfMultipolygon(OsmPrimitive ref) { 685 if (ref instanceof Relation && ref.isSelected() && ((Relation) ref).isMultipolygon()) { 686 for (RelationMember rm : ((Relation) ref).getMembersFor(Collections.singleton(this))) { 687 if ("outer".equals(rm.getRole())) { 688 return true; 689 } 690 } 691 } 692 return false; 693 } 694 695 /** 696 * Updates the highlight flag for this primitive. 697 * @param highlighted The new highlight flag. 698 */ 699 public void setHighlighted(boolean highlighted) { 700 if (isHighlighted() != highlighted) { 701 updateFlags(FLAG_HIGHLIGHTED, highlighted); 702 if (dataSet != null) { 703 dataSet.fireHighlightingChanged(); 704 } 705 } 706 } 707 708 /** 709 * Checks if the highlight flag for this primitive was set 710 * @return The highlight flag. 711 */ 712 public boolean isHighlighted() { 713 return (flags & FLAG_HIGHLIGHTED) != 0; 714 } 715 716 /*--------------------------------------------------- 717 * WORK IN PROGRESS, UNINTERESTING AND DIRECTION KEYS 718 *--------------------------------------------------*/ 719 720 private static volatile Collection<String> workinprogress; 721 private static volatile Collection<String> uninteresting; 722 private static volatile Collection<String> discardable; 723 724 /** 725 * Returns a list of "uninteresting" keys that do not make an object 726 * "tagged". Entries that end with ':' are causing a whole namespace to be considered 727 * "uninteresting". Only the first level namespace is considered. 728 * Initialized by isUninterestingKey() 729 * @return The list of uninteresting keys. 730 */ 731 public static Collection<String> getUninterestingKeys() { 732 if (uninteresting == null) { 733 List<String> l = new LinkedList<>(Arrays.asList( 734 "source", "source_ref", "source:", "comment", 735 "converted_by", "watch", "watch:", 736 "description", "attribution")); 737 l.addAll(getDiscardableKeys()); 738 l.addAll(getWorkInProgressKeys()); 739 uninteresting = Main.pref.getCollection("tags.uninteresting", l); 740 } 741 return uninteresting; 742 } 743 744 /** 745 * Returns a list of keys which have been deemed uninteresting to the point 746 * that they can be silently removed from data which is being edited. 747 * @return The list of discardable keys. 748 */ 749 public static Collection<String> getDiscardableKeys() { 750 if (discardable == null) { 751 discardable = Main.pref.getCollection("tags.discardable", 752 Arrays.asList( 753 "created_by", 754 "geobase:datasetName", 755 "geobase:uuid", 756 "KSJ2:ADS", 757 "KSJ2:ARE", 758 "KSJ2:AdminArea", 759 "KSJ2:COP_label", 760 "KSJ2:DFD", 761 "KSJ2:INT", 762 "KSJ2:INT_label", 763 "KSJ2:LOC", 764 "KSJ2:LPN", 765 "KSJ2:OPC", 766 "KSJ2:PubFacAdmin", 767 "KSJ2:RAC", 768 "KSJ2:RAC_label", 769 "KSJ2:RIC", 770 "KSJ2:RIN", 771 "KSJ2:WSC", 772 "KSJ2:coordinate", 773 "KSJ2:curve_id", 774 "KSJ2:curve_type", 775 "KSJ2:filename", 776 "KSJ2:lake_id", 777 "KSJ2:lat", 778 "KSJ2:long", 779 "KSJ2:river_id", 780 "odbl", 781 "odbl:note", 782 "SK53_bulk:load", 783 "sub_sea:type", 784 "tiger:source", 785 "tiger:separated", 786 "tiger:tlid", 787 "tiger:upload_uuid", 788 "yh:LINE_NAME", 789 "yh:LINE_NUM", 790 "yh:STRUCTURE", 791 "yh:TOTYUMONO", 792 "yh:TYPE", 793 "yh:WIDTH", 794 "yh:WIDTH_RANK" 795 )); 796 } 797 return discardable; 798 } 799 800 /** 801 * Returns a list of "work in progress" keys that do not make an object 802 * "tagged" but "annotated". 803 * @return The list of work in progress keys. 804 * @since 5754 805 */ 806 public static Collection<String> getWorkInProgressKeys() { 807 if (workinprogress == null) { 808 workinprogress = Main.pref.getCollection("tags.workinprogress", 809 Arrays.asList("note", "fixme", "FIXME")); 810 } 811 return workinprogress; 812 } 813 814 /** 815 * Determines if key is considered "uninteresting". 816 * @param key The key to check 817 * @return true if key is considered "uninteresting". 818 */ 819 public static boolean isUninterestingKey(String key) { 820 getUninterestingKeys(); 821 if (uninteresting.contains(key)) 822 return true; 823 int pos = key.indexOf(':'); 824 if (pos > 0) 825 return uninteresting.contains(key.substring(0, pos + 1)); 826 return false; 827 } 828 829 /** 830 * Returns {@link #getKeys()} for which {@code key} does not fulfill {@link #isUninterestingKey}. 831 * @return A map of interesting tags 832 */ 833 public Map<String, String> getInterestingTags() { 834 Map<String, String> result = new HashMap<>(); 835 String[] keys = this.keys; 836 if (keys != null) { 837 for (int i = 0; i < keys.length; i += 2) { 838 if (!isUninterestingKey(keys[i])) { 839 result.put(keys[i], keys[i + 1]); 840 } 841 } 842 } 843 return result; 844 } 845 846 /** 847 * A tagged way that matches this pattern has a direction. 848 * @see #FLAG_HAS_DIRECTIONS 849 */ 850 private static volatile Match directionKeys; 851 852 /** 853 * A tagged way that matches this pattern has a direction that is reversed. 854 * <p> 855 * This pattern should be a subset of {@link #directionKeys} 856 * @see #FLAG_DIRECTION_REVERSED 857 */ 858 private static volatile Match reversedDirectionKeys; 859 860 static { 861 String reversedDirectionDefault = "oneway=\"-1\""; 862 863 String directionDefault = "oneway? | (aerialway=* -aerialway=station) | "+ 864 "waterway=stream | waterway=river | waterway=ditch | waterway=drain | "+ 865 "\"piste:type\"=downhill | \"piste:type\"=sled | man_made=\"piste:halfpipe\" | "+ 866 "junction=roundabout | (highway=motorway & -oneway=no & -oneway=reversible) | "+ 867 "(highway=motorway_link & -oneway=no & -oneway=reversible)"; 868 869 reversedDirectionKeys = compileDirectionKeys("tags.reversed_direction", reversedDirectionDefault); 870 directionKeys = compileDirectionKeys("tags.direction", directionDefault); 871 } 872 873 private static Match compileDirectionKeys(String prefName, String defaultValue) throws AssertionError { 874 try { 875 return SearchCompiler.compile(Main.pref.get(prefName, defaultValue)); 876 } catch (ParseError e) { 877 Main.error("Unable to compile pattern for " + prefName + ", trying default pattern: " + e.getMessage()); 878 } 879 880 try { 881 return SearchCompiler.compile(defaultValue); 882 } catch (ParseError e2) { 883 throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2); 884 } 885 } 886 887 private void updateTagged() { 888 for (String key: keySet()) { 889 // 'area' is not really uninteresting (putting it in that list may have unpredictable side effects) 890 // but it's clearly not enough to consider an object as tagged (see #9261) 891 if (!isUninterestingKey(key) && !"area".equals(key)) { 892 updateFlagsNoLock(FLAG_TAGGED, true); 893 return; 894 } 895 } 896 updateFlagsNoLock(FLAG_TAGGED, false); 897 } 898 899 private void updateAnnotated() { 900 for (String key: keySet()) { 901 if (getWorkInProgressKeys().contains(key)) { 902 updateFlagsNoLock(FLAG_ANNOTATED, true); 903 return; 904 } 905 } 906 updateFlagsNoLock(FLAG_ANNOTATED, false); 907 } 908 909 /** 910 * Determines if this object is considered "tagged". To be "tagged", an object 911 * must have one or more "interesting" tags. "created_by" and "source" 912 * are typically considered "uninteresting" and do not make an object 913 * "tagged". 914 * @return true if this object is considered "tagged" 915 */ 916 public boolean isTagged() { 917 return (flags & FLAG_TAGGED) != 0; 918 } 919 920 /** 921 * Determines if this object is considered "annotated". To be "annotated", an object 922 * must have one or more "work in progress" tags, such as "note" or "fixme". 923 * @return true if this object is considered "annotated" 924 * @since 5754 925 */ 926 public boolean isAnnotated() { 927 return (flags & FLAG_ANNOTATED) != 0; 928 } 929 930 private void updateDirectionFlags() { 931 boolean hasDirections = false; 932 boolean directionReversed = false; 933 if (reversedDirectionKeys.match(this)) { 934 hasDirections = true; 935 directionReversed = true; 936 } 937 if (directionKeys.match(this)) { 938 hasDirections = true; 939 } 940 941 updateFlagsNoLock(FLAG_DIRECTION_REVERSED, directionReversed); 942 updateFlagsNoLock(FLAG_HAS_DIRECTIONS, hasDirections); 943 } 944 945 /** 946 * true if this object has direction dependent tags (e.g. oneway) 947 * @return {@code true} if this object has direction dependent tags 948 */ 949 public boolean hasDirectionKeys() { 950 return (flags & FLAG_HAS_DIRECTIONS) != 0; 951 } 952 953 /** 954 * true if this object has the "reversed diretion" flag enabled 955 * @return {@code true} if this object has the "reversed diretion" flag enabled 956 */ 957 public boolean reversedDirection() { 958 return (flags & FLAG_DIRECTION_REVERSED) != 0; 959 } 960 961 /*------------ 962 * Keys handling 963 ------------*/ 964 965 @Override 966 public final void setKeys(TagMap keys) { 967 boolean locked = writeLock(); 968 try { 969 super.setKeys(keys); 970 } finally { 971 writeUnlock(locked); 972 } 973 } 974 975 @Override 976 public final void setKeys(Map<String, String> keys) { 977 boolean locked = writeLock(); 978 try { 979 super.setKeys(keys); 980 } finally { 981 writeUnlock(locked); 982 } 983 } 984 985 @Override 986 public final void put(String key, String value) { 987 boolean locked = writeLock(); 988 try { 989 super.put(key, value); 990 } finally { 991 writeUnlock(locked); 992 } 993 } 994 995 @Override 996 public final void remove(String key) { 997 boolean locked = writeLock(); 998 try { 999 super.remove(key); 1000 } finally { 1001 writeUnlock(locked); 1002 } 1003 } 1004 1005 @Override 1006 public final void removeAll() { 1007 boolean locked = writeLock(); 1008 try { 1009 super.removeAll(); 1010 } finally { 1011 writeUnlock(locked); 1012 } 1013 } 1014 1015 @Override 1016 protected void keysChangedImpl(Map<String, String> originalKeys) { 1017 clearCachedStyle(); 1018 if (dataSet != null) { 1019 for (OsmPrimitive ref : getReferrers()) { 1020 ref.clearCachedStyle(); 1021 } 1022 } 1023 updateDirectionFlags(); 1024 updateTagged(); 1025 updateAnnotated(); 1026 if (dataSet != null) { 1027 dataSet.fireTagsChanged(this, originalKeys); 1028 } 1029 } 1030 1031 /*------------ 1032 * Referrers 1033 ------------*/ 1034 1035 private Object referrers; 1036 1037 /** 1038 * Add new referrer. If referrer is already included then no action is taken 1039 * @param referrer The referrer to add 1040 */ 1041 protected void addReferrer(OsmPrimitive referrer) { 1042 if (referrers == null) { 1043 referrers = referrer; 1044 } else if (referrers instanceof OsmPrimitive) { 1045 if (referrers != referrer) { 1046 referrers = new OsmPrimitive[] {(OsmPrimitive) referrers, referrer}; 1047 } 1048 } else { 1049 for (OsmPrimitive primitive:(OsmPrimitive[]) referrers) { 1050 if (primitive == referrer) 1051 return; 1052 } 1053 referrers = Utils.addInArrayCopy((OsmPrimitive[]) referrers, referrer); 1054 } 1055 } 1056 1057 /** 1058 * Remove referrer. No action is taken if referrer is not registered 1059 * @param referrer The referrer to remove 1060 */ 1061 protected void removeReferrer(OsmPrimitive referrer) { 1062 if (referrers instanceof OsmPrimitive) { 1063 if (referrers == referrer) { 1064 referrers = null; 1065 } 1066 } else if (referrers instanceof OsmPrimitive[]) { 1067 OsmPrimitive[] orig = (OsmPrimitive[]) referrers; 1068 int idx = -1; 1069 for (int i = 0; i < orig.length; i++) { 1070 if (orig[i] == referrer) { 1071 idx = i; 1072 break; 1073 } 1074 } 1075 if (idx == -1) 1076 return; 1077 1078 if (orig.length == 2) { 1079 referrers = orig[1-idx]; // idx is either 0 or 1, take the other 1080 } else { // downsize the array 1081 OsmPrimitive[] smaller = new OsmPrimitive[orig.length-1]; 1082 System.arraycopy(orig, 0, smaller, 0, idx); 1083 System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx); 1084 referrers = smaller; 1085 } 1086 } 1087 } 1088 1089 /** 1090 * Find primitives that reference this primitive. Returns only primitives that are included in the same 1091 * dataset as this primitive. <br> 1092 * 1093 * For example following code will add wnew as referer to all nodes of existingWay, but this method will 1094 * not return wnew because it's not part of the dataset <br> 1095 * 1096 * <code>Way wnew = new Way(existingWay)</code> 1097 * 1098 * @param allowWithoutDataset If true, method will return empty list if primitive is not part of the dataset. If false, 1099 * exception will be thrown in this case 1100 * 1101 * @return a collection of all primitives that reference this primitive. 1102 */ 1103 public final List<OsmPrimitive> getReferrers(boolean allowWithoutDataset) { 1104 // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example 1105 // when way is cloned 1106 1107 if (dataSet == null && allowWithoutDataset) 1108 return Collections.emptyList(); 1109 1110 checkDataset(); 1111 Object referrers = this.referrers; 1112 List<OsmPrimitive> result = new ArrayList<>(); 1113 if (referrers != null) { 1114 if (referrers instanceof OsmPrimitive) { 1115 OsmPrimitive ref = (OsmPrimitive) referrers; 1116 if (ref.dataSet == dataSet) { 1117 result.add(ref); 1118 } 1119 } else { 1120 for (OsmPrimitive o:(OsmPrimitive[]) referrers) { 1121 if (dataSet == o.dataSet) { 1122 result.add(o); 1123 } 1124 } 1125 } 1126 } 1127 return result; 1128 } 1129 1130 public final List<OsmPrimitive> getReferrers() { 1131 return getReferrers(false); 1132 } 1133 1134 /** 1135 * <p>Visits {@code visitor} for all referrers.</p> 1136 * 1137 * @param visitor the visitor. Ignored, if null. 1138 */ 1139 public void visitReferrers(Visitor visitor) { 1140 if (visitor == null) return; 1141 if (this.referrers == null) 1142 return; 1143 else if (this.referrers instanceof OsmPrimitive) { 1144 OsmPrimitive ref = (OsmPrimitive) this.referrers; 1145 if (ref.dataSet == dataSet) { 1146 ref.accept(visitor); 1147 } 1148 } else if (this.referrers instanceof OsmPrimitive[]) { 1149 OsmPrimitive[] refs = (OsmPrimitive[]) this.referrers; 1150 for (OsmPrimitive ref: refs) { 1151 if (ref.dataSet == dataSet) { 1152 ref.accept(visitor); 1153 } 1154 } 1155 } 1156 } 1157 1158 /** 1159 Return true, if this primitive is referred by at least n ways 1160 @param n Minimal number of ways to return true. Must be positive 1161 * @return {@code true} if this primitive is referred by at least n ways 1162 */ 1163 public final boolean isReferredByWays(int n) { 1164 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example 1165 // when way is cloned 1166 Object referrers = this.referrers; 1167 if (referrers == null) return false; 1168 checkDataset(); 1169 if (referrers instanceof OsmPrimitive) 1170 return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet; 1171 else { 1172 int counter = 0; 1173 for (OsmPrimitive o : (OsmPrimitive[]) referrers) { 1174 if (dataSet == o.dataSet && o instanceof Way) { 1175 if (++counter >= n) 1176 return true; 1177 } 1178 } 1179 return false; 1180 } 1181 } 1182 1183 /*----------------- 1184 * OTHER METHODS 1185 *----------------*/ 1186 1187 /** 1188 * Implementation of the visitor scheme. Subclasses have to call the correct 1189 * visitor function. 1190 * @param visitor The visitor from which the visit() function must be called. 1191 */ 1192 public abstract void accept(Visitor visitor); 1193 1194 /** 1195 * Get and write all attributes from the parameter. Does not fire any listener, so 1196 * use this only in the data initializing phase 1197 * @param other other primitive 1198 */ 1199 public void cloneFrom(OsmPrimitive other) { 1200 // write lock is provided by subclasses 1201 if (id != other.id && dataSet != null) 1202 throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset"); 1203 1204 super.cloneFrom(other); 1205 clearCachedStyle(); 1206 } 1207 1208 /** 1209 * Merges the technical and semantical attributes from <code>other</code> onto this. 1210 * 1211 * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code> 1212 * have an assigend OSM id, the IDs have to be the same. 1213 * 1214 * @param other the other primitive. Must not be null. 1215 * @throws IllegalArgumentException if other is null. 1216 * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not 1217 * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId() 1218 */ 1219 public void mergeFrom(OsmPrimitive other) { 1220 boolean locked = writeLock(); 1221 try { 1222 CheckParameterUtil.ensureParameterNotNull(other, "other"); 1223 if (other.isNew() ^ isNew()) 1224 throw new DataIntegrityProblemException( 1225 tr("Cannot merge because either of the participating primitives is new and the other is not")); 1226 if (!other.isNew() && other.getId() != id) 1227 throw new DataIntegrityProblemException( 1228 tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId())); 1229 1230 setKeys(other.hasKeys() ? other.getKeys() : null); 1231 timestamp = other.timestamp; 1232 version = other.version; 1233 setIncomplete(other.isIncomplete()); 1234 flags = other.flags; 1235 user = other.user; 1236 changesetId = other.changesetId; 1237 } finally { 1238 writeUnlock(locked); 1239 } 1240 } 1241 1242 /** 1243 * Replies true if other isn't null and has the same interesting tags (key/value-pairs) as this. 1244 * 1245 * @param other the other object primitive 1246 * @return true if other isn't null and has the same interesting tags (key/value-pairs) as this. 1247 */ 1248 public boolean hasSameInterestingTags(OsmPrimitive other) { 1249 return (keys == null && other.keys == null) 1250 || getInterestingTags().equals(other.getInterestingTags()); 1251 } 1252 1253 /** 1254 * Replies true if this primitive and other are equal with respect to their semantic attributes. 1255 * <ol> 1256 * <li>equal id</li> 1257 * <li>both are complete or both are incomplete</li> 1258 * <li>both have the same tags</li> 1259 * </ol> 1260 * @param other other primitive to compare 1261 * @return true if this primitive and other are equal with respect to their semantic attributes. 1262 */ 1263 public final boolean hasEqualSemanticAttributes(OsmPrimitive other) { 1264 return hasEqualSemanticAttributes(other, true); 1265 } 1266 1267 boolean hasEqualSemanticAttributes(final OsmPrimitive other, final boolean testInterestingTagsOnly) { 1268 if (!isNew() && id != other.id) 1269 return false; 1270 if (isIncomplete() ^ other.isIncomplete()) // exclusive or operator for performance (see #7159) 1271 return false; 1272 return testInterestingTagsOnly ? hasSameInterestingTags(other) : getKeys().equals(other.getKeys()); 1273 } 1274 1275 /** 1276 * Replies true if this primitive and other are equal with respect to their technical attributes. 1277 * The attributes: 1278 * <ol> 1279 * <li>deleted</li> 1280 * <li>modified</li> 1281 * <li>timestamp</li> 1282 * <li>version</li> 1283 * <li>visible</li> 1284 * <li>user</li> 1285 * </ol> 1286 * have to be equal 1287 * @param other the other primitive 1288 * @return true if this primitive and other are equal with respect to their technical attributes 1289 */ 1290 public boolean hasEqualTechnicalAttributes(OsmPrimitive other) { 1291 if (other == null) return false; 1292 1293 return isDeleted() == other.isDeleted() 1294 && isModified() == other.isModified() 1295 && timestamp == other.timestamp 1296 && version == other.version 1297 && isVisible() == other.isVisible() 1298 && (user == null ? other.user == null : user == other.user) 1299 && changesetId == other.changesetId; 1300 } 1301 1302 /** 1303 * Loads (clone) this primitive from provided PrimitiveData 1304 * @param data The object which should be cloned 1305 */ 1306 public void load(PrimitiveData data) { 1307 // Write lock is provided by subclasses 1308 setKeys(data.hasKeys() ? data.getKeys() : null); 1309 setRawTimestamp(data.getRawTimestamp()); 1310 user = data.getUser(); 1311 setChangesetId(data.getChangesetId()); 1312 setDeleted(data.isDeleted()); 1313 setModified(data.isModified()); 1314 setIncomplete(data.isIncomplete()); 1315 version = data.getVersion(); 1316 } 1317 1318 /** 1319 * Save parameters of this primitive to the transport object 1320 * @return The saved object data 1321 */ 1322 public abstract PrimitiveData save(); 1323 1324 /** 1325 * Save common parameters of primitives to the transport object 1326 * @param data The object to save the data into 1327 */ 1328 protected void saveCommonAttributes(PrimitiveData data) { 1329 data.setId(id); 1330 data.setKeys(hasKeys() ? getKeys() : null); 1331 data.setRawTimestamp(getRawTimestamp()); 1332 data.setUser(user); 1333 data.setDeleted(isDeleted()); 1334 data.setModified(isModified()); 1335 data.setVisible(isVisible()); 1336 data.setIncomplete(isIncomplete()); 1337 data.setChangesetId(changesetId); 1338 data.setVersion(version); 1339 } 1340 1341 /** 1342 * Fetch the bounding box of the primitive 1343 * @return Bounding box of the object 1344 */ 1345 public abstract BBox getBBox(); 1346 1347 /** 1348 * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...) 1349 */ 1350 public abstract void updatePosition(); 1351 1352 /*---------------- 1353 * OBJECT METHODS 1354 *---------------*/ 1355 1356 @Override 1357 protected String getFlagsAsString() { 1358 StringBuilder builder = new StringBuilder(super.getFlagsAsString()); 1359 1360 if (isDisabled()) { 1361 if (isDisabledAndHidden()) { 1362 builder.append('h'); 1363 } else { 1364 builder.append('d'); 1365 } 1366 } 1367 if (isTagged()) { 1368 builder.append('T'); 1369 } 1370 if (hasDirectionKeys()) { 1371 if (reversedDirection()) { 1372 builder.append('<'); 1373 } else { 1374 builder.append('>'); 1375 } 1376 } 1377 return builder.toString(); 1378 } 1379 1380 /** 1381 * Equal, if the id (and class) is equal. 1382 * 1383 * An primitive is equal to its incomplete counter part. 1384 */ 1385 @Override 1386 public boolean equals(Object obj) { 1387 if (this == obj) return true; 1388 if (obj == null || getClass() != obj.getClass()) return false; 1389 OsmPrimitive that = (OsmPrimitive) obj; 1390 return Objects.equals(id, that.id); 1391 } 1392 1393 /** 1394 * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0. 1395 * 1396 * An primitive has the same hashcode as its incomplete counterpart. 1397 */ 1398 @Override 1399 public int hashCode() { 1400 return Objects.hash(id); 1401 } 1402 1403 /** 1404 * Replies the display name of a primitive formatted by <code>formatter</code> 1405 * @param formatter formatter to use 1406 * 1407 * @return the display name 1408 */ 1409 public abstract String getDisplayName(NameFormatter formatter); 1410 1411 @Override 1412 public Collection<String> getTemplateKeys() { 1413 Collection<String> keySet = keySet(); 1414 List<String> result = new ArrayList<>(keySet.size() + 2); 1415 result.add(SPECIAL_VALUE_ID); 1416 result.add(SPECIAL_VALUE_LOCAL_NAME); 1417 result.addAll(keySet); 1418 return result; 1419 } 1420 1421 @Override 1422 public Object getTemplateValue(String name, boolean special) { 1423 if (special) { 1424 String lc = name.toLowerCase(Locale.ENGLISH); 1425 if (SPECIAL_VALUE_ID.equals(lc)) 1426 return getId(); 1427 else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc)) 1428 return getLocalName(); 1429 else 1430 return null; 1431 1432 } else 1433 return getIgnoreCase(name); 1434 } 1435 1436 @Override 1437 public boolean evaluateCondition(Match condition) { 1438 return condition.match(this); 1439 } 1440 1441 /** 1442 * Replies the set of referring relations 1443 * @param primitives primitives to fetch relations from 1444 * 1445 * @return the set of referring relations 1446 */ 1447 public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) { 1448 Set<Relation> ret = new HashSet<>(); 1449 for (OsmPrimitive w : primitives) { 1450 ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class)); 1451 } 1452 return ret; 1453 } 1454 1455 /** 1456 * Determines if this primitive has tags denoting an area. 1457 * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise. 1458 * @since 6491 1459 */ 1460 public final boolean hasAreaTags() { 1461 return hasKey("landuse") 1462 || "yes".equals(get("area")) 1463 || "riverbank".equals(get("waterway")) 1464 || hasKey("natural") 1465 || hasKey("amenity") 1466 || hasKey("leisure") 1467 || hasKey("building") 1468 || hasKey("building:part"); 1469 } 1470 1471 /** 1472 * Determines if this primitive semantically concerns an area. 1473 * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise. 1474 * @since 6491 1475 */ 1476 public abstract boolean concernsArea(); 1477 1478 /** 1479 * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}. 1480 * @return {@code true} if this primitive lies outside of the downloaded area 1481 */ 1482 public abstract boolean isOutsideDownloadArea(); 1483}