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