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