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