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.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 * A tagged way that matches this pattern has a direction. 112 * @see #FLAG_HAS_DIRECTIONS 113 */ 114 static volatile Match directionKeys; 115 116 /** 117 * A tagged way that matches this pattern has a direction that is reversed. 118 * <p> 119 * This pattern should be a subset of {@link #directionKeys} 120 * @see #FLAG_DIRECTION_REVERSED 121 */ 122 private static volatile Match reversedDirectionKeys; 123 124 static { 125 String reversedDirectionDefault = "oneway=\"-1\""; 126 127 String directionDefault = "oneway? | (aerialway=* -aerialway=station) | "+ 128 "waterway=stream | waterway=river | waterway=ditch | waterway=drain | "+ 129 "(\"piste:type\"=downhill & -area=yes) | (\"piste:type\"=sled & -area=yes) | (man_made=\"piste:halfpipe\" & -area=yes) | "+ 130 "junction=roundabout | (highway=motorway & -oneway=no & -oneway=reversible) | "+ 131 "(highway=motorway_link & -oneway=no & -oneway=reversible)"; 132 133 reversedDirectionKeys = compileDirectionKeys("tags.reversed_direction", reversedDirectionDefault); 134 directionKeys = compileDirectionKeys("tags.direction", directionDefault); 135 } 136 137 /** 138 * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in 139 * another collection of {@link OsmPrimitive}s. The result collection is a list. 140 * 141 * If <code>list</code> is null, replies an empty list. 142 * 143 * @param <T> type of data (must be one of the {@link OsmPrimitive} types 144 * @param list the original list 145 * @param type the type to filter for 146 * @return the sub-list of OSM primitives of type <code>type</code> 147 */ 148 public static <T extends OsmPrimitive> List<T> getFilteredList(Collection<OsmPrimitive> list, Class<T> type) { 149 if (list == null) return Collections.emptyList(); 150 List<T> ret = new LinkedList<>(); 151 for (OsmPrimitive p: list) { 152 if (type.isInstance(p)) { 153 ret.add(type.cast(p)); 154 } 155 } 156 return ret; 157 } 158 159 /** 160 * Replies the sub-collection of {@link OsmPrimitive}s of type <code>type</code> present in 161 * another collection of {@link OsmPrimitive}s. The result collection is a set. 162 * 163 * If <code>list</code> is null, replies an empty set. 164 * 165 * @param <T> type of data (must be one of the {@link OsmPrimitive} types 166 * @param set the original collection 167 * @param type the type to filter for 168 * @return the sub-set of OSM primitives of type <code>type</code> 169 */ 170 public static <T extends OsmPrimitive> Set<T> getFilteredSet(Collection<OsmPrimitive> set, Class<T> type) { 171 Set<T> ret = new LinkedHashSet<>(); 172 if (set != null) { 173 for (OsmPrimitive p: set) { 174 if (type.isInstance(p)) { 175 ret.add(type.cast(p)); 176 } 177 } 178 } 179 return ret; 180 } 181 182 /** 183 * Replies the collection of referring primitives for the primitives in <code>primitives</code>. 184 * 185 * @param primitives the collection of primitives. 186 * @return the collection of referring primitives for the primitives in <code>primitives</code>; 187 * empty set if primitives is null or if there are no referring primitives 188 */ 189 public static Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) { 190 Set<OsmPrimitive> ret = new HashSet<>(); 191 if (primitives == null || primitives.isEmpty()) return ret; 192 for (OsmPrimitive p: primitives) { 193 ret.addAll(p.getReferrers()); 194 } 195 return ret; 196 } 197 198 /** 199 * Creates a new primitive for the given id. 200 * 201 * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. 202 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or 203 * positive number. 204 * 205 * @param id the id 206 * @param allowNegativeId {@code true} to allow negative id 207 * @throws IllegalArgumentException if id < 0 and allowNegativeId is false 208 */ 209 protected OsmPrimitive(long id, boolean allowNegativeId) { 210 if (allowNegativeId) { 211 this.id = id; 212 } else { 213 if (id < 0) 214 throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id)); 215 else if (id == 0) { 216 this.id = generateUniqueId(); 217 } else { 218 this.id = id; 219 } 220 221 } 222 this.version = 0; 223 this.setIncomplete(id > 0); 224 } 225 226 /** 227 * Creates a new primitive for the given id and version. 228 * 229 * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. 230 * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or 231 * positive number. 232 * 233 * If id is not > 0 version is ignored and set to 0. 234 * 235 * @param id the id 236 * @param version the version (positive integer) 237 * @param allowNegativeId {@code true} to allow negative id 238 * @throws IllegalArgumentException if id < 0 and allowNegativeId is false 239 */ 240 protected OsmPrimitive(long id, int version, boolean allowNegativeId) { 241 this(id, allowNegativeId); 242 this.version = id > 0 ? version : 0; 243 setIncomplete(id > 0 && version == 0); 244 } 245 246 /*---------- 247 * MAPPAINT 248 *--------*/ 249 public StyleCache mappaintStyle; 250 public int mappaintCacheIdx; 251 252 /* This should not be called from outside. Fixing the UI to add relevant 253 get/set functions calling this implicitely is preferred, so we can have 254 transparent cache handling in the future. */ 255 public void clearCachedStyle() { 256 mappaintStyle = null; 257 } 258 /* end of mappaint data */ 259 260 /*--------- 261 * DATASET 262 *---------*/ 263 264 /** the parent dataset */ 265 private DataSet dataSet; 266 267 /** 268 * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods 269 * @param dataSet the parent dataset 270 */ 271 void setDataset(DataSet dataSet) { 272 if (this.dataSet != null && dataSet != null && this.dataSet != dataSet) 273 throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset"); 274 this.dataSet = dataSet; 275 } 276 277 /** 278 * 279 * @return DataSet this primitive is part of. 280 */ 281 public DataSet getDataSet() { 282 return dataSet; 283 } 284 285 /** 286 * Throws exception if primitive is not part of the dataset 287 */ 288 public void checkDataset() { 289 if (dataSet == null) 290 throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString()); 291 } 292 293 protected boolean writeLock() { 294 if (dataSet != null) { 295 dataSet.beginUpdate(); 296 return true; 297 } else 298 return false; 299 } 300 301 protected void writeUnlock(boolean locked) { 302 if (locked) { 303 // It shouldn't be possible for dataset to become null because 304 // method calling setDataset would need write lock which is owned by this thread 305 dataSet.endUpdate(); 306 } 307 } 308 309 /** 310 * Sets the id and the version of this primitive if it is known to the OSM API. 311 * 312 * Since we know the id and its version it can't be incomplete anymore. incomplete 313 * is set to false. 314 * 315 * @param id the id. > 0 required 316 * @param version the version > 0 required 317 * @throws IllegalArgumentException if id <= 0 318 * @throws IllegalArgumentException if version <= 0 319 * @throws DataIntegrityProblemException if id is changed and primitive was already added to the dataset 320 */ 321 @Override 322 public void setOsmId(long id, int version) { 323 boolean locked = writeLock(); 324 try { 325 if (id <= 0) 326 throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id)); 327 if (version <= 0) 328 throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version)); 329 if (dataSet != null && id != this.id) { 330 DataSet datasetCopy = dataSet; 331 // Reindex primitive 332 datasetCopy.removePrimitive(this); 333 this.id = id; 334 datasetCopy.addPrimitive(this); 335 } 336 super.setOsmId(id, version); 337 } finally { 338 writeUnlock(locked); 339 } 340 } 341 342 /** 343 * Clears the metadata, including id and version known to the OSM API. 344 * The id is a new unique id. The version, changeset and timestamp are set to 0. 345 * incomplete and deleted are set to false. It's preferred to use copy constructor with clearMetadata set to true instead 346 * 347 * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@link DataSet}. 348 * 349 * @throws DataIntegrityProblemException If primitive was already added to the dataset 350 * @since 6140 351 */ 352 @Override 353 public void clearOsmMetadata() { 354 if (dataSet != null) 355 throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset"); 356 super.clearOsmMetadata(); 357 } 358 359 @Override 360 public void setUser(User user) { 361 boolean locked = writeLock(); 362 try { 363 super.setUser(user); 364 } finally { 365 writeUnlock(locked); 366 } 367 } 368 369 @Override 370 public void setChangesetId(int changesetId) { 371 boolean locked = writeLock(); 372 try { 373 int old = this.changesetId; 374 super.setChangesetId(changesetId); 375 if (dataSet != null) { 376 dataSet.fireChangesetIdChanged(this, old, changesetId); 377 } 378 } finally { 379 writeUnlock(locked); 380 } 381 } 382 383 @Override 384 public void setTimestamp(Date timestamp) { 385 boolean locked = writeLock(); 386 try { 387 super.setTimestamp(timestamp); 388 } finally { 389 writeUnlock(locked); 390 } 391 } 392 393 394 /* ------- 395 /* FLAGS 396 /* ------*/ 397 398 private void updateFlagsNoLock(int flag, boolean value) { 399 super.updateFlags(flag, value); 400 } 401 402 @Override 403 protected final void updateFlags(int flag, boolean value) { 404 boolean locked = writeLock(); 405 try { 406 updateFlagsNoLock(flag, value); 407 } finally { 408 writeUnlock(locked); 409 } 410 } 411 412 /** 413 * Make the primitive disabled (e.g. if a filter applies). 414 * 415 * To enable the primitive again, use unsetDisabledState. 416 * @param hidden if the primitive should be completely hidden from view or 417 * just shown in gray color. 418 * @return true, any flag has changed; false if you try to set the disabled 419 * state to the value that is already preset 420 */ 421 public boolean setDisabledState(boolean hidden) { 422 boolean locked = writeLock(); 423 try { 424 int oldFlags = flags; 425 updateFlagsNoLock(FLAG_DISABLED, true); 426 updateFlagsNoLock(FLAG_HIDE_IF_DISABLED, hidden); 427 return oldFlags != flags; 428 } finally { 429 writeUnlock(locked); 430 } 431 } 432 433 /** 434 * Remove the disabled flag from the primitive. 435 * Afterwards, the primitive is displayed normally and can be selected again. 436 * @return {@code true} if a change occurred 437 */ 438 public boolean unsetDisabledState() { 439 boolean locked = writeLock(); 440 try { 441 int oldFlags = flags; 442 updateFlagsNoLock(FLAG_DISABLED + FLAG_HIDE_IF_DISABLED, false); 443 return oldFlags != flags; 444 } finally { 445 writeUnlock(locked); 446 } 447 } 448 449 /** 450 * Set binary property used internally by the filter mechanism. 451 * @param isExplicit new "disabled type" flag value 452 */ 453 public void setDisabledType(boolean isExplicit) { 454 updateFlags(FLAG_DISABLED_TYPE, isExplicit); 455 } 456 457 /** 458 * Set binary property used internally by the filter mechanism. 459 * @param isExplicit new "hidden type" flag value 460 */ 461 public void setHiddenType(boolean isExplicit) { 462 updateFlags(FLAG_HIDDEN_TYPE, isExplicit); 463 } 464 465 /** 466 * Replies true, if this primitive is disabled. (E.g. a filter applies) 467 * @return {@code true} if this object has the "disabled" flag enabled 468 */ 469 public boolean isDisabled() { 470 return (flags & FLAG_DISABLED) != 0; 471 } 472 473 /** 474 * Replies true, if this primitive is disabled and marked as completely hidden on the map. 475 * @return {@code true} if this object has both the "disabled" and "hide if disabled" flags enabled 476 */ 477 public boolean isDisabledAndHidden() { 478 return ((flags & FLAG_DISABLED) != 0) && ((flags & FLAG_HIDE_IF_DISABLED) != 0); 479 } 480 481 /** 482 * Get binary property used internally by the filter mechanism. 483 * @return {@code true} if this object has the "hidden type" flag enabled 484 */ 485 public boolean getHiddenType() { 486 return (flags & FLAG_HIDDEN_TYPE) != 0; 487 } 488 489 /** 490 * Get binary property used internally by the filter mechanism. 491 * @return {@code true} if this object has the "disabled type" flag enabled 492 */ 493 public boolean getDisabledType() { 494 return (flags & FLAG_DISABLED_TYPE) != 0; 495 } 496 497 /** 498 * Determines if this object is selectable. 499 * <p> 500 * A primitive can be selected if all conditions are met: 501 * <ul> 502 * <li>it is drawable 503 * <li>it is not disabled (greyed out) by a filter. 504 * </ul> 505 * @return {@code true} if this object is selectable 506 */ 507 public boolean isSelectable() { 508 // not synchronized -> check disabled twice just to be sure we did not have a race condition. 509 return !isDisabled() && isDrawable() && !isDisabled(); 510 } 511 512 /** 513 * Determines if this object is drawable. 514 * <p> 515 * A primitive is complete if all conditions are met: 516 * <ul> 517 * <li>type and id is known 518 * <li>tags are known 519 * <li>it is not deleted 520 * <li>it is not hidden by a filter 521 * <li>for nodes: lat/lon are known 522 * <li>for ways: all nodes are known and complete 523 * <li>for relations: all members are known and complete 524 * </ul> 525 * @return {@code true} if this object is drawable 526 */ 527 public boolean isDrawable() { 528 return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_HIDE_IF_DISABLED)) == 0; 529 } 530 531 @Override 532 public void setModified(boolean modified) { 533 boolean locked = writeLock(); 534 try { 535 super.setModified(modified); 536 if (dataSet != null) { 537 dataSet.firePrimitiveFlagsChanged(this); 538 } 539 clearCachedStyle(); 540 } finally { 541 writeUnlock(locked); 542 } 543 } 544 545 @Override 546 public void setVisible(boolean visible) { 547 boolean locked = writeLock(); 548 try { 549 super.setVisible(visible); 550 clearCachedStyle(); 551 } finally { 552 writeUnlock(locked); 553 } 554 } 555 556 @Override 557 public void setDeleted(boolean deleted) { 558 boolean locked = writeLock(); 559 try { 560 super.setDeleted(deleted); 561 if (dataSet != null) { 562 if (deleted) { 563 dataSet.firePrimitivesRemoved(Collections.singleton(this), false); 564 } else { 565 dataSet.firePrimitivesAdded(Collections.singleton(this), false); 566 } 567 } 568 clearCachedStyle(); 569 } finally { 570 writeUnlock(locked); 571 } 572 } 573 574 @Override 575 protected final void setIncomplete(boolean incomplete) { 576 boolean locked = writeLock(); 577 try { 578 if (dataSet != null && incomplete != this.isIncomplete()) { 579 if (incomplete) { 580 dataSet.firePrimitivesRemoved(Collections.singletonList(this), true); 581 } else { 582 dataSet.firePrimitivesAdded(Collections.singletonList(this), true); 583 } 584 } 585 super.setIncomplete(incomplete); 586 } finally { 587 writeUnlock(locked); 588 } 589 } 590 591 /** 592 * Determines whether the primitive is selected 593 * @return whether the primitive is selected 594 * @see DataSet#isSelected(OsmPrimitive) 595 */ 596 public boolean isSelected() { 597 return dataSet != null && dataSet.isSelected(this); 598 } 599 600 /** 601 * Determines if this primitive is a member of a selected relation. 602 * @return {@code true} if this primitive is a member of a selected relation, {@code false} otherwise 603 */ 604 public boolean isMemberOfSelected() { 605 if (referrers == null) 606 return false; 607 if (referrers instanceof OsmPrimitive) 608 return referrers instanceof Relation && ((OsmPrimitive) referrers).isSelected(); 609 for (OsmPrimitive ref : (OsmPrimitive[]) referrers) { 610 if (ref instanceof Relation && ref.isSelected()) 611 return true; 612 } 613 return false; 614 } 615 616 /** 617 * Determines if this primitive is an outer member of a selected multipolygon relation. 618 * @return {@code true} if this primitive is an outer member of a selected multipolygon relation, {@code false} otherwise 619 * @since 7621 620 */ 621 public boolean isOuterMemberOfSelected() { 622 if (referrers == null) 623 return false; 624 if (referrers instanceof OsmPrimitive) { 625 return isOuterMemberOfMultipolygon((OsmPrimitive) referrers); 626 } 627 for (OsmPrimitive ref : (OsmPrimitive[]) referrers) { 628 if (isOuterMemberOfMultipolygon(ref)) 629 return true; 630 } 631 return false; 632 } 633 634 private boolean isOuterMemberOfMultipolygon(OsmPrimitive ref) { 635 if (ref instanceof Relation && ref.isSelected() && ((Relation) ref).isMultipolygon()) { 636 for (RelationMember rm : ((Relation) ref).getMembersFor(Collections.singleton(this))) { 637 if ("outer".equals(rm.getRole())) { 638 return true; 639 } 640 } 641 } 642 return false; 643 } 644 645 /** 646 * Updates the highlight flag for this primitive. 647 * @param highlighted The new highlight flag. 648 */ 649 public void setHighlighted(boolean highlighted) { 650 if (isHighlighted() != highlighted) { 651 updateFlags(FLAG_HIGHLIGHTED, highlighted); 652 if (dataSet != null) { 653 dataSet.fireHighlightingChanged(); 654 } 655 } 656 } 657 658 /** 659 * Checks if the highlight flag for this primitive was set 660 * @return The highlight flag. 661 */ 662 public boolean isHighlighted() { 663 return (flags & FLAG_HIGHLIGHTED) != 0; 664 } 665 666 /*--------------------------------------------------- 667 * WORK IN PROGRESS, UNINTERESTING AND DIRECTION KEYS 668 *--------------------------------------------------*/ 669 670 private static volatile Collection<String> workinprogress; 671 private static volatile Collection<String> uninteresting; 672 private static volatile Collection<String> discardable; 673 674 /** 675 * Returns a list of "uninteresting" keys that do not make an object 676 * "tagged". Entries that end with ':' are causing a whole namespace to be considered 677 * "uninteresting". Only the first level namespace is considered. 678 * Initialized by isUninterestingKey() 679 * @return The list of uninteresting keys. 680 */ 681 public static Collection<String> getUninterestingKeys() { 682 if (uninteresting == null) { 683 List<String> l = new LinkedList<>(Arrays.asList( 684 "source", "source_ref", "source:", "comment", 685 "watch", "watch:", "description", "attribution")); 686 l.addAll(getDiscardableKeys()); 687 l.addAll(getWorkInProgressKeys()); 688 uninteresting = Main.pref.getCollection("tags.uninteresting", l); 689 } 690 return uninteresting; 691 } 692 693 /** 694 * Returns a list of keys which have been deemed uninteresting to the point 695 * that they can be silently removed from data which is being edited. 696 * @return The list of discardable keys. 697 */ 698 public static Collection<String> getDiscardableKeys() { 699 if (discardable == null) { 700 discardable = Main.pref.getCollection("tags.discardable", 701 Arrays.asList( 702 "created_by", 703 "converted_by", 704 "geobase:datasetName", 705 "geobase:uuid", 706 "KSJ2:ADS", 707 "KSJ2:ARE", 708 "KSJ2:AdminArea", 709 "KSJ2:COP_label", 710 "KSJ2:DFD", 711 "KSJ2:INT", 712 "KSJ2:INT_label", 713 "KSJ2:LOC", 714 "KSJ2:LPN", 715 "KSJ2:OPC", 716 "KSJ2:PubFacAdmin", 717 "KSJ2:RAC", 718 "KSJ2:RAC_label", 719 "KSJ2:RIC", 720 "KSJ2:RIN", 721 "KSJ2:WSC", 722 "KSJ2:coordinate", 723 "KSJ2:curve_id", 724 "KSJ2:curve_type", 725 "KSJ2:filename", 726 "KSJ2:lake_id", 727 "KSJ2:lat", 728 "KSJ2:long", 729 "KSJ2:river_id", 730 "odbl", 731 "odbl:note", 732 "SK53_bulk:load", 733 "sub_sea:type", 734 "tiger:source", 735 "tiger:separated", 736 "tiger:tlid", 737 "tiger:upload_uuid", 738 "yh:LINE_NAME", 739 "yh:LINE_NUM", 740 "yh:STRUCTURE", 741 "yh:TOTYUMONO", 742 "yh:TYPE", 743 "yh:WIDTH", 744 "yh:WIDTH_RANK" 745 )); 746 } 747 return discardable; 748 } 749 750 /** 751 * Returns a list of "work in progress" keys that do not make an object 752 * "tagged" but "annotated". 753 * @return The list of work in progress keys. 754 * @since 5754 755 */ 756 public static Collection<String> getWorkInProgressKeys() { 757 if (workinprogress == null) { 758 workinprogress = Main.pref.getCollection("tags.workinprogress", 759 Arrays.asList("note", "fixme", "FIXME")); 760 } 761 return workinprogress; 762 } 763 764 /** 765 * Determines if key is considered "uninteresting". 766 * @param key The key to check 767 * @return true if key is considered "uninteresting". 768 */ 769 public static boolean isUninterestingKey(String key) { 770 getUninterestingKeys(); 771 if (uninteresting.contains(key)) 772 return true; 773 int pos = key.indexOf(':'); 774 if (pos > 0) 775 return uninteresting.contains(key.substring(0, pos + 1)); 776 return false; 777 } 778 779 /** 780 * Returns {@link #getKeys()} for which {@code key} does not fulfill {@link #isUninterestingKey}. 781 * @return A map of interesting tags 782 */ 783 public Map<String, String> getInterestingTags() { 784 Map<String, String> result = new HashMap<>(); 785 String[] keys = this.keys; 786 if (keys != null) { 787 for (int i = 0; i < keys.length; i += 2) { 788 if (!isUninterestingKey(keys[i])) { 789 result.put(keys[i], keys[i + 1]); 790 } 791 } 792 } 793 return result; 794 } 795 796 private static Match compileDirectionKeys(String prefName, String defaultValue) throws AssertionError { 797 try { 798 return SearchCompiler.compile(Main.pref.get(prefName, defaultValue)); 799 } catch (ParseError e) { 800 Main.error(e, "Unable to compile pattern for " + prefName + ", trying default pattern:"); 801 } 802 803 try { 804 return SearchCompiler.compile(defaultValue); 805 } catch (ParseError e2) { 806 throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2); 807 } 808 } 809 810 private void updateTagged() { 811 for (String key: keySet()) { 812 // 'area' is not really uninteresting (putting it in that list may have unpredictable side effects) 813 // but it's clearly not enough to consider an object as tagged (see #9261) 814 if (!isUninterestingKey(key) && !"area".equals(key)) { 815 updateFlagsNoLock(FLAG_TAGGED, true); 816 return; 817 } 818 } 819 updateFlagsNoLock(FLAG_TAGGED, false); 820 } 821 822 private void updateAnnotated() { 823 for (String key: keySet()) { 824 if (getWorkInProgressKeys().contains(key)) { 825 updateFlagsNoLock(FLAG_ANNOTATED, true); 826 return; 827 } 828 } 829 updateFlagsNoLock(FLAG_ANNOTATED, false); 830 } 831 832 /** 833 * Determines if this object is considered "tagged". To be "tagged", an object 834 * must have one or more "interesting" tags. "created_by" and "source" 835 * are typically considered "uninteresting" and do not make an object 836 * "tagged". 837 * @return true if this object is considered "tagged" 838 */ 839 public boolean isTagged() { 840 return (flags & FLAG_TAGGED) != 0; 841 } 842 843 /** 844 * Determines if this object is considered "annotated". To be "annotated", an object 845 * must have one or more "work in progress" tags, such as "note" or "fixme". 846 * @return true if this object is considered "annotated" 847 * @since 5754 848 */ 849 public boolean isAnnotated() { 850 return (flags & FLAG_ANNOTATED) != 0; 851 } 852 853 private void updateDirectionFlags() { 854 boolean hasDirections = false; 855 boolean directionReversed = false; 856 if (reversedDirectionKeys.match(this)) { 857 hasDirections = true; 858 directionReversed = true; 859 } 860 if (directionKeys.match(this)) { 861 hasDirections = true; 862 } 863 864 updateFlagsNoLock(FLAG_DIRECTION_REVERSED, directionReversed); 865 updateFlagsNoLock(FLAG_HAS_DIRECTIONS, hasDirections); 866 } 867 868 /** 869 * true if this object has direction dependent tags (e.g. oneway) 870 * @return {@code true} if this object has direction dependent tags 871 */ 872 public boolean hasDirectionKeys() { 873 return (flags & FLAG_HAS_DIRECTIONS) != 0; 874 } 875 876 /** 877 * true if this object has the "reversed diretion" flag enabled 878 * @return {@code true} if this object has the "reversed diretion" flag enabled 879 */ 880 public boolean reversedDirection() { 881 return (flags & FLAG_DIRECTION_REVERSED) != 0; 882 } 883 884 /*------------ 885 * Keys handling 886 ------------*/ 887 888 @Override 889 public final void setKeys(TagMap keys) { 890 boolean locked = writeLock(); 891 try { 892 super.setKeys(keys); 893 } finally { 894 writeUnlock(locked); 895 } 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 * @return {@code true} if this primitive is referred by at least n ways 1085 */ 1086 public final boolean isReferredByWays(int n) { 1087 // Count only referrers that are members of the same dataset (primitive can have some fake references, for example 1088 // when way is cloned 1089 Object referrers = this.referrers; 1090 if (referrers == null) return false; 1091 checkDataset(); 1092 if (referrers instanceof OsmPrimitive) 1093 return n <= 1 && referrers instanceof Way && ((OsmPrimitive) referrers).dataSet == dataSet; 1094 else { 1095 int counter = 0; 1096 for (OsmPrimitive o : (OsmPrimitive[]) referrers) { 1097 if (dataSet == o.dataSet && o instanceof Way) { 1098 if (++counter >= n) 1099 return true; 1100 } 1101 } 1102 return false; 1103 } 1104 } 1105 1106 /*----------------- 1107 * OTHER METHODS 1108 *----------------*/ 1109 1110 /** 1111 * Implementation of the visitor scheme. Subclasses have to call the correct 1112 * visitor function. 1113 * @param visitor The visitor from which the visit() function must be called. 1114 */ 1115 public abstract void accept(Visitor visitor); 1116 1117 /** 1118 * Get and write all attributes from the parameter. Does not fire any listener, so 1119 * use this only in the data initializing phase 1120 * @param other other primitive 1121 */ 1122 public void cloneFrom(OsmPrimitive other) { 1123 // write lock is provided by subclasses 1124 if (id != other.id && dataSet != null) 1125 throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset"); 1126 1127 super.cloneFrom(other); 1128 clearCachedStyle(); 1129 } 1130 1131 /** 1132 * Merges the technical and semantical attributes from <code>other</code> onto this. 1133 * 1134 * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code> 1135 * have an assigend OSM id, the IDs have to be the same. 1136 * 1137 * @param other the other primitive. Must not be null. 1138 * @throws IllegalArgumentException if other is null. 1139 * @throws DataIntegrityProblemException if either this is new and other is not, or other is new and this is not 1140 * @throws DataIntegrityProblemException if other isn't new and other.getId() != this.getId() 1141 */ 1142 public void mergeFrom(OsmPrimitive other) { 1143 boolean locked = writeLock(); 1144 try { 1145 CheckParameterUtil.ensureParameterNotNull(other, "other"); 1146 if (other.isNew() ^ isNew()) 1147 throw new DataIntegrityProblemException( 1148 tr("Cannot merge because either of the participating primitives is new and the other is not")); 1149 if (!other.isNew() && other.getId() != id) 1150 throw new DataIntegrityProblemException( 1151 tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId())); 1152 1153 setKeys(other.hasKeys() ? other.getKeys() : null); 1154 timestamp = other.timestamp; 1155 version = other.version; 1156 setIncomplete(other.isIncomplete()); 1157 flags = other.flags; 1158 user = other.user; 1159 changesetId = other.changesetId; 1160 } finally { 1161 writeUnlock(locked); 1162 } 1163 } 1164 1165 /** 1166 * Replies true if other isn't null and has the same interesting tags (key/value-pairs) as this. 1167 * 1168 * @param other the other object primitive 1169 * @return true if other isn't null and has the same interesting tags (key/value-pairs) as this. 1170 */ 1171 public boolean hasSameInterestingTags(OsmPrimitive other) { 1172 return (keys == null && other.keys == null) 1173 || getInterestingTags().equals(other.getInterestingTags()); 1174 } 1175 1176 /** 1177 * Replies true if this primitive and other are equal with respect to their semantic attributes. 1178 * <ol> 1179 * <li>equal id</li> 1180 * <li>both are complete or both are incomplete</li> 1181 * <li>both have the same tags</li> 1182 * </ol> 1183 * @param other other primitive to compare 1184 * @return true if this primitive and other are equal with respect to their semantic attributes. 1185 */ 1186 public final boolean hasEqualSemanticAttributes(OsmPrimitive other) { 1187 return hasEqualSemanticAttributes(other, true); 1188 } 1189 1190 boolean hasEqualSemanticAttributes(final OsmPrimitive other, final boolean testInterestingTagsOnly) { 1191 if (!isNew() && id != other.id) 1192 return false; 1193 if (isIncomplete() ^ other.isIncomplete()) // exclusive or operator for performance (see #7159) 1194 return false; 1195 return testInterestingTagsOnly ? hasSameInterestingTags(other) : getKeys().equals(other.getKeys()); 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 && Objects.equals(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.hasKeys() ? data.getKeys() : null); 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(hasKeys() ? getKeys() : null); 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 (this == obj) { 1311 return true; 1312 } else if (obj == null || getClass() != obj.getClass()) { 1313 return false; 1314 } else { 1315 OsmPrimitive that = (OsmPrimitive) obj; 1316 return id == that.id; 1317 } 1318 } 1319 1320 /** 1321 * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0. 1322 * 1323 * An primitive has the same hashcode as its incomplete counterpart. 1324 */ 1325 @Override 1326 public int hashCode() { 1327 return Long.hashCode(id); 1328 } 1329 1330 /** 1331 * Replies the display name of a primitive formatted by <code>formatter</code> 1332 * @param formatter formatter to use 1333 * 1334 * @return the display name 1335 */ 1336 public abstract String getDisplayName(NameFormatter formatter); 1337 1338 @Override 1339 public Collection<String> getTemplateKeys() { 1340 Collection<String> keySet = keySet(); 1341 List<String> result = new ArrayList<>(keySet.size() + 2); 1342 result.add(SPECIAL_VALUE_ID); 1343 result.add(SPECIAL_VALUE_LOCAL_NAME); 1344 result.addAll(keySet); 1345 return result; 1346 } 1347 1348 @Override 1349 public Object getTemplateValue(String name, boolean special) { 1350 if (special) { 1351 String lc = name.toLowerCase(Locale.ENGLISH); 1352 if (SPECIAL_VALUE_ID.equals(lc)) 1353 return getId(); 1354 else if (SPECIAL_VALUE_LOCAL_NAME.equals(lc)) 1355 return getLocalName(); 1356 else 1357 return null; 1358 1359 } else 1360 return getIgnoreCase(name); 1361 } 1362 1363 @Override 1364 public boolean evaluateCondition(Match condition) { 1365 return condition.match(this); 1366 } 1367 1368 /** 1369 * Replies the set of referring relations 1370 * @param primitives primitives to fetch relations from 1371 * 1372 * @return the set of referring relations 1373 */ 1374 public static Set<Relation> getParentRelations(Collection<? extends OsmPrimitive> primitives) { 1375 Set<Relation> ret = new HashSet<>(); 1376 for (OsmPrimitive w : primitives) { 1377 ret.addAll(OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class)); 1378 } 1379 return ret; 1380 } 1381 1382 /** 1383 * Determines if this primitive has tags denoting an area. 1384 * @return {@code true} if this primitive has tags denoting an area, {@code false} otherwise. 1385 * @since 6491 1386 */ 1387 public final boolean hasAreaTags() { 1388 return hasKey("landuse") 1389 || "yes".equals(get("area")) 1390 || "riverbank".equals(get("waterway")) 1391 || hasKey("natural") 1392 || hasKey("amenity") 1393 || hasKey("leisure") 1394 || hasKey("building") 1395 || hasKey("building:part"); 1396 } 1397 1398 /** 1399 * Determines if this primitive semantically concerns an area. 1400 * @return {@code true} if this primitive semantically concerns an area, according to its type, geometry and tags, {@code false} otherwise. 1401 * @since 6491 1402 */ 1403 public abstract boolean concernsArea(); 1404 1405 /** 1406 * Tests if this primitive lies outside of the downloaded area of its {@link DataSet}. 1407 * @return {@code true} if this primitive lies outside of the downloaded area 1408 */ 1409 public abstract boolean isOutsideDownloadArea(); 1410 1411 /** 1412 * Determines if this object is a relation and behaves as a multipolygon. 1413 * @return {@code true} if it is a real mutlipolygon or a boundary relation 1414 * @since 10716 1415 */ 1416 public boolean isMultipolygon() { 1417 return false; 1418 } 1419}