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.HashSet;
008import java.util.List;
009import java.util.Map;
010import java.util.Set;
011
012import org.openstreetmap.josm.Main;
013import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
014import org.openstreetmap.josm.data.osm.visitor.Visitor;
015import org.openstreetmap.josm.tools.CopyList;
016import org.openstreetmap.josm.tools.Utils;
017
018/**
019 * An relation, having a set of tags and any number (0...n) of members.
020 *
021 * @author Frederik Ramm <frederik@remote.org>
022 */
023public final class Relation extends OsmPrimitive implements IRelation {
024
025    private RelationMember[] members = new RelationMember[0];
026
027    private BBox bbox;
028
029    /**
030     * @return Members of the relation. Changes made in returned list are not mapped
031     * back to the primitive, use setMembers() to modify the members
032     * @since 1925
033     */
034    public List<RelationMember> getMembers() {
035        return new CopyList<RelationMember>(members);
036    }
037
038    /**
039     *
040     * @param members Can be null, in that case all members are removed
041     * @since 1925
042     */
043    public void setMembers(List<RelationMember> members) {
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[members.size()]);
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    /**
068     * @return number of members
069     */
070    @Override
071    public int getMembersCount() {
072        return members.length;
073    }
074
075    public RelationMember getMember(int index) {
076        return members[index];
077    }
078
079    public void addMember(RelationMember member) {
080        boolean locked = writeLock();
081        try {
082            RelationMember[] newMembers = new RelationMember[members.length + 1];
083            System.arraycopy(members, 0, newMembers, 0, members.length);
084            newMembers[members.length] = member;
085            members = newMembers;
086            member.getMember().addReferrer(this);
087            member.getMember().clearCachedStyle();
088            fireMembersChanged();
089        } finally {
090            writeUnlock(locked);
091        }
092    }
093
094    public void addMember(int index, RelationMember member) {
095        boolean locked = writeLock();
096        try {
097            RelationMember[] newMembers = new RelationMember[members.length + 1];
098            System.arraycopy(members, 0, newMembers, 0, index);
099            System.arraycopy(members, index, newMembers, index + 1, members.length - index);
100            newMembers[index] = member;
101            members = newMembers;
102            member.getMember().addReferrer(this);
103            member.getMember().clearCachedStyle();
104            fireMembersChanged();
105        } finally {
106            writeUnlock(locked);
107        }
108    }
109
110    /**
111     * Replace member at position specified by index.
112     * @param index
113     * @param member
114     * @return Member that was at the position
115     */
116    public RelationMember setMember(int index, RelationMember member) {
117        boolean locked = writeLock();
118        try {
119            RelationMember originalMember = members[index];
120            members[index] = member;
121            if (originalMember.getMember() != member.getMember()) {
122                member.getMember().addReferrer(this);
123                member.getMember().clearCachedStyle();
124                originalMember.getMember().removeReferrer(this);
125                originalMember.getMember().clearCachedStyle();
126                fireMembersChanged();
127            }
128            return originalMember;
129        } finally {
130            writeUnlock(locked);
131        }
132    }
133
134    /**
135     * Removes member at specified position.
136     * @param index
137     * @return Member that was at the position
138     */
139    public RelationMember removeMember(int index) {
140        boolean locked = writeLock();
141        try {
142            List<RelationMember> members = getMembers();
143            RelationMember result = members.remove(index);
144            setMembers(members);
145            return result;
146        } finally {
147            writeUnlock(locked);
148        }
149    }
150
151    @Override
152    public long getMemberId(int idx) {
153        return members[idx].getUniqueId();
154    }
155
156    @Override
157    public String getRole(int idx) {
158        return members[idx].getRole();
159    }
160
161    @Override
162    public OsmPrimitiveType getMemberType(int idx) {
163        return members[idx].getType();
164    }
165
166    @Override public void accept(Visitor visitor) {
167        visitor.visit(this);
168    }
169
170    @Override public void accept(PrimitiveVisitor visitor) {
171        visitor.visit(this);
172    }
173
174    protected Relation(long id, boolean allowNegative) {
175        super(id, allowNegative);
176    }
177
178    /**
179     * Create a new relation with id 0
180     */
181    public Relation() {
182        super(0, false);
183    }
184
185    /**
186     * Constructs an identical clone of the argument.
187     * @param clone The relation to clone
188     * @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}. If {@code false}, does nothing
189     */
190    public Relation(Relation clone, boolean clearMetadata) {
191        super(clone.getUniqueId(), true);
192        cloneFrom(clone);
193        if (clearMetadata) {
194            clearOsmMetadata();
195        }
196    }
197
198    /**
199     * Create an identical clone of the argument (including the id)
200     * @param clone The relation to clone, including its id
201     */
202    public Relation(Relation clone) {
203        this(clone, false);
204    }
205
206    /**
207     * Creates a new relation for the given id. If the id > 0, the way is marked
208     * as incomplete.
209     *
210     * @param id the id. > 0 required
211     * @throws IllegalArgumentException thrown if id < 0
212     */
213    public Relation(long id) throws IllegalArgumentException {
214        super(id, false);
215    }
216
217    /**
218     * Creates new relation
219     * @param id
220     * @param version
221     */
222    public Relation(long id, int version) {
223        super(id, version, false);
224    }
225
226    @Override 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 public void load(PrimitiveData data) {
238        boolean locked = writeLock();
239        try {
240            super.load(data);
241
242            RelationData relationData = (RelationData) data;
243
244            List<RelationMember> newMembers = new ArrayList<RelationMember>();
245            for (RelationMemberData member : relationData.getMembers()) {
246                OsmPrimitive primitive = getDataSet().getPrimitiveById(member);
247                if (primitive == null)
248                    throw new AssertionError("Data consistency problem - relation with missing member detected");
249                newMembers.add(new RelationMember(member.getRole(), primitive));
250            }
251            setMembers(newMembers);
252        } finally {
253            writeUnlock(locked);
254        }
255    }
256
257    @Override public RelationData save() {
258        RelationData data = new RelationData();
259        saveCommonAttributes(data);
260        for (RelationMember member:getMembers()) {
261            data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember()));
262        }
263        return data;
264    }
265
266    @Override public String toString() {
267        StringBuilder result = new StringBuilder();
268        result.append("{Relation id=");
269        result.append(getUniqueId());
270        result.append(" version=");
271        result.append(getVersion());
272        result.append(" ");
273        result.append(getFlagsAsString());
274        result.append(" [");
275        for (RelationMember rm:getMembers()) {
276            result.append(OsmPrimitiveType.from(rm.getMember()));
277            result.append(" ");
278            result.append(rm.getMember().getUniqueId());
279            result.append(", ");
280        }
281        result.delete(result.length()-2, result.length());
282        result.append("]");
283        result.append("}");
284        return result.toString();
285    }
286
287    @Override
288    public boolean hasEqualSemanticAttributes(OsmPrimitive other) {
289        if (!(other instanceof Relation))
290            return false;
291        if (! super.hasEqualSemanticAttributes(other))
292            return false;
293        Relation r = (Relation)other;
294        return Arrays.equals(members, r.members);
295    }
296
297    @Override
298    public int compareTo(OsmPrimitive o) {
299        return o instanceof Relation ? Long.valueOf(getUniqueId()).compareTo(o.getUniqueId()) : -1;
300    }
301
302    public RelationMember firstMember() {
303        if (isIncomplete()) return null;
304
305        RelationMember[] members = this.members;
306        return (members.length == 0) ? null : members[0];
307    }
308    public RelationMember lastMember() {
309        if (isIncomplete()) return null;
310
311        RelationMember[] members = this.members;
312        return (members.length == 0) ? null : members[members.length - 1];
313    }
314
315    /**
316     * removes all members with member.member == primitive
317     *
318     * @param primitive the primitive to check for
319     */
320    public void removeMembersFor(OsmPrimitive primitive) {
321        if (primitive == null)
322            return;
323
324        boolean locked = writeLock();
325        try {
326            List<RelationMember> todelete = new ArrayList<RelationMember>();
327            for (RelationMember member: members) {
328                if (member.getMember() == primitive) {
329                    todelete.add(member);
330                }
331            }
332            List<RelationMember> members = getMembers();
333            members.removeAll(todelete);
334            setMembers(members);
335        } finally {
336            writeUnlock(locked);
337        }
338    }
339
340    @Override
341    public void setDeleted(boolean deleted) {
342        boolean locked = writeLock();
343        try {
344            for (RelationMember rm:members) {
345                if (deleted) {
346                    rm.getMember().removeReferrer(this);
347                } else {
348                    rm.getMember().addReferrer(this);
349                }
350            }
351            super.setDeleted(deleted);
352        } finally {
353            writeUnlock(locked);
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> todelete = new ArrayList<RelationMember>();
370            for (RelationMember member: members) {
371                if (primitives.contains(member.getMember())) {
372                    todelete.add(member);
373                }
374            }
375            List<RelationMember> members = getMembers();
376            members.removeAll(todelete);
377            setMembers(members);
378        } finally {
379            writeUnlock(locked);
380        }
381    }
382
383    @Override
384    public String getDisplayName(NameFormatter formatter) {
385        return formatter.format(this);
386    }
387
388    /**
389     * Replies the set of  {@link OsmPrimitive}s referred to by at least one
390     * member of this relation
391     *
392     * @return the set of  {@link OsmPrimitive}s referred to by at least one
393     * member of this relation
394     */
395    public Set<OsmPrimitive> getMemberPrimitives() {
396        HashSet<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
397        RelationMember[] members = this.members;
398        for (RelationMember m: members) {
399            if (m.getMember() != null) {
400                ret.add(m.getMember());
401            }
402        }
403        return ret;
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() ? OsmPrimitiveType.MULTIPOLYGON
423        : OsmPrimitiveType.RELATION;
424    }
425
426    public boolean isMultipolygon() {
427        return "multipolygon".equals(get("type")) || "boundary".equals(get("type"));
428    }
429
430    @Override
431    public BBox getBBox() {
432        RelationMember[] members = this.members;
433
434        if (members.length == 0)
435            return new BBox(0, 0, 0, 0);
436        if (getDataSet() == null)
437            return calculateBBox(new HashSet<PrimitiveId>());
438        else {
439            if (bbox == null) {
440                bbox = calculateBBox(new HashSet<PrimitiveId>());
441            }
442            if (bbox == null)
443                return new BBox(0, 0, 0, 0); // No real members
444            else
445                return new BBox(bbox);
446        }
447    }
448
449    private BBox calculateBBox(Set<PrimitiveId> visitedRelations) {
450        if (visitedRelations.contains(this))
451            return null;
452        visitedRelations.add(this);
453
454        RelationMember[] members = this.members;
455        if (members.length == 0)
456            return null;
457        else {
458            BBox result = null;
459            for (RelationMember rm:members) {
460                BBox box = rm.isRelation()?rm.getRelation().calculateBBox(visitedRelations):rm.getMember().getBBox();
461                if (box != null) {
462                    if (result == null) {
463                        result = box;
464                    } else {
465                        result.add(box);
466                    }
467                }
468            }
469            return result;
470        }
471    }
472
473    @Override
474    public void updatePosition() {
475        bbox = calculateBBox(new HashSet<PrimitiveId>());
476    }
477
478    @Override
479    public void setDataset(DataSet dataSet) {
480        super.setDataset(dataSet);
481        checkMembers();
482        bbox = null; // bbox might have changed if relation was in ds, was removed, modified, added back to dataset
483    }
484
485    private void checkMembers() {
486        DataSet dataSet = getDataSet();
487        if (dataSet != null) {
488            RelationMember[] members = this.members;
489            for (RelationMember rm: members) {
490                if (rm.getMember().getDataSet() != dataSet)
491                    throw new DataIntegrityProblemException(String.format("Relation member must be part of the same dataset as relation(%s, %s)", getPrimitiveId(), rm.getMember().getPrimitiveId()));
492            }
493            if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) {
494                for (RelationMember rm: members) {
495                    if (rm.getMember().isDeleted())
496                        throw new DataIntegrityProblemException("Deleted member referenced: " + toString());
497                }
498            }
499        }
500    }
501
502    private void fireMembersChanged() {
503        checkMembers();
504        if (getDataSet() != null) {
505            getDataSet().fireRelationMembersChanged(this);
506        }
507    }
508
509    /**
510     * Replies true if at least one child primitive is incomplete
511     *
512     * @return true if at least one child primitive is incomplete
513     */
514    public boolean hasIncompleteMembers() {
515        RelationMember[] members = this.members;
516        for (RelationMember rm: members) {
517            if (rm.getMember().isIncomplete()) return true;
518        }
519        return false;
520    }
521
522    /**
523     * Replies a collection with the incomplete children this relation
524     * refers to
525     *
526     * @return the incomplete children. Empty collection if no children are incomplete.
527     */
528    public Collection<OsmPrimitive> getIncompleteMembers() {
529        Set<OsmPrimitive> ret = new HashSet<OsmPrimitive>();
530        RelationMember[] members = this.members;
531        for (RelationMember rm: members) {
532            if (!rm.getMember().isIncomplete()) {
533                continue;
534            }
535            ret.add(rm.getMember());
536        }
537        return ret;
538    }
539
540    @Override
541    protected void keysChangedImpl(Map<String, String> originalKeys) {
542        super.keysChangedImpl(originalKeys);
543        // fix #8346 - Clear style cache for multipolygon members after a tag change
544        if (isMultipolygon()) {
545            for (OsmPrimitive member : getMemberPrimitives()) {
546                member.clearCachedStyle();
547            }
548        }
549    }
550
551    @Override
552    public boolean concernsArea() {
553        return isMultipolygon() && hasAreaTags();
554    }
555}