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