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