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