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