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     *
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    @Override
321    public int compareTo(OsmPrimitive o) {
322        return o instanceof Relation ? Long.compare(getUniqueId(), o.getUniqueId()) : -1;
323    }
324
325    /**
326     * Returns the first member.
327     * @return first member, or {@code null}
328     */
329    public RelationMember firstMember() {
330        return (isIncomplete() || members.length == 0) ? null : members[0];
331    }
332
333    /**
334     * Returns the last member.
335     * @return last member, or {@code null}
336     */
337    public RelationMember lastMember() {
338        return (isIncomplete() || members.length == 0) ? null : members[members.length - 1];
339    }
340
341    /**
342     * removes all members with member.member == primitive
343     *
344     * @param primitive the primitive to check for
345     */
346    public void removeMembersFor(OsmPrimitive primitive) {
347        removeMembersFor(Collections.singleton(primitive));
348    }
349
350    @Override
351    public void setDeleted(boolean deleted) {
352        boolean locked = writeLock();
353        try {
354            for (RelationMember rm:members) {
355                if (deleted) {
356                    rm.getMember().removeReferrer(this);
357                } else {
358                    rm.getMember().addReferrer(this);
359                }
360            }
361            super.setDeleted(deleted);
362        } finally {
363            writeUnlock(locked);
364        }
365    }
366
367    /**
368     * Obtains all members with member.member == primitive
369     * @param primitives the primitives to check for
370     * @return all relation members for the given primitives
371     */
372    public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) {
373        return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember()));
374    }
375
376    /**
377     * removes all members with member.member == primitive
378     *
379     * @param primitives the primitives to check for
380     * @since 5613
381     */
382    public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) {
383        checkDatasetNotReadOnly();
384        if (primitives == null || primitives.isEmpty())
385            return;
386
387        boolean locked = writeLock();
388        try {
389            List<RelationMember> members = getMembers();
390            members.removeAll(getMembersFor(primitives));
391            setMembers(members);
392        } finally {
393            writeUnlock(locked);
394        }
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 (Config.getPref().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    @Override
523    public boolean hasIncompleteMembers() {
524        RelationMember[] members = this.members;
525        for (RelationMember rm: members) {
526            if (rm.getMember().isIncomplete()) return true;
527        }
528        return false;
529    }
530
531    /**
532     * Replies a collection with the incomplete children this relation refers to.
533     *
534     * @return the incomplete children. Empty collection if no children are incomplete.
535     */
536    public Collection<OsmPrimitive> getIncompleteMembers() {
537        Set<OsmPrimitive> ret = new HashSet<>();
538        RelationMember[] members = this.members;
539        for (RelationMember rm: members) {
540            if (!rm.getMember().isIncomplete()) {
541                continue;
542            }
543            ret.add(rm.getMember());
544        }
545        return ret;
546    }
547
548    @Override
549    protected void keysChangedImpl(Map<String, String> originalKeys) {
550        super.keysChangedImpl(originalKeys);
551        for (OsmPrimitive member : getMemberPrimitivesList()) {
552            member.clearCachedStyle();
553        }
554    }
555
556    @Override
557    public boolean concernsArea() {
558        return isMultipolygon() && hasAreaTags();
559    }
560
561    @Override
562    public boolean isOutsideDownloadArea() {
563        return false;
564    }
565
566    /**
567     * Returns the set of roles used in this relation.
568     * @return the set of roles used in this relation. Can be empty but never null
569     * @since 7556
570     */
571    public Set<String> getMemberRoles() {
572        Set<String> result = new HashSet<>();
573        for (RelationMember rm : members) {
574            String role = rm.getRole();
575            if (!role.isEmpty()) {
576                result.add(role);
577            }
578        }
579        return result;
580    }
581}