// License: GPL. Copyright 2007 by Immanuel Scholz and others package org.openstreetmap.josm.data.osm; import static org.openstreetmap.josm.tools.I18n.tr; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import java.util.concurrent.atomic.AtomicLong; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.search.SearchCompiler; import org.openstreetmap.josm.actions.search.SearchCompiler.Match; import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError; import org.openstreetmap.josm.data.osm.visitor.Visitor; import org.openstreetmap.josm.gui.mappaint.ElemStyle; import org.openstreetmap.josm.tools.CheckParameterUtil; import org.openstreetmap.josm.tools.Predicate; /** * An OSM primitive can be associated with a key/value pair. It can be created, deleted * and updated within the OSM-Server. * * Although OsmPrimitive is designed as a base class, it is not to be meant to subclass * it by any other than from the package {@link org.openstreetmap.josm.data.osm}. The available primitives are a fixed set that are given * by the server environment and not an extendible data stuff. * * @author imi */ abstract public class OsmPrimitive implements Comparable<OsmPrimitive>, Tagged, PrimitiveId { private static final AtomicLong idCounter = new AtomicLong(0); static long generateUniqueId() { return idCounter.decrementAndGet(); } /** * This flag shows, that the properties have been changed by the user * and on upload the object will be send to the server */ private static final int FLAG_MODIFIED = 1 << 0; /** * The visible flag indicates, that an object is marked * as deleted on the server. */ private static final int FLAG_VISIBLE = 1 << 1; /** * An object can be disabled by the filter mechanism. * Then it will show in a shade of grey on the map. * Disabled objects usually cannot be selected or modified * while the filter is active. */ private static final int FLAG_DISABLED = 1 << 2; /** * An object that was deleted by the user. * Deleted objects are usually hidden on the map and a request * for deletion will be send to the server on upload. * An object usually cannot be deleted if it has non-deleted * objects still referring to it. */ private static final int FLAG_DELETED = 1 << 3; /** * An object can be filtered by the filter mechanism. * Then it will be hidden on the map and usually * cannot be selected or modified while the filter is active. */ private static final int FLAG_FILTERED = 1 << 4; /** * This flag is set if the primitive is a way and * according to the tags, the direction of the way is important. * (e.g. one way street.) */ private static final int FLAG_HAS_DIRECTIONS = 1 << 5; /** * If the primitive is tagged. * Some trivial tags like source=* are ignored here. */ private static final int FLAG_TAGGED = 1 << 6; /** * This flag is only relevant if FLAG_HAS_DIRECTIONS is set. * It shows, that direction of the arrows should be reversed. * (E.g. oneway=-1.) */ private static final int FLAG_DIRECTION_REVERSED = 1 << 7; /** * When hovering over ways and nodes in add mode, the * "target" objects are visually highlighted. This flag indicates * that the primitive is currently highlighted. */ private static final int FLAG_HIGHLIGHTED = 1 << 8; /** * A primitive is incomplete if we know its id and type, but nothing more. * Typically some members of a relation are incomplete until they are * fetched from the server. */ private static final int FLAG_INCOMPLETE = 1 << 9; /** * Replies the sub-collection of {@see OsmPrimitive}s of type <code>type</code> present in * another collection of {@see OsmPrimitive}s. The result collection is a list. * * If <code>list</code> is null, replies an empty list. * * @param <T> * @param list the original list * @param type the type to filter for * @return the sub-list of OSM primitives of type <code>type</code> */ static public <T extends OsmPrimitive> List<T> getFilteredList(Collection<OsmPrimitive> list, Class<T> type) { if (list == null) return Collections.emptyList(); List<T> ret = new LinkedList<T>(); for(OsmPrimitive p: list) { if (type.isInstance(p)) { ret.add(type.cast(p)); } } return ret; } /** * Replies the sub-collection of {@see OsmPrimitive}s of type <code>type</code> present in * another collection of {@see OsmPrimitive}s. The result collection is a set. * * If <code>list</code> is null, replies an empty set. * * @param <T> * @param list the original collection * @param type the type to filter for * @return the sub-set of OSM primitives of type <code>type</code> */ static public <T extends OsmPrimitive> LinkedHashSet<T> getFilteredSet(Collection<OsmPrimitive> set, Class<T> type) { LinkedHashSet<T> ret = new LinkedHashSet<T>(); if (set != null) { for(OsmPrimitive p: set) { if (type.isInstance(p)) { ret.add(type.cast(p)); } } } return ret; } /** * Replies the collection of referring primitives for the primitives in <code>primitives</code>. * * @param primitives the collection of primitives. * @return the collection of referring primitives for the primitives in <code>primitives</code>; * empty set if primitives is null or if there are no referring primitives */ static public Set<OsmPrimitive> getReferrer(Collection<? extends OsmPrimitive> primitives) { HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>(); if (primitives == null || primitives.isEmpty()) return ret; for (OsmPrimitive p: primitives) { ret.addAll(p.getReferrers()); } return ret; } /* mappaint data */ public ElemStyle mappaintStyle = null; public int mappaintDrawnCode = 0; /* This should not be called from outside. Fixing the UI to add relevant get/set functions calling this implicitely is preferred, so we can have transparent cache handling in the future. */ protected void clearCached() { mappaintDrawnCode = 0; mappaintStyle = null; } /* end of mappaint data */ /** * Unique identifier in OSM. This is used to identify objects on the server. * An id of 0 means an unknown id. The object has not been uploaded yet to * know what id it will get. * */ private long id = 0; /** the parent dataset */ private DataSet dataSet; /** * This method should never ever by called from somewhere else than Dataset.addPrimitive or removePrimitive methods * @param dataSet */ void setDataset(DataSet dataSet) { if (this.dataSet != null && dataSet != null && this.dataSet != dataSet) throw new DataIntegrityProblemException("Primitive cannot be included in more than one Dataset"); this.dataSet = dataSet; } /** * * @return DataSet this primitive is part of. */ public DataSet getDataSet() { return dataSet; } /** * Throws exception if primitive is not part of the dataset */ public void checkDataset() { if (dataSet == null) throw new DataIntegrityProblemException("Primitive must be part of the dataset: " + toString()); } private volatile short flags = FLAG_VISIBLE; // visible per default /** * User that last modified this primitive, as specified by the server. * Never changed by JOSM. */ private User user = null; /** * Contains the version number as returned by the API. Needed to * ensure update consistency */ private int version = 0; /** * The id of the changeset this primitive was last uploaded to. * 0 if it wasn't uploaded to a changeset yet of if the changeset * id isn't known. */ private int changesetId; /** * Creates a new primitive for the given id. * * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or * positive number. * * @param id the id * @param allowNegativeId * @throws IllegalArgumentException thrown if id < 0 and allowNegativeId is false */ protected OsmPrimitive(long id, boolean allowNegativeId) throws IllegalArgumentException { if (allowNegativeId) { this.id = id; } else { if (id < 0) throw new IllegalArgumentException(MessageFormat.format("Expected ID >= 0. Got {0}.", id)); else if (id == 0) { this.id = generateUniqueId(); } else { this.id = id; } } this.version = 0; this.setIncomplete(id > 0); } /** * Creates a new primitive for the given id and version. * * If allowNegativeId is set, provided id can be < 0 and will be set to primitive without any processing. * If allowNegativeId is not set, then id will have to be 0 (in that case new unique id will be generated) or * positive number. * * If id is not > 0 version is ignored and set to 0. * * @param id * @param version * @param allowNegativeId * @throws IllegalArgumentException thrown if id < 0 and allowNegativeId is false */ protected OsmPrimitive(long id, int version, boolean allowNegativeId) throws IllegalArgumentException { this(id, allowNegativeId); this.version = (id > 0 ? version : 0); setIncomplete(id > 0 && version == 0); } /* ------------------------------------------------------------------------------------ */ /* accessors */ /* ------------------------------------------------------------------------------------ */ /** * Sets whether this primitive is disabled or not. * * @param disabled true, if this primitive is disabled; false, otherwise */ public void setDisabled(boolean disabled) { if (disabled) { flags |= FLAG_DISABLED; } else { flags &= ~FLAG_DISABLED; } } /** * Replies true, if this primitive is disabled. * * @return true, if this primitive is disabled */ public boolean isDisabled() { return (flags & FLAG_DISABLED) != 0; } /** * Sets whether this primitive is filtered out or not. * * @param filtered true, if this primitive is filtered out; false, otherwise */ public void setFiltered(boolean filtered) { if (filtered) { flags |= FLAG_FILTERED; } else { flags &= ~FLAG_FILTERED; } } /** * Replies true, if this primitive is filtered out. * * @return true, if this primitive is filtered out */ public boolean isFiltered() { return (flags & FLAG_FILTERED) != 0; } /** * Marks this primitive as being modified. * * @param modified true, if this primitive is to be modified */ public void setModified(boolean modified) { if (modified) { flags |= FLAG_MODIFIED; } else { flags &= ~FLAG_MODIFIED; } } /** * Replies <code>true</code> if the object has been modified since it was loaded from * the server. In this case, on next upload, this object will be updated. * * Deleted objects are deleted from the server. If the objects are added (id=0), * the modified is ignored and the object is added to the server. * * @return <code>true</code> if the object has been modified since it was loaded from * the server */ public boolean isModified() { return (flags & FLAG_MODIFIED) != 0; } /** * Replies <code>true</code>, if the object has been deleted. * * @return <code>true</code>, if the object has been deleted. * @see #setDeleted(boolean) */ public boolean isDeleted() { return (flags & FLAG_DELETED) != 0; } /** * Replies <code>true</code>, if the object is usable. * * @return <code>true</code>, if the object is unusable. * @see #delete(boolean) */ public boolean isUsable() { return (flags & (FLAG_DELETED + FLAG_INCOMPLETE)) == 0; } public boolean isSelectable() { return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_DISABLED + FLAG_FILTERED)) == 0; } public boolean isDrawable() { return (flags & (FLAG_DELETED + FLAG_INCOMPLETE + FLAG_FILTERED)) == 0; } /** * Some predicates, that describe conditions on primitives. */ public static Predicate<OsmPrimitive> isUsablePredicate = new Predicate<OsmPrimitive>() { public boolean evaluate(OsmPrimitive primitive) { return primitive.isUsable(); } }; public static Predicate<OsmPrimitive> isSelectablePredicate = new Predicate<OsmPrimitive>() { public boolean evaluate(OsmPrimitive primitive) { return primitive.isSelectable(); } }; public static Predicate<OsmPrimitive> nonDeletedPredicate = new Predicate<OsmPrimitive>() { public boolean evaluate(OsmPrimitive primitive) { return primitive.isVisible() && !primitive.isDeleted(); } }; public static Predicate<OsmPrimitive> nonDeletedCompletePredicate = new Predicate<OsmPrimitive>() { public boolean evaluate(OsmPrimitive primitive) { return primitive.isVisible() && !primitive.isDeleted() && !primitive.isIncomplete(); } }; public static Predicate<OsmPrimitive> nonDeletedPhysicalPredicate = new Predicate<OsmPrimitive>() { public boolean evaluate(OsmPrimitive primitive) { return primitive.isVisible() && !primitive.isDeleted() && !primitive.isIncomplete() && !(primitive instanceof Relation); } }; public static Predicate<OsmPrimitive> modifiedPredicate = new Predicate<OsmPrimitive>() { public boolean evaluate(OsmPrimitive primitive) { return primitive.isVisible() && primitive.isModified(); } }; /** * Replies true if this primitive is either unknown to the server (i.e. its id * is 0) or it is known to the server and it hasn't be deleted on the server. * Replies false, if this primitive is known on the server and has been deleted * on the server. * * @see #setVisible(boolean) */ public boolean isVisible() { return (flags & FLAG_VISIBLE) != 0; } /** * Sets whether this primitive is visible, i.e. whether it is known on the server * and not deleted on the server. * * @see #isVisible() * @throws IllegalStateException thrown if visible is set to false on an primitive with * id==0 */ public void setVisible(boolean visible) throws IllegalStateException{ if (isNew() && visible == false) throw new IllegalStateException(tr("A primitive with ID = 0 cannot be invisible.")); if (visible) { flags |= FLAG_VISIBLE; } else { flags &= ~FLAG_VISIBLE; } } /** * Replies the version number as returned by the API. The version is 0 if the id is 0 or * if this primitive is incomplete. * * @see #setVersion(int) */ public long getVersion() { return version; } /** * Replies the id of this primitive. * * @return the id of this primitive. */ public long getId() { return id >= 0?id:0; } /** * * @return Osm id if primitive already exists on the server. Unique negative value if primitive is new */ public long getUniqueId() { return id; } /** * * @return True if primitive is new (not yet uploaded the server, id <= 0) */ public boolean isNew() { return id <= 0; } /** * Sets the id and the version of this primitive if it is known to the OSM API. * * Since we know the id and its version it can't be incomplete anymore. incomplete * is set to false. * * @param id the id. > 0 required * @param version the version > 0 required * @throws IllegalArgumentException thrown if id <= 0 * @throws IllegalArgumentException thrown if version <= 0 * @throws DataIntegrityProblemException If id is changed and primitive was already added to the dataset */ public void setOsmId(long id, int version) { if (id <= 0) throw new IllegalArgumentException(tr("ID > 0 expected. Got {0}.", id)); if (version <= 0) throw new IllegalArgumentException(tr("Version > 0 expected. Got {0}.", version)); if (dataSet != null && id != this.id) { DataSet datasetCopy = dataSet; // Reindex primitive datasetCopy.removePrimitive(this); this.id = id; datasetCopy.addPrimitive(this); } this.id = id; this.version = version; this.setIncomplete(false); } /** * Clears the id and version known to the OSM API. The id and the version is set to 0. * incomplete is set to false. It's preferred to use copy constructor with clearId set to true instead * of calling this method. * * <strong>Caution</strong>: Do not use this method on primitives which are already added to a {@see DataSet}. * * @throws DataIntegrityProblemException If primitive was already added to the dataset */ public void clearOsmId() { if (dataSet != null) throw new DataIntegrityProblemException("Method cannot be called after primitive was added to the dataset"); this.id = generateUniqueId(); this.version = 0; this.changesetId = 0; // reset changeset id on a new object this.setIncomplete(false); } public void setTimestamp(Date timestamp) { this.timestamp = (int)(timestamp.getTime() / 1000); } /** * Time of last modification to this object. This is not set by JOSM but * read from the server and delivered back to the server unmodified. It is * used to check against edit conflicts. * */ public Date getTimestamp() { return new Date(timestamp * 1000l); } public boolean isTimestampEmpty() { return timestamp == 0; } private int timestamp; private static volatile Collection<String> uninteresting = null; /** * Contains a list of "uninteresting" keys that do not make an object * "tagged". Entries that end with ':' are causing a whole namespace to be considered * "uninteresting". Only the first level namespace is considered. * Initialized by isUninterestingKey() */ public static Collection<String> getUninterestingKeys() { if (uninteresting == null) { uninteresting = Main.pref.getCollection("tags.uninteresting", Arrays.asList(new String[]{"source", "source_ref", "source:", "note", "comment", "converted_by", "created_by", "watch", "watch:"})); } return uninteresting; } /** * Returns true if key is considered "uninteresting". */ public static boolean isUninterestingKey(String key) { getUninterestingKeys(); if (uninteresting.contains(key)) return true; int pos = key.indexOf(':'); if (pos > 0) return uninteresting.contains(key.substring(0, pos + 1)); return false; } private static volatile Match directionKeys = null; private static volatile Match reversedDirectionKeys = null; /** * Contains a list of direction-dependent keys that make an object * direction dependent. * Initialized by checkDirectionTagged() */ static { // Legacy support - convert list of keys to search pattern if (Main.pref.isCollection("tags.direction", false)) { System.out.println("Collection of keys in tags.direction is no longer supported, value will converted to search pattern"); Collection<String> keys = Main.pref.getCollection("tags.direction", null); StringBuilder builder = new StringBuilder(); for (String key:keys) { builder.append(key); builder.append("=* | "); } builder.delete(builder.length() - 3, builder.length()); Main.pref.put("tags.direction", builder.toString()); } // FIXME: incline=\"-*\" search pattern does not work. String reversedDirectionDefault = "oneway=\"-1\" | incline=down | incline=\"-*\""; String directionDefault = "oneway? | incline=* | aerialway=* | "+ "waterway=stream | waterway=river | waterway=canal | waterway=drain | waterway=rapids | "+ "\"piste:type\"=downhill | \"piste:type\"=sled | man_made=\"piste:halfpipe\" | "+ "junction=roundabout"; try { reversedDirectionKeys = SearchCompiler.compile(Main.pref.get("tags.reversed_direction", reversedDirectionDefault), false, false); } catch (ParseError e) { System.err.println("Unable to compile pattern for tags.reversed_direction, trying default pattern: " + e.getMessage()); try { reversedDirectionKeys = SearchCompiler.compile(reversedDirectionDefault, false, false); } catch (ParseError e2) { throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage()); } } try { directionKeys = SearchCompiler.compile(Main.pref.get("tags.direction", directionDefault), false, false); } catch (ParseError e) { System.err.println("Unable to compile pattern for tags.direction, trying default pattern: " + e.getMessage()); try { directionKeys = SearchCompiler.compile(directionDefault, false, false); } catch (ParseError e2) { throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage()); } } } /** * Replies a list of direction-dependent keys that make an object * direction dependent. * * @return a list of direction-dependent keys that make an object * direction dependent. */ @Deprecated public static Collection<String> getDirectionKeys() { return Main.pref.getCollection("tags.direction", Arrays.asList("oneway","incline","incline_steep","aerialway")); } /** * Implementation of the visitor scheme. Subclasses have to call the correct * visitor function. * @param visitor The visitor from which the visit() function must be called. */ abstract public void visit(Visitor visitor); /** * Sets whether this primitive is deleted or not. * * Also marks this primitive as modified if deleted is true. * * @param deleted true, if this primitive is deleted; false, otherwise */ public void setDeleted(boolean deleted) { if (deleted) { flags |= FLAG_DELETED; } else { flags &= ~FLAG_DELETED; } setModified(deleted); if (dataSet != null) { if (deleted) { dataSet.firePrimitivesRemoved(Collections.singleton(this), false); } else { dataSet.firePrimitivesAdded(Collections.singleton(this), false); } } } /** * Replies the user who has last touched this object. May be null. * * @return the user who has last touched this object. May be null. */ public User getUser() { return user; } /** * Sets the user who has last touched this object. * * @param user the user */ public void setUser(User user) { this.user = user; } /** * Replies the id of the changeset this primitive was last uploaded to. * 0 if this primitive wasn't uploaded to a changeset yet or if the * changeset isn't known. * * @return the id of the changeset this primitive was last uploaded to. */ public int getChangesetId() { return changesetId; } /** * Sets the changeset id of this primitive. Can't be set on a new * primitive. * * @param changesetId the id. >= 0 required. * @throws IllegalStateException thrown if this primitive is new. * @throws IllegalArgumentException thrown if id < 0 */ public void setChangesetId(int changesetId) throws IllegalStateException, IllegalArgumentException { if (this.changesetId == changesetId) return; if (changesetId < 0) throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' >= 0 expected, got {1}", "changesetId", changesetId)); if (isNew() && changesetId > 0) throw new IllegalStateException(tr("Cannot assign a changesetId > 0 to a new primitive. Value of changesetId is {0}", changesetId)); int old = this.changesetId; this.changesetId = changesetId; if (dataSet != null) { dataSet.fireChangesetIdChanged(this, old, changesetId); } } /** * Equal, if the id (and class) is equal. * * An primitive is equal to its incomplete counter part. */ @Override public boolean equals(Object obj) { if (obj instanceof OsmPrimitive) return ((OsmPrimitive)obj).id == id && obj.getClass() == getClass(); return false; } /** * Return the id plus the class type encoded as hashcode or super's hashcode if id is 0. * * An primitive has the same hashcode as its incomplete counterpart. */ @Override public final int hashCode() { return (int)id; } /*------------ * Keys handling ------------*/ /** * The key/value list for this primitive. * */ private String[] keys; /** * Replies the map of key/value pairs. Never replies null. The map can be empty, though. * * @return tags of this primitive. Changes made in returned map are not mapped * back to the primitive, use setKeys() to modify the keys * @since 1924 */ public Map<String, String> getKeys() { Map<String, String> result = new HashMap<String, String>(); if (keys != null) { for (int i=0; i<keys.length ; i+=2) { result.put(keys[i], keys[i + 1]); } } return result; } /** * Sets the keys of this primitives to the key/value pairs in <code>keys</code>. * If <code>keys</code> is null removes all existing key/value pairs. * * @param keys the key/value pairs to set. If null, removes all existing key/value pairs. * @since 1924 */ public void setKeys(Map<String, String> keys) { Map<String, String> originalKeys = getKeys(); if (keys == null || keys.isEmpty()) { this.keys = null; keysChangedImpl(originalKeys); return; } String[] newKeys = new String[keys.size() * 2]; int index = 0; for (Entry<String, String> entry:keys.entrySet()) { newKeys[index++] = entry.getKey(); newKeys[index++] = entry.getValue(); } this.keys = newKeys; keysChangedImpl(originalKeys); } /** * Set the given value to the given key. If key is null, does nothing. If value is null, * removes the key and behaves like {@see #remove(String)}. * * @param key The key, for which the value is to be set. Can be null, does nothing in this case. * @param value The value for the key. If null, removes the respective key/value pair. * * @see #remove(String) */ public final void put(String key, String value) { Map<String, String> originalKeys = getKeys(); if (key == null) return; else if (value == null) { remove(key); } else if (keys == null){ keys = new String[] {key, value}; keysChangedImpl(originalKeys); } else { for (int i=0; i<keys.length;i+=2) { if (keys[i].equals(key)) { keys[i+1] = value; keysChangedImpl(originalKeys); return; } } String[] newKeys = new String[keys.length + 2]; for (int i=0; i< keys.length;i+=2) { newKeys[i] = keys[i]; newKeys[i+1] = keys[i+1]; } newKeys[keys.length] = key; newKeys[keys.length + 1] = value; keys = newKeys; keysChangedImpl(originalKeys); } } /** * Remove the given key from the list * * @param key the key to be removed. Ignored, if key is null. */ public final void remove(String key) { if (key == null || keys == null) return; if (!hasKey(key)) return; Map<String, String> originalKeys = getKeys(); if (keys.length == 2) { keys = null; keysChangedImpl(originalKeys); return; } String[] newKeys = new String[keys.length - 2]; int j=0; for (int i=0; i < keys.length; i+=2) { if (!keys[i].equals(key)) { newKeys[j++] = keys[i]; newKeys[j++] = keys[i+1]; } } keys = newKeys; keysChangedImpl(originalKeys); } /** * Removes all keys from this primitive. * * @since 1843 */ public final void removeAll() { if (keys != null) { Map<String, String> originalKeys = getKeys(); keys = null; keysChangedImpl(originalKeys); } } /** * Replies the value for key <code>key</code>. Replies null, if <code>key</code> is null. * Replies null, if there is no value for the given key. * * @param key the key. Can be null, replies null in this case. * @return the value for key <code>key</code>. */ public final String get(String key) { if (key == null) return null; if (keys == null) return null; for (int i=0; i<keys.length;i+=2) { if (keys[i].equals(key)) return keys[i+1]; } return null; } public final Collection<String> keySet() { if (keys == null) return Collections.emptySet(); Set<String> result = new HashSet<String>(keys.length / 2); for (int i=0; i<keys.length; i+=2) { result.add(keys[i]); } return result; } /** * Replies true, if the map of key/value pairs of this primitive is not empty. * * @return true, if the map of key/value pairs of this primitive is not empty; false * otherwise */ public final boolean hasKeys() { return keys != null; } private void keysChangedImpl(Map<String, String> originalKeys) { clearCached(); updateDirectionFlags(); updateTagged(); if (dataSet != null) { dataSet.fireTagsChanged(this, originalKeys); } } /** * Replies true if this primitive has a tag with key <code>key</code> * * @param key the key * @return true, if his primitive has a tag with key <code>key</code> */ public boolean hasKey(String key) { if (key == null) return false; if (keys == null) return false; for (int i=0; i< keys.length;i+=2) { if (keys[i].equals(key)) return true; } return false; } /** * Replies true if other isn't null and has the same tags (key/value-pairs) as this. * * @param other the other object primitive * @return true if other isn't null and has the same tags (key/value-pairs) as this. */ public boolean hasSameTags(OsmPrimitive other) { return getKeys().equals(other.getKeys()); } /*------------ * Referrers ------------*/ private Object referrers; /** * Add new referrer. If referrer is already included then no action is taken * @param referrer */ protected void addReferrer(OsmPrimitive referrer) { // Based on methods from josm-ng if (referrers == null) { referrers = referrer; } else if (referrers instanceof OsmPrimitive) { if (referrers != referrer) { referrers = new OsmPrimitive[] { (OsmPrimitive)referrers, referrer }; } } else { for (OsmPrimitive primitive:(OsmPrimitive[])referrers) { if (primitive == referrer) return; } OsmPrimitive[] orig = (OsmPrimitive[])referrers; OsmPrimitive[] bigger = new OsmPrimitive[orig.length+1]; System.arraycopy(orig, 0, bigger, 0, orig.length); bigger[orig.length] = referrer; referrers = bigger; } } /** * Remove referrer. No action is taken if referrer is not registered * @param referrer */ protected void removeReferrer(OsmPrimitive referrer) { // Based on methods from josm-ng if (referrers instanceof OsmPrimitive) { if (referrers == referrer) { referrers = null; } } else if (referrers instanceof OsmPrimitive[]) { OsmPrimitive[] orig = (OsmPrimitive[])referrers; int idx = -1; for (int i=0; i<orig.length; i++) { if (orig[i] == referrer) { idx = i; break; } } if (idx == -1) return; if (orig.length == 2) { referrers = orig[1-idx]; // idx is either 0 or 1, take the other } else { // downsize the array OsmPrimitive[] smaller = new OsmPrimitive[orig.length-1]; System.arraycopy(orig, 0, smaller, 0, idx); System.arraycopy(orig, idx+1, smaller, idx, smaller.length-idx); referrers = smaller; } } } /** * Find primitives that reference this primitive. Returns only primitives that are included in the same * dataset as this primitive. <br> * * For example following code will add wnew as referer to all nodes of existingWay, but this method will * not return wnew because it's not part of the dataset <br> * * <code>Way wnew = new Way(existingWay)</code> * * @return a collection of all primitives that reference this primitive. */ public final List<OsmPrimitive> getReferrers() { // Method copied from OsmPrimitive in josm-ng // Returns only referrers that are members of the same dataset (primitive can have some fake references, for example // when way is cloned checkDataset(); List<OsmPrimitive> result = new ArrayList<OsmPrimitive>(); if (referrers != null) { if (referrers instanceof OsmPrimitive) { OsmPrimitive ref = (OsmPrimitive)referrers; if (ref.dataSet == dataSet) { result.add(ref); } } else { for (OsmPrimitive o:(OsmPrimitive[])referrers) { if (dataSet == o.dataSet) { result.add(o); } } } } return result; } /** * Get and write all attributes from the parameter. Does not fire any listener, so * use this only in the data initializing phase */ public void cloneFrom(OsmPrimitive other) { if (id != other.id && dataSet != null) throw new DataIntegrityProblemException("Osm id cannot be changed after primitive was added to the dataset"); setKeys(other.getKeys()); id = other.id; if (id <=0) { // reset version and changeset id version = 0; changesetId = 0; } timestamp = other.timestamp; if (id > 0) { version = other.version; } setIncomplete(other.isIncomplete()); flags = other.flags; user= other.user; if (id > 0 && other.changesetId > 0) { // #4208: sometimes we cloned from other with id < 0 *and* // an assigned changeset id. Don't know why yet. For primitives // with id < 0 we don't propagate the changeset id any more. // setChangesetId(other.changesetId); } clearCached(); } /** * Merges the technical and semantical attributes from <code>other</code> onto this. * * Both this and other must be new, or both must be assigned an OSM ID. If both this and <code>other</code> * have an assigend OSM id, the IDs have to be the same. * * @param other the other primitive. Must not be null. * @throws IllegalArgumentException thrown if other is null. * @throws DataIntegrityProblemException thrown if either this is new and other is not, or other is new and this is not * @throws DataIntegrityProblemException thrown if other isn't new and other.getId() != this.getId() */ public void mergeFrom(OsmPrimitive other) { CheckParameterUtil.ensureParameterNotNull(other, "other"); if (other.isNew() ^ isNew()) throw new DataIntegrityProblemException(tr("Cannot merge because either of the participating primitives is new and the other is not")); if (! other.isNew() && other.getId() != id) throw new DataIntegrityProblemException(tr("Cannot merge primitives with different ids. This id is {0}, the other is {1}", id, other.getId())); setKeys(other.getKeys()); timestamp = other.timestamp; version = other.version; setIncomplete(other.isIncomplete()); flags = other.flags; user= other.user; changesetId = other.changesetId; } /** * Replies true if this primitive and other are equal with respect to their * semantic attributes. * <ol> * <li>equal id</ol> * <li>both are complete or both are incomplete</li> * <li>both have the same tags</li> * </ol> * @param other * @return true if this primitive and other are equal with respect to their * semantic attributes. */ public boolean hasEqualSemanticAttributes(OsmPrimitive other) { if (!isNew() && id != other.id) return false; if (isIncomplete() && ! other.isIncomplete() || !isIncomplete() && other.isIncomplete()) return false; // can't do an equals check on the internal keys array because it is not ordered // return hasSameTags(other); } /** * Replies true if this primitive and other are equal with respect to their * technical attributes. The attributes: * <ol> * <li>deleted</ol> * <li>modified</ol> * <li>timestamp</ol> * <li>version</ol> * <li>visible</ol> * <li>user</ol> * </ol> * have to be equal * @param other the other primitive * @return true if this primitive and other are equal with respect to their * technical attributes */ public boolean hasEqualTechnicalAttributes(OsmPrimitive other) { if (other == null) return false; return isDeleted() == other.isDeleted() && isModified() == other.isModified() && timestamp == other.timestamp && version == other.version && isVisible() == other.isVisible() && (user == null ? other.user==null : user==other.user) && changesetId == other.changesetId; } private void updateTagged() { if (keys != null) { for (String key: keySet()) { if (!isUninterestingKey(key)) { flags |= FLAG_TAGGED; return; } } } flags &= ~FLAG_TAGGED; } /** * true if this object is considered "tagged". To be "tagged", an object * must have one or more "interesting" tags. "created_by" and "source" * are typically considered "uninteresting" and do not make an object * "tagged". */ public boolean isTagged() { return (flags & FLAG_TAGGED) != 0; } private void updateDirectionFlags() { boolean hasDirections = false; boolean directionReversed = false; if (reversedDirectionKeys.match(this)) { hasDirections = true; directionReversed = true; } if (directionKeys.match(this)) { hasDirections = true; } if (directionReversed) { flags |= FLAG_DIRECTION_REVERSED; } else { flags &= ~FLAG_DIRECTION_REVERSED; } if (hasDirections) { flags |= FLAG_HAS_DIRECTIONS; } else { flags &= ~FLAG_HAS_DIRECTIONS; } } /** * true if this object has direction dependent tags (e.g. oneway) */ public boolean hasDirectionKeys() { return (flags & FLAG_HAS_DIRECTIONS) != 0; } public boolean reversedDirection() { return (flags & FLAG_DIRECTION_REVERSED) != 0; } /** * Replies the name of this primitive. The default implementation replies the value * of the tag <tt>name</tt> or null, if this tag is not present. * * @return the name of this primitive */ public String getName() { if (get("name") != null) return get("name"); return null; } /** * Replies the a localized name for this primitive given by the value of the tags (in this order) * <ul> * <li>name:lang_COUNTRY_Variant of the current locale</li> * <li>name:lang_COUNTRY of the current locale</li> * <li>name:lang of the current locale</li> * <li>name of the current locale</li> * </ul> * * null, if no such tag exists * * @return the name of this primitive */ public String getLocalName() { String key = "name:" + Locale.getDefault().toString(); if (get(key) != null) return get(key); key = "name:" + Locale.getDefault().getLanguage() + "_" + Locale.getDefault().getCountry(); if (get(key) != null) return get(key); key = "name:" + Locale.getDefault().getLanguage(); if (get(key) != null) return get(key); return getName(); } /** * Replies the display name of a primitive formatted by <code>formatter</code> * * @return the display name */ public abstract String getDisplayName(NameFormatter formatter); /** * Loads (clone) this primitive from provided PrimitiveData * @param data */ public void load(PrimitiveData data) { setKeys(data.getKeys()); setTimestamp(data.getTimestamp()); user = data.getUser(); setChangesetId(data.getChangesetId()); setDeleted(data.isDeleted()); setModified(data.isModified()); setVisible(data.isVisible()); setIncomplete(data.isIncomplete()); version = data.getVersion(); } /** * Save parameters of this primitive to the transport object * @return */ public abstract PrimitiveData save(); protected void saveCommonAttributes(PrimitiveData data) { data.setId(id); data.getKeys().clear(); data.getKeys().putAll(getKeys()); data.setTimestamp(getTimestamp()); data.setUser(user); data.setDeleted(isDeleted()); data.setModified(isModified()); data.setVisible(isVisible()); data.setIncomplete(isIncomplete()); data.setChangesetId(changesetId); data.setVersion(version); } protected String getFlagsAsString() { StringBuilder builder = new StringBuilder(); if (isIncomplete()) { builder.append("I"); } if (isModified()) { builder.append("M"); } if (isVisible()) { builder.append("V"); } if (isDeleted()) { builder.append("D"); } if (isFiltered()) { builder.append("f"); } if (isDisabled()) { builder.append("d"); } if (isTagged()) { builder.append("T"); } if (hasDirectionKeys()) { if (reversedDirection()) { builder.append("<"); } else { builder.append(">"); } } return builder.toString(); } public abstract BBox getBBox(); /** * Called by Dataset to update cached position information of primitive (bbox, cached EarthNorth, ...) */ public abstract void updatePosition(); /** * Replies the unique primitive id for this primitive * * @return the unique primitive id for this primitive */ public PrimitiveId getPrimitiveId() { return new SimplePrimitiveId(getUniqueId(), getType()); } /** * If set to true, this object is incomplete, which means only the id * and type is known (type is the objects instance class) */ private void setIncomplete(boolean incomplete) { if (dataSet != null && incomplete != this.isIncomplete()) { if (incomplete) { dataSet.firePrimitivesRemoved(Collections.singletonList(this), true); } else { dataSet.firePrimitivesAdded(Collections.singletonList(this), true); } } if (incomplete) { flags |= FLAG_INCOMPLETE; } else { flags &= ~FLAG_INCOMPLETE; } } public boolean isIncomplete() { return (flags & FLAG_INCOMPLETE) != 0; } public boolean isSelected() { return dataSet != null && dataSet.isSelected(this); } public void setHighlighted(boolean highlighted) { if (isHighlighted() != highlighted) { if (highlighted) { flags |= FLAG_HIGHLIGHTED; } else { flags &= ~FLAG_HIGHLIGHTED; } if (dataSet != null) { dataSet.fireHighlightingChanged(this); } } } public boolean isHighlighted() { return (flags & FLAG_HIGHLIGHTED) != 0; } }