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