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