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