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     */
352    public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) {
353        return Utils.filter(getMembers(), new Predicate<RelationMember>() {
354            @Override
355            public boolean evaluate(RelationMember member) {
356                return primitives.contains(member.getMember());
357            }
358        });
359    }
360
361    /**
362     * removes all members with member.member == primitive
363     *
364     * @param primitives the primitives to check for
365     * @since 5613
366     */
367    public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) {
368        if (primitives == null || primitives.isEmpty())
369            return;
370
371        boolean locked = writeLock();
372        try {
373            List<RelationMember> members = getMembers();
374            members.removeAll(getMembersFor(primitives));
375            setMembers(members);
376        } finally {
377            writeUnlock(locked);
378        }
379    }
380
381    @Override
382    public String getDisplayName(NameFormatter formatter) {
383        return formatter.format(this);
384    }
385
386    /**
387     * Replies the set of  {@link OsmPrimitive}s referred to by at least one
388     * member of this relation
389     *
390     * @return the set of  {@link OsmPrimitive}s referred to by at least one
391     * member of this relation
392     */
393    public Set<OsmPrimitive> getMemberPrimitives() {
394        Set<OsmPrimitive> ret = new HashSet<>();
395        RelationMember[] members = this.members;
396        for (RelationMember m: members) {
397            if (m.getMember() != null) {
398                ret.add(m.getMember());
399            }
400        }
401        return ret;
402    }
403
404    public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) {
405        return Utils.filteredCollection(getMemberPrimitives(), tClass);
406    }
407
408    public List<OsmPrimitive> getMemberPrimitivesList() {
409        return Utils.transform(getMembers(), new Utils.Function<RelationMember, OsmPrimitive>() {
410            @Override
411            public OsmPrimitive apply(RelationMember x) {
412                return x.getMember();
413            }
414        });
415    }
416
417    @Override
418    public OsmPrimitiveType getType() {
419        return OsmPrimitiveType.RELATION;
420    }
421
422    @Override
423    public OsmPrimitiveType getDisplayType() {
424        return isMultipolygon() ? OsmPrimitiveType.MULTIPOLYGON
425        : OsmPrimitiveType.RELATION;
426    }
427
428    public boolean isMultipolygon() {
429        return "multipolygon".equals(get("type")) || "boundary".equals(get("type"));
430    }
431
432    @Override
433    public BBox getBBox() {
434        RelationMember[] members = this.members;
435
436        if (members.length == 0)
437            return new BBox(0, 0, 0, 0);
438        if (getDataSet() == null)
439            return calculateBBox(new HashSet<PrimitiveId>());
440        else {
441            if (bbox == null) {
442                bbox = calculateBBox(new HashSet<PrimitiveId>());
443            }
444            if (bbox == null)
445                return new BBox(0, 0, 0, 0); // No real members
446            else
447                return new BBox(bbox);
448        }
449    }
450
451    private BBox calculateBBox(Set<PrimitiveId> visitedRelations) {
452        if (visitedRelations.contains(this))
453            return null;
454        visitedRelations.add(this);
455
456        RelationMember[] members = this.members;
457        if (members.length == 0)
458            return null;
459        else {
460            BBox result = null;
461            for (RelationMember rm:members) {
462                BBox box = rm.isRelation() ? rm.getRelation().calculateBBox(visitedRelations) : rm.getMember().getBBox();
463                if (box != null) {
464                    if (result == null) {
465                        result = box;
466                    } else {
467                        result.add(box);
468                    }
469                }
470            }
471            return result;
472        }
473    }
474
475    @Override
476    public void updatePosition() {
477        bbox = calculateBBox(new HashSet<PrimitiveId>());
478    }
479
480    @Override
481    void setDataset(DataSet dataSet) {
482        super.setDataset(dataSet);
483        checkMembers();
484        bbox = null; // bbox might have changed if relation was in ds, was removed, modified, added back to dataset
485    }
486
487    private void checkMembers() throws DataIntegrityProblemException {
488        DataSet dataSet = getDataSet();
489        if (dataSet != null) {
490            RelationMember[] members = this.members;
491            for (RelationMember rm: members) {
492                if (rm.getMember().getDataSet() != dataSet)
493                    throw new DataIntegrityProblemException(
494                            String.format("Relation member must be part of the same dataset as relation(%s, %s)",
495                                    getPrimitiveId(), rm.getMember().getPrimitiveId()));
496            }
497            if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) {
498                for (RelationMember rm: members) {
499                    if (rm.getMember().isDeleted())
500                        throw new DataIntegrityProblemException("Deleted member referenced: " + toString());
501                }
502            }
503        }
504    }
505
506    private void fireMembersChanged() throws DataIntegrityProblemException {
507        checkMembers();
508        if (getDataSet() != null) {
509            getDataSet().fireRelationMembersChanged(this);
510        }
511    }
512
513    /**
514     * Determines if at least one child primitive is incomplete.
515     *
516     * @return true if at least one child primitive is incomplete
517     */
518    public boolean hasIncompleteMembers() {
519        RelationMember[] members = this.members;
520        for (RelationMember rm: members) {
521            if (rm.getMember().isIncomplete()) return true;
522        }
523        return false;
524    }
525
526    /**
527     * Replies a collection with the incomplete children this relation refers to.
528     *
529     * @return the incomplete children. Empty collection if no children are incomplete.
530     */
531    public Collection<OsmPrimitive> getIncompleteMembers() {
532        Set<OsmPrimitive> ret = new HashSet<>();
533        RelationMember[] members = this.members;
534        for (RelationMember rm: members) {
535            if (!rm.getMember().isIncomplete()) {
536                continue;
537            }
538            ret.add(rm.getMember());
539        }
540        return ret;
541    }
542
543    @Override
544    protected void keysChangedImpl(Map<String, String> originalKeys) {
545        super.keysChangedImpl(originalKeys);
546        for (OsmPrimitive member : getMemberPrimitives()) {
547            member.clearCachedStyle();
548        }
549    }
550
551    @Override
552    public boolean concernsArea() {
553        return isMultipolygon() && hasAreaTags();
554    }
555
556    @Override
557    public boolean isOutsideDownloadArea() {
558        return false;
559    }
560
561    /**
562     * Returns the set of roles used in this relation.
563     * @return the set of roles used in this relation. Can be empty but never null
564     * @since 7556
565     */
566    public Set<String> getMemberRoles() {
567        Set<String> result = new HashSet<>();
568        for (RelationMember rm : members) {
569            String role = rm.getRole();
570            if (!role.isEmpty()) {
571                result.add(role);
572            }
573        }
574        return result;
575    }
576}