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