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