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        if (!(osm instanceof Relation))
243            throw new IllegalArgumentException("Not a relation: " + osm);
244        boolean locked = writeLock();
245        try {
246            super.cloneFrom(osm);
247            // It's not necessary to clone members as RelationMember class is immutable
248            setMembers(((Relation) osm).getMembers());
249        } finally {
250            writeUnlock(locked);
251        }
252    }
253
254    @Override
255    public void load(PrimitiveData data) {
256        if (!(data instanceof RelationData))
257            throw new IllegalArgumentException("Not a relation data: " + data);
258        boolean locked = writeLock();
259        try {
260            super.load(data);
261
262            RelationData relationData = (RelationData) data;
263
264            List<RelationMember> newMembers = new ArrayList<>();
265            for (RelationMemberData member : relationData.getMembers()) {
266                OsmPrimitive primitive = getDataSet().getPrimitiveById(member);
267                if (primitive == null)
268                    throw new AssertionError("Data consistency problem - relation with missing member detected");
269                newMembers.add(new RelationMember(member.getRole(), primitive));
270            }
271            setMembers(newMembers);
272        } finally {
273            writeUnlock(locked);
274        }
275    }
276
277    @Override
278    public RelationData save() {
279        RelationData data = new RelationData();
280        saveCommonAttributes(data);
281        for (RelationMember member:getMembers()) {
282            data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember()));
283        }
284        return data;
285    }
286
287    @Override
288    public String toString() {
289        StringBuilder result = new StringBuilder(32);
290        result.append("{Relation id=")
291              .append(getUniqueId())
292              .append(" version=")
293              .append(getVersion())
294              .append(' ')
295              .append(getFlagsAsString())
296              .append(" [");
297        for (RelationMember rm:getMembers()) {
298            result.append(OsmPrimitiveType.from(rm.getMember()))
299                  .append(' ')
300                  .append(rm.getMember().getUniqueId())
301                  .append(", ");
302        }
303        result.delete(result.length()-2, result.length())
304              .append("]}");
305        return result.toString();
306    }
307
308    @Override
309    public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
310        return (other instanceof Relation)
311                && hasEqualSemanticFlags(other)
312                && Arrays.equals(members, ((Relation) other).members)
313                && super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
314    }
315
316    @Override
317    public int compareTo(OsmPrimitive o) {
318        return o instanceof Relation ? Long.compare(getUniqueId(), o.getUniqueId()) : -1;
319    }
320
321    /**
322     * Returns the first member.
323     * @return first member, or {@code null}
324     */
325    public RelationMember firstMember() {
326        return (isIncomplete() || members.length == 0) ? null : members[0];
327    }
328
329    /**
330     * Returns the last member.
331     * @return last member, or {@code null}
332     */
333    public RelationMember lastMember() {
334        return (isIncomplete() || members.length == 0) ? null : members[members.length - 1];
335    }
336
337    /**
338     * removes all members with member.member == primitive
339     *
340     * @param primitive the primitive to check for
341     */
342    public void removeMembersFor(OsmPrimitive primitive) {
343        removeMembersFor(Collections.singleton(primitive));
344    }
345
346    @Override
347    public void setDeleted(boolean deleted) {
348        boolean locked = writeLock();
349        try {
350            for (RelationMember rm:members) {
351                if (deleted) {
352                    rm.getMember().removeReferrer(this);
353                } else {
354                    rm.getMember().addReferrer(this);
355                }
356            }
357            super.setDeleted(deleted);
358        } finally {
359            writeUnlock(locked);
360        }
361    }
362
363    /**
364     * Obtains all members with member.member == primitive
365     * @param primitives the primitives to check for
366     * @return all relation members for the given primitives
367     */
368    public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) {
369        return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember()));
370    }
371
372    /**
373     * removes all members with member.member == primitive
374     *
375     * @param primitives the primitives to check for
376     * @since 5613
377     */
378    public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) {
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    @Override
393    public String getDisplayName(NameFormatter formatter) {
394        return formatter.format(this);
395    }
396
397    /**
398     * Replies the set of  {@link OsmPrimitive}s referred to by at least one
399     * member of this relation
400     *
401     * @return the set of  {@link OsmPrimitive}s referred to by at least one
402     * member of this relation
403     * @see #getMemberPrimitivesList()
404     */
405    public Set<OsmPrimitive> getMemberPrimitives() {
406        return getMembers().stream().map(RelationMember::getMember).collect(Collectors.toSet());
407    }
408
409    /**
410     * Returns the {@link OsmPrimitive}s of the specified type referred to by at least one member of this relation.
411     * @param tClass the type of the primitive
412     * @param <T> the type of the primitive
413     * @return the primitives
414     */
415    public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) {
416        return Utils.filteredCollection(getMemberPrimitivesList(), tClass);
417    }
418
419    /**
420     * Returns an unmodifiable list of the {@link OsmPrimitive}s referred to by at least one member of this relation.
421     * @return an unmodifiable list of the primitives
422     */
423    public List<OsmPrimitive> getMemberPrimitivesList() {
424        return Utils.transform(getMembers(), RelationMember::getMember);
425    }
426
427    @Override
428    public OsmPrimitiveType getType() {
429        return OsmPrimitiveType.RELATION;
430    }
431
432    @Override
433    public OsmPrimitiveType getDisplayType() {
434        return isMultipolygon() && !isBoundary() ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
435    }
436
437    /**
438     * Determines if this relation is a boundary.
439     * @return {@code true} if a boundary relation
440     */
441    public boolean isBoundary() {
442        return "boundary".equals(get("type"));
443    }
444
445    @Override
446    public boolean isMultipolygon() {
447        return "multipolygon".equals(get("type")) || isBoundary();
448    }
449
450    @Override
451    public BBox getBBox() {
452        if (getDataSet() != null && bbox != null)
453            return new BBox(bbox); // use cached value
454
455        BBox box = new BBox();
456        addToBBox(box, new HashSet<PrimitiveId>());
457        if (getDataSet() != null)
458            setBBox(box); // set cache
459        return new BBox(box);
460    }
461
462    private void setBBox(BBox bbox) {
463        this.bbox = bbox;
464    }
465
466    @Override
467    protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
468        for (RelationMember rm : members) {
469            if (visited.add(rm.getMember()))
470                rm.getMember().addToBBox(box, visited);
471        }
472    }
473
474    @Override
475    public void updatePosition() {
476        setBBox(null); // make sure that it is recalculated
477        setBBox(getBBox());
478    }
479
480    @Override
481    void setDataset(DataSet dataSet) {
482        super.setDataset(dataSet);
483        checkMembers();
484        setBBox(null); // bbox might have changed if relation was in ds, was removed, modified, added back to dataset
485    }
486
487    /**
488     * Checks that members are part of the same dataset, and that they're not deleted.
489     * @throws DataIntegrityProblemException if one the above conditions is not met
490     */
491    private void checkMembers() {
492        DataSet dataSet = getDataSet();
493        if (dataSet != null) {
494            RelationMember[] members = this.members;
495            for (RelationMember rm: members) {
496                if (rm.getMember().getDataSet() != dataSet)
497                    throw new DataIntegrityProblemException(
498                            String.format("Relation member must be part of the same dataset as relation(%s, %s)",
499                                    getPrimitiveId(), rm.getMember().getPrimitiveId()));
500            }
501            if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) {
502                for (RelationMember rm: members) {
503                    if (rm.getMember().isDeleted())
504                        throw new DataIntegrityProblemException("Deleted member referenced: " + toString());
505                }
506            }
507        }
508    }
509
510    /**
511     * Fires the {@code RelationMembersChangedEvent} to listeners.
512     * @throws DataIntegrityProblemException if members are not valid
513     * @see #checkMembers
514     */
515    private void fireMembersChanged() {
516        checkMembers();
517        if (getDataSet() != null) {
518            getDataSet().fireRelationMembersChanged(this);
519        }
520    }
521
522    /**
523     * Determines if at least one child primitive is incomplete.
524     *
525     * @return true if at least one child primitive is incomplete
526     */
527    public boolean hasIncompleteMembers() {
528        RelationMember[] members = this.members;
529        for (RelationMember rm: members) {
530            if (rm.getMember().isIncomplete()) return true;
531        }
532        return false;
533    }
534
535    /**
536     * Replies a collection with the incomplete children this relation refers to.
537     *
538     * @return the incomplete children. Empty collection if no children are incomplete.
539     */
540    public Collection<OsmPrimitive> getIncompleteMembers() {
541        Set<OsmPrimitive> ret = new HashSet<>();
542        RelationMember[] members = this.members;
543        for (RelationMember rm: members) {
544            if (!rm.getMember().isIncomplete()) {
545                continue;
546            }
547            ret.add(rm.getMember());
548        }
549        return ret;
550    }
551
552    @Override
553    protected void keysChangedImpl(Map<String, String> originalKeys) {
554        super.keysChangedImpl(originalKeys);
555        for (OsmPrimitive member : getMemberPrimitivesList()) {
556            member.clearCachedStyle();
557        }
558    }
559
560    @Override
561    public boolean concernsArea() {
562        return isMultipolygon() && hasAreaTags();
563    }
564
565    @Override
566    public boolean isOutsideDownloadArea() {
567        return false;
568    }
569
570    /**
571     * Returns the set of roles used in this relation.
572     * @return the set of roles used in this relation. Can be empty but never null
573     * @since 7556
574     */
575    public Set<String> getMemberRoles() {
576        Set<String> result = new HashSet<>();
577        for (RelationMember rm : members) {
578            String role = rm.getRole();
579            if (!role.isEmpty()) {
580                result.add(role);
581            }
582        }
583        return result;
584    }
585}