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