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