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