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