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
112     * @param member
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
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}. If {@code false}, does nothing
190     */
191    public Relation(Relation clone, boolean clearMetadata) {
192        super(clone.getUniqueId(), true);
193        cloneFrom(clone);
194        if (clearMetadata) {
195            clearOsmMetadata();
196        }
197    }
198
199    /**
200     * Create an identical clone of the argument (including the id)
201     * @param clone The relation to clone, including its id
202     */
203    public Relation(Relation clone) {
204        this(clone, false);
205    }
206
207    /**
208     * Creates a new relation for the given id. If the id &gt; 0, the way is marked
209     * as incomplete.
210     *
211     * @param id the id. &gt; 0 required
212     * @throws IllegalArgumentException thrown if id &lt; 0
213     */
214    public Relation(long id) throws IllegalArgumentException {
215        super(id, false);
216    }
217
218    /**
219     * Creates new relation
220     * @param id
221     * @param version
222     */
223    public Relation(long id, int version) {
224        super(id, version, false);
225    }
226
227    @Override
228    public void cloneFrom(OsmPrimitive osm) {
229        boolean locked = writeLock();
230        try {
231            super.cloneFrom(osm);
232            // It's not necessary to clone members as RelationMember class is immutable
233            setMembers(((Relation)osm).getMembers());
234        } finally {
235            writeUnlock(locked);
236        }
237    }
238
239    @Override
240    public void load(PrimitiveData data) {
241        boolean locked = writeLock();
242        try {
243            super.load(data);
244
245            RelationData relationData = (RelationData) data;
246
247            List<RelationMember> newMembers = new ArrayList<>();
248            for (RelationMemberData member : relationData.getMembers()) {
249                OsmPrimitive primitive = getDataSet().getPrimitiveById(member);
250                if (primitive == null)
251                    throw new AssertionError("Data consistency problem - relation with missing member detected");
252                newMembers.add(new RelationMember(member.getRole(), primitive));
253            }
254            setMembers(newMembers);
255        } finally {
256            writeUnlock(locked);
257        }
258    }
259
260    @Override public RelationData save() {
261        RelationData data = new RelationData();
262        saveCommonAttributes(data);
263        for (RelationMember member:getMembers()) {
264            data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember()));
265        }
266        return data;
267    }
268
269    @Override public String toString() {
270        StringBuilder result = new StringBuilder();
271        result.append("{Relation id=");
272        result.append(getUniqueId());
273        result.append(" version=");
274        result.append(getVersion());
275        result.append(" ");
276        result.append(getFlagsAsString());
277        result.append(" [");
278        for (RelationMember rm:getMembers()) {
279            result.append(OsmPrimitiveType.from(rm.getMember()));
280            result.append(" ");
281            result.append(rm.getMember().getUniqueId());
282            result.append(", ");
283        }
284        result.delete(result.length()-2, result.length());
285        result.append("]");
286        result.append("}");
287        return result.toString();
288    }
289
290    @Override
291    public boolean hasEqualSemanticAttributes(OsmPrimitive other) {
292        if (!(other instanceof Relation))
293            return false;
294        if (! super.hasEqualSemanticAttributes(other))
295            return false;
296        Relation r = (Relation)other;
297        return Arrays.equals(members, r.members);
298    }
299
300    @Override
301    public int compareTo(OsmPrimitive o) {
302        return o instanceof Relation ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : -1;
303    }
304
305    public RelationMember firstMember() {
306        if (isIncomplete()) return null;
307
308        RelationMember[] members = this.members;
309        return (members.length == 0) ? null : members[0];
310    }
311    public RelationMember lastMember() {
312        if (isIncomplete()) return null;
313
314        RelationMember[] members = this.members;
315        return (members.length == 0) ? null : members[members.length - 1];
316    }
317
318    /**
319     * removes all members with member.member == primitive
320     *
321     * @param primitive the primitive to check for
322     */
323    public void removeMembersFor(OsmPrimitive primitive) {
324        removeMembersFor(Collections.singleton(primitive));
325    }
326
327    @Override
328    public void setDeleted(boolean deleted) {
329        boolean locked = writeLock();
330        try {
331            for (RelationMember rm:members) {
332                if (deleted) {
333                    rm.getMember().removeReferrer(this);
334                } else {
335                    rm.getMember().addReferrer(this);
336                }
337            }
338            super.setDeleted(deleted);
339        } finally {
340            writeUnlock(locked);
341        }
342    }
343
344    /**
345     * Obtains all members with member.member == primitive
346     * @param primitives the primitives to check for
347     */
348    public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) {
349        return Utils.filter(getMembers(), new Predicate<RelationMember>() {
350            @Override
351            public boolean evaluate(RelationMember member) {
352                return primitives.contains(member.getMember());
353            }
354        });
355    }
356
357    /**
358     * removes all members with member.member == primitive
359     *
360     * @param primitives the primitives to check for
361     * @since 5613
362     */
363    public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) {
364        if (primitives == null || primitives.isEmpty())
365            return;
366
367        boolean locked = writeLock();
368        try {
369            List<RelationMember> members = getMembers();
370            members.removeAll(getMembersFor(primitives));
371            setMembers(members);
372        } finally {
373            writeUnlock(locked);
374        }
375    }
376
377    @Override
378    public String getDisplayName(NameFormatter formatter) {
379        return formatter.format(this);
380    }
381
382    /**
383     * Replies the set of  {@link OsmPrimitive}s referred to by at least one
384     * member of this relation
385     *
386     * @return the set of  {@link OsmPrimitive}s referred to by at least one
387     * member of this relation
388     */
389    public Set<OsmPrimitive> getMemberPrimitives() {
390        HashSet<OsmPrimitive> ret = new HashSet<>();
391        RelationMember[] members = this.members;
392        for (RelationMember m: members) {
393            if (m.getMember() != null) {
394                ret.add(m.getMember());
395            }
396        }
397        return ret;
398    }
399
400    public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) {
401        return Utils.filteredCollection(getMemberPrimitives(), tClass);
402    }
403
404    public List<OsmPrimitive> getMemberPrimitivesList() {
405        return Utils.transform(getMembers(), new Utils.Function<RelationMember, OsmPrimitive>() {
406            @Override
407            public OsmPrimitive apply(RelationMember x) {
408                return x.getMember();
409            }
410        });
411    }
412
413    @Override
414    public OsmPrimitiveType getType() {
415        return OsmPrimitiveType.RELATION;
416    }
417
418    @Override
419    public OsmPrimitiveType getDisplayType() {
420        return isMultipolygon() ? OsmPrimitiveType.MULTIPOLYGON
421        : OsmPrimitiveType.RELATION;
422    }
423
424    public boolean isMultipolygon() {
425        return "multipolygon".equals(get("type")) || "boundary".equals(get("type"));
426    }
427
428    @Override
429    public BBox getBBox() {
430        RelationMember[] members = this.members;
431
432        if (members.length == 0)
433            return new BBox(0, 0, 0, 0);
434        if (getDataSet() == null)
435            return calculateBBox(new HashSet<PrimitiveId>());
436        else {
437            if (bbox == null) {
438                bbox = calculateBBox(new HashSet<PrimitiveId>());
439            }
440            if (bbox == null)
441                return new BBox(0, 0, 0, 0); // No real members
442            else
443                return new BBox(bbox);
444        }
445    }
446
447    private BBox calculateBBox(Set<PrimitiveId> visitedRelations) {
448        if (visitedRelations.contains(this))
449            return null;
450        visitedRelations.add(this);
451
452        RelationMember[] members = this.members;
453        if (members.length == 0)
454            return null;
455        else {
456            BBox result = null;
457            for (RelationMember rm:members) {
458                BBox box = rm.isRelation()?rm.getRelation().calculateBBox(visitedRelations):rm.getMember().getBBox();
459                if (box != null) {
460                    if (result == null) {
461                        result = box;
462                    } else {
463                        result.add(box);
464                    }
465                }
466            }
467            return result;
468        }
469    }
470
471    @Override
472    public void updatePosition() {
473        bbox = calculateBBox(new HashSet<PrimitiveId>());
474    }
475
476    @Override
477    public void setDataset(DataSet dataSet) {
478        super.setDataset(dataSet);
479        checkMembers();
480        bbox = null; // bbox might have changed if relation was in ds, was removed, modified, added back to dataset
481    }
482
483    private void checkMembers() throws DataIntegrityProblemException {
484        DataSet dataSet = getDataSet();
485        if (dataSet != null) {
486            RelationMember[] members = this.members;
487            for (RelationMember rm: members) {
488                if (rm.getMember().getDataSet() != dataSet)
489                    throw new DataIntegrityProblemException(String.format("Relation member must be part of the same dataset as relation(%s, %s)", getPrimitiveId(), rm.getMember().getPrimitiveId()));
490            }
491            if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) {
492                for (RelationMember rm: members) {
493                    if (rm.getMember().isDeleted())
494                        throw new DataIntegrityProblemException("Deleted member referenced: " + toString());
495                }
496            }
497        }
498    }
499
500    private void fireMembersChanged() throws DataIntegrityProblemException {
501        checkMembers();
502        if (getDataSet() != null) {
503            getDataSet().fireRelationMembersChanged(this);
504        }
505    }
506
507    /**
508     * Determines if at least one child primitive is incomplete.
509     *
510     * @return true if at least one child primitive is incomplete
511     */
512    public boolean hasIncompleteMembers() {
513        RelationMember[] members = this.members;
514        for (RelationMember rm: members) {
515            if (rm.getMember().isIncomplete()) return true;
516        }
517        return false;
518    }
519
520    /**
521     * Replies a collection with the incomplete children this relation refers to.
522     *
523     * @return the incomplete children. Empty collection if no children are incomplete.
524     */
525    public Collection<OsmPrimitive> getIncompleteMembers() {
526        Set<OsmPrimitive> ret = new HashSet<>();
527        RelationMember[] members = this.members;
528        for (RelationMember rm: members) {
529            if (!rm.getMember().isIncomplete()) {
530                continue;
531            }
532            ret.add(rm.getMember());
533        }
534        return ret;
535    }
536
537    @Override
538    protected void keysChangedImpl(Map<String, String> originalKeys) {
539        super.keysChangedImpl(originalKeys);
540        for (OsmPrimitive member : getMemberPrimitives()) {
541            member.clearCachedStyle();
542        }
543    }
544
545    @Override
546    public boolean concernsArea() {
547        return isMultipolygon() && hasAreaTags();
548    }
549
550    @Override
551    public boolean isOutsideDownloadArea() {
552        return false;
553    }
554
555    /**
556     * Returns the set of roles used in this relation.
557     * @return the set of roles used in this relation. Can be empty but never null
558     * @since 7556
559     */
560    public Set<String> getMemberRoles() {
561        Set<String> result = new HashSet<>();
562        for (RelationMember rm : members) {
563            String role = rm.getRole();
564            if (!role.isEmpty()) {
565                result.add(role);
566            }
567        }
568        return result;
569    }
570}