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