001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.osm;
003
004import java.util.ArrayList;
005import java.util.Arrays;
006import java.util.Collection;
007import java.util.Collections;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map;
011import java.util.Optional;
012import java.util.Set;
013import java.util.stream.Collectors;
014import java.util.stream.Stream;
015
016import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
017import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
018import org.openstreetmap.josm.spi.preferences.Config;
019import org.openstreetmap.josm.tools.CopyList;
020import org.openstreetmap.josm.tools.SubclassFilteredCollection;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * A relation, having a set of tags and any number (0...n) of members.
025 *
026 * @author Frederik Ramm
027 * @since 343
028 */
029public final class Relation extends OsmPrimitive implements IRelation<RelationMember> {
030
031    private RelationMember[] members = new RelationMember[0];
032
033    private BBox bbox;
034
035    @Override
036    public List<RelationMember> getMembers() {
037        return new CopyList<>(members);
038    }
039
040    @Override
041    public void setMembers(List<RelationMember> members) {
042        checkDatasetNotReadOnly();
043        boolean locked = writeLock();
044        try {
045            for (RelationMember rm : this.members) {
046                rm.getMember().removeReferrer(this);
047                rm.getMember().clearCachedStyle();
048            }
049
050            if (members != null) {
051                this.members = members.toArray(new RelationMember[0]);
052            } else {
053                this.members = new RelationMember[0];
054            }
055            for (RelationMember rm : this.members) {
056                rm.getMember().addReferrer(this);
057                rm.getMember().clearCachedStyle();
058            }
059
060            fireMembersChanged();
061        } finally {
062            writeUnlock(locked);
063        }
064    }
065
066    @Override
067    public int getMembersCount() {
068        return members.length;
069    }
070
071    @Override
072    public RelationMember getMember(int index) {
073        return members[index];
074    }
075
076    /**
077     * Adds the specified relation member at the last position.
078     * @param member the member to add
079     */
080    public void addMember(RelationMember member) {
081        checkDatasetNotReadOnly();
082        boolean locked = writeLock();
083        try {
084            members = Utils.addInArrayCopy(members, member);
085            member.getMember().addReferrer(this);
086            member.getMember().clearCachedStyle();
087            fireMembersChanged();
088        } finally {
089            writeUnlock(locked);
090        }
091    }
092
093    /**
094     * Adds the specified relation member at the specified index.
095     * @param member the member to add
096     * @param index the index at which the specified element is to be inserted
097     */
098    public void addMember(int index, RelationMember member) {
099        checkDatasetNotReadOnly();
100        boolean locked = writeLock();
101        try {
102            RelationMember[] newMembers = new RelationMember[members.length + 1];
103            System.arraycopy(members, 0, newMembers, 0, index);
104            System.arraycopy(members, index, newMembers, index + 1, members.length - index);
105            newMembers[index] = member;
106            members = newMembers;
107            member.getMember().addReferrer(this);
108            member.getMember().clearCachedStyle();
109            fireMembersChanged();
110        } finally {
111            writeUnlock(locked);
112        }
113    }
114
115    /**
116     * Replace member at position specified by index.
117     * @param index index (positive integer)
118     * @param member relation member to set
119     * @return Member that was at the position
120     */
121    public RelationMember setMember(int index, RelationMember member) {
122        checkDatasetNotReadOnly();
123        boolean locked = writeLock();
124        try {
125            RelationMember originalMember = members[index];
126            members[index] = member;
127            if (originalMember.getMember() != member.getMember()) {
128                member.getMember().addReferrer(this);
129                member.getMember().clearCachedStyle();
130                originalMember.getMember().removeReferrer(this);
131                originalMember.getMember().clearCachedStyle();
132                fireMembersChanged();
133            }
134            return originalMember;
135        } finally {
136            writeUnlock(locked);
137        }
138    }
139
140    /**
141     * Removes member at specified position.
142     * @param index index (positive integer)
143     * @return Member that was at the position
144     */
145    public RelationMember removeMember(int index) {
146        checkDatasetNotReadOnly();
147        boolean locked = writeLock();
148        try {
149            List<RelationMember> members = getMembers();
150            RelationMember result = members.remove(index);
151            setMembers(members);
152            return result;
153        } finally {
154            writeUnlock(locked);
155        }
156    }
157
158    @Override
159    public long getMemberId(int idx) {
160        return members[idx].getUniqueId();
161    }
162
163    @Override
164    public String getRole(int idx) {
165        return members[idx].getRole();
166    }
167
168    @Override
169    public OsmPrimitiveType getMemberType(int idx) {
170        return members[idx].getType();
171    }
172
173    @Override
174    public void accept(OsmPrimitiveVisitor visitor) {
175        visitor.visit(this);
176    }
177
178    @Override
179    public void accept(PrimitiveVisitor visitor) {
180        visitor.visit(this);
181    }
182
183    protected Relation(long id, boolean allowNegative) {
184        super(id, allowNegative);
185    }
186
187    /**
188     * Create a new relation with id 0
189     */
190    public Relation() {
191        super(0, false);
192    }
193
194    /**
195     * Constructs an identical clone of the argument.
196     * @param clone The relation to clone
197     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
198     * If {@code false}, does nothing
199     */
200    public Relation(Relation clone, boolean clearMetadata) {
201        super(clone.getUniqueId(), true);
202        cloneFrom(clone);
203        if (clearMetadata) {
204            clearOsmMetadata();
205        }
206    }
207
208    /**
209     * Create an identical clone of the argument (including the id)
210     * @param clone The relation to clone, including its id
211     */
212    public Relation(Relation clone) {
213        this(clone, false);
214    }
215
216    /**
217     * Creates a new relation for the given id. If the id &gt; 0, the way is marked
218     * as incomplete.
219     *
220     * @param id the id. &gt; 0 required
221     * @throws IllegalArgumentException if id &lt; 0
222     */
223    public Relation(long id) {
224        super(id, false);
225    }
226
227    /**
228     * Creates new relation
229     * @param id the id
230     * @param version version number (positive integer)
231     */
232    public Relation(long id, int version) {
233        super(id, version, false);
234    }
235
236    @Override
237    public void cloneFrom(OsmPrimitive osm) {
238        if (!(osm instanceof Relation))
239            throw new IllegalArgumentException("Not a relation: " + osm);
240        boolean locked = writeLock();
241        try {
242            super.cloneFrom(osm);
243            // It's not necessary to clone members as RelationMember class is immutable
244            setMembers(((Relation) osm).getMembers());
245        } finally {
246            writeUnlock(locked);
247        }
248    }
249
250    @Override
251    public void load(PrimitiveData data) {
252        if (!(data instanceof RelationData))
253            throw new IllegalArgumentException("Not a relation data: " + data);
254        boolean locked = writeLock();
255        try {
256            super.load(data);
257
258            RelationData relationData = (RelationData) data;
259
260            List<RelationMember> newMembers = new ArrayList<>();
261            for (RelationMemberData member : relationData.getMembers()) {
262                newMembers.add(new RelationMember(member.getRole(), Optional.ofNullable(getDataSet().getPrimitiveById(member))
263                        .orElseThrow(() -> new AssertionError("Data consistency problem - relation with missing member detected"))));
264            }
265            setMembers(newMembers);
266        } finally {
267            writeUnlock(locked);
268        }
269    }
270
271    @Override
272    public RelationData save() {
273        RelationData data = new RelationData();
274        saveCommonAttributes(data);
275        for (RelationMember member:getMembers()) {
276            data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember()));
277        }
278        return data;
279    }
280
281    @Override
282    public String toString() {
283        StringBuilder result = new StringBuilder(32);
284        result.append("{Relation id=")
285              .append(getUniqueId())
286              .append(" version=")
287              .append(getVersion())
288              .append(' ')
289              .append(getFlagsAsString())
290              .append(" [");
291        for (RelationMember rm:getMembers()) {
292            result.append(OsmPrimitiveType.from(rm.getMember()))
293                  .append(' ')
294                  .append(rm.getMember().getUniqueId())
295                  .append(", ");
296        }
297        result.delete(result.length()-2, result.length())
298              .append("]}");
299        return result.toString();
300    }
301
302    @Override
303    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
304        return (other instanceof Relation)
305                && hasEqualSemanticFlags(other)
306                && Arrays.equals(members, ((Relation) other).members)
307                && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
308    }
309
310    /**
311     * Returns the first member.
312     * @return first member, or {@code null}
313     */
314    public RelationMember firstMember() {
315        return (isIncomplete() || members.length == 0) ? null : members[0];
316    }
317
318    /**
319     * Returns the last member.
320     * @return last member, or {@code null}
321     */
322    public RelationMember lastMember() {
323        return (isIncomplete() || members.length == 0) ? null : members[members.length - 1];
324    }
325
326    /**
327     * removes all members with member.member == primitive
328     *
329     * @param primitive the primitive to check for
330     */
331    public void removeMembersFor(OsmPrimitive primitive) {
332        removeMembersFor(Collections.singleton(primitive));
333    }
334
335    @Override
336    public void setDeleted(boolean deleted) {
337        boolean locked = writeLock();
338        try {
339            for (RelationMember rm:members) {
340                if (deleted) {
341                    rm.getMember().removeReferrer(this);
342                } else {
343                    rm.getMember().addReferrer(this);
344                }
345            }
346            super.setDeleted(deleted);
347        } finally {
348            writeUnlock(locked);
349        }
350    }
351
352    /**
353     * Obtains all members with member.member == primitive
354     * @param primitives the primitives to check for
355     * @return all relation members for the given primitives
356     */
357    public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) {
358        return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember()));
359    }
360
361    /**
362     * removes all members with member.member == primitive
363     *
364     * @param primitives the primitives to check for
365     * @since 5613
366     */
367    public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) {
368        checkDatasetNotReadOnly();
369        if (primitives == null || primitives.isEmpty())
370            return;
371
372        boolean locked = writeLock();
373        try {
374            List<RelationMember> members = getMembers();
375            members.removeAll(getMembersFor(primitives));
376            setMembers(members);
377        } finally {
378            writeUnlock(locked);
379        }
380    }
381
382    /**
383     * Replies the set of  {@link OsmPrimitive}s referred to by at least one
384     * member of this relation
385     *
386     * @return the set of  {@link OsmPrimitive}s referred to by at least one
387     * member of this relation
388     * @see #getMemberPrimitivesList()
389     */
390    public Set<OsmPrimitive> getMemberPrimitives() {
391        return getMembers().stream().map(RelationMember::getMember).collect(Collectors.toSet());
392    }
393
394    /**
395     * Returns the {@link OsmPrimitive}s of the specified type referred to by at least one member of this relation.
396     * @param tClass the type of the primitive
397     * @param <T> the type of the primitive
398     * @return the primitives
399     */
400    public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) {
401        return Utils.filteredCollection(getMemberPrimitivesList(), tClass);
402    }
403
404    /**
405     * Returns an unmodifiable list of the {@link OsmPrimitive}s referred to by at least one member of this relation.
406     * @return an unmodifiable list of the primitives
407     */
408    @Override
409    public List<OsmPrimitive> getMemberPrimitivesList() {
410        return Utils.transform(getMembers(), RelationMember::getMember);
411    }
412
413    @Override
414    public OsmPrimitiveType getType() {
415        return OsmPrimitiveType.RELATION;
416    }
417
418    @Override
419    public OsmPrimitiveType getDisplayType() {
420        return isMultipolygon() && !isBoundary() ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
421    }
422
423    @Override
424    public BBox getBBox() {
425        if (getDataSet() != null && bbox != null)
426            return new BBox(bbox); // use cached value
427
428        BBox box = new BBox();
429        addToBBox(box, new HashSet<PrimitiveId>());
430        if (getDataSet() != null)
431            setBBox(box); // set cache
432        return new BBox(box);
433    }
434
435    private void setBBox(BBox bbox) {
436        this.bbox = bbox;
437    }
438
439    @Override
440    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
441        for (RelationMember rm : members) {
442            if (visited.add(rm.getMember()))
443                rm.getMember().addToBBox(box, visited);
444        }
445    }
446
447    @Override
448    public void updatePosition() {
449        setBBox(null); // make sure that it is recalculated
450        setBBox(getBBox());
451    }
452
453    @Override
454    void setDataset(DataSet dataSet) {
455        super.setDataset(dataSet);
456        checkMembers();
457        setBBox(null); // bbox might have changed if relation was in ds, was removed, modified, added back to dataset
458    }
459
460    /**
461     * Checks that members are part of the same dataset, and that they're not deleted.
462     * @throws DataIntegrityProblemException if one the above conditions is not met
463     */
464    private void checkMembers() {
465        DataSet dataSet = getDataSet();
466        if (dataSet != null) {
467            for (RelationMember rm: members) {
468                if (rm.getMember().getDataSet() != dataSet)
469                    throw new DataIntegrityProblemException(
470                            String.format("Relation member must be part of the same dataset as relation(%s, %s)",
471                                    getPrimitiveId(), rm.getMember().getPrimitiveId()));
472            }
473            if (Config.getPref().getBoolean("debug.checkDeleteReferenced", true)) {
474                for (RelationMember rm: members) {
475                    if (rm.getMember().isDeleted())
476                        throw new DataIntegrityProblemException("Deleted member referenced: " + toString());
477                }
478            }
479        }
480    }
481
482    /**
483     * Fires the {@code RelationMembersChangedEvent} to listeners.
484     * @throws DataIntegrityProblemException if members are not valid
485     * @see #checkMembers
486     */
487    private void fireMembersChanged() {
488        checkMembers();
489        if (getDataSet() != null) {
490            getDataSet().fireRelationMembersChanged(this);
491        }
492    }
493
494    @Override
495    public boolean hasIncompleteMembers() {
496        for (RelationMember rm: members) {
497            if (rm.getMember().isIncomplete()) return true;
498        }
499        return false;
500    }
501
502    /**
503     * Replies a collection with the incomplete children this relation refers to.
504     *
505     * @return the incomplete children. Empty collection if no children are incomplete.
506     */
507    @Override
508    public Collection<OsmPrimitive> getIncompleteMembers() {
509        Set<OsmPrimitive> ret = new HashSet<>();
510        for (RelationMember rm: members) {
511            if (!rm.getMember().isIncomplete()) {
512                continue;
513            }
514            ret.add(rm.getMember());
515        }
516        return ret;
517    }
518
519    @Override
520    protected void keysChangedImpl(Map<String, String> originalKeys) {
521        super.keysChangedImpl(originalKeys);
522        for (OsmPrimitive member : getMemberPrimitivesList()) {
523            member.clearCachedStyle();
524        }
525    }
526
527    @Override
528    public boolean concernsArea() {
529        return isMultipolygon() && hasAreaTags();
530    }
531
532    @Override
533    public boolean isOutsideDownloadArea() {
534        return false;
535    }
536
537    /**
538     * Returns the set of roles used in this relation.
539     * @return the set of roles used in this relation. Can be empty but never null
540     * @since 7556
541     */
542    public Set<String> getMemberRoles() {
543        return Stream.of(members).map(RelationMember::getRole).filter(role -> !role.isEmpty()).collect(Collectors.toSet());
544    }
545}