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