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