// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.visitor.PrimitiveVisitor;
import org.openstreetmap.josm.data.osm.visitor.Visitor;
import org.openstreetmap.josm.tools.CopyList;
import org.openstreetmap.josm.tools.SubclassFilteredCollection;
import org.openstreetmap.josm.tools.Utils;
/**
* A relation, having a set of tags and any number (0...n) of members.
*
* @author Frederik Ramm
*/
public final class Relation extends OsmPrimitive implements IRelation {
private RelationMember[] members = new RelationMember[0];
private BBox bbox;
/**
* @return Members of the relation. Changes made in returned list are not mapped
* back to the primitive, use setMembers() to modify the members
* @since 1925
*/
public List<RelationMember> getMembers() {
return new CopyList<>(members);
}
/**
*
* @param members Can be null, in that case all members are removed
* @since 1925
*/
public void setMembers(List<RelationMember> members) {
boolean locked = writeLock();
try {
for (RelationMember rm : this.members) {
rm.getMember().removeReferrer(this);
rm.getMember().clearCachedStyle();
}
if (members != null) {
this.members = members.toArray(new RelationMember[members.size()]);
} else {
this.members = new RelationMember[0];
}
for (RelationMember rm : this.members) {
rm.getMember().addReferrer(this);
rm.getMember().clearCachedStyle();
}
fireMembersChanged();
} finally {
writeUnlock(locked);
}
}
@Override
public int getMembersCount() {
return members.length;
}
/**
* Returns the relation member at the specified index.
* @param index the index of the relation member
* @return relation member at the specified index
*/
public RelationMember getMember(int index) {
return members[index];
}
/**
* Adds the specified relation member at the last position.
* @param member the member to add
*/
public void addMember(RelationMember member) {
boolean locked = writeLock();
try {
members = Utils.addInArrayCopy(members, member);
member.getMember().addReferrer(this);
member.getMember().clearCachedStyle();
fireMembersChanged();
} finally {
writeUnlock(locked);
}
}
/**
* Adds the specified relation member at the specified index.
* @param member the member to add
* @param index the index at which the specified element is to be inserted
*/
public void addMember(int index, RelationMember member) {
boolean locked = writeLock();
try {
RelationMember[] newMembers = new RelationMember[members.length + 1];
System.arraycopy(members, 0, newMembers, 0, index);
System.arraycopy(members, index, newMembers, index + 1, members.length - index);
newMembers[index] = member;
members = newMembers;
member.getMember().addReferrer(this);
member.getMember().clearCachedStyle();
fireMembersChanged();
} finally {
writeUnlock(locked);
}
}
/**
* Replace member at position specified by index.
* @param index index (positive integer)
* @param member relation member to set
* @return Member that was at the position
*/
public RelationMember setMember(int index, RelationMember member) {
boolean locked = writeLock();
try {
RelationMember originalMember = members[index];
members[index] = member;
if (originalMember.getMember() != member.getMember()) {
member.getMember().addReferrer(this);
member.getMember().clearCachedStyle();
originalMember.getMember().removeReferrer(this);
originalMember.getMember().clearCachedStyle();
fireMembersChanged();
}
return originalMember;
} finally {
writeUnlock(locked);
}
}
/**
* Removes member at specified position.
* @param index index (positive integer)
* @return Member that was at the position
*/
public RelationMember removeMember(int index) {
boolean locked = writeLock();
try {
List<RelationMember> members = getMembers();
RelationMember result = members.remove(index);
setMembers(members);
return result;
} finally {
writeUnlock(locked);
}
}
@Override
public long getMemberId(int idx) {
return members[idx].getUniqueId();
}
@Override
public String getRole(int idx) {
return members[idx].getRole();
}
@Override
public OsmPrimitiveType getMemberType(int idx) {
return members[idx].getType();
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
@Override
public void accept(PrimitiveVisitor visitor) {
visitor.visit(this);
}
protected Relation(long id, boolean allowNegative) {
super(id, allowNegative);
}
/**
* Create a new relation with id 0
*/
public Relation() {
super(0, false);
}
/**
* Constructs an identical clone of the argument.
* @param clone The relation to clone
* @param clearMetadata If {@code true}, clears the OSM id and other metadata as defined by {@link #clearOsmMetadata}.
* If {@code false}, does nothing
*/
public Relation(Relation clone, boolean clearMetadata) {
super(clone.getUniqueId(), true);
cloneFrom(clone);
if (clearMetadata) {
clearOsmMetadata();
}
}
/**
* Create an identical clone of the argument (including the id)
* @param clone The relation to clone, including its id
*/
public Relation(Relation clone) {
this(clone, false);
}
/**
* Creates a new relation for the given id. If the id > 0, the way is marked
* as incomplete.
*
* @param id the id. > 0 required
* @throws IllegalArgumentException if id < 0
*/
public Relation(long id) {
super(id, false);
}
/**
* Creates new relation
* @param id the id
* @param version version number (positive integer)
*/
public Relation(long id, int version) {
super(id, version, false);
}
@Override
public void cloneFrom(OsmPrimitive osm) {
if (!(osm instanceof Relation))
throw new IllegalArgumentException("Not a relation: " + osm);
boolean locked = writeLock();
try {
super.cloneFrom(osm);
// It's not necessary to clone members as RelationMember class is immutable
setMembers(((Relation) osm).getMembers());
} finally {
writeUnlock(locked);
}
}
@Override
public void load(PrimitiveData data) {
if (!(data instanceof RelationData))
throw new IllegalArgumentException("Not a relation data: " + data);
boolean locked = writeLock();
try {
super.load(data);
RelationData relationData = (RelationData) data;
List<RelationMember> newMembers = new ArrayList<>();
for (RelationMemberData member : relationData.getMembers()) {
newMembers.add(new RelationMember(member.getRole(), Optional.ofNullable(getDataSet().getPrimitiveById(member))
.orElseThrow(() -> new AssertionError("Data consistency problem - relation with missing member detected"))));
}
setMembers(newMembers);
} finally {
writeUnlock(locked);
}
}
@Override
public RelationData save() {
RelationData data = new RelationData();
saveCommonAttributes(data);
for (RelationMember member:getMembers()) {
data.getMembers().add(new RelationMemberData(member.getRole(), member.getMember()));
}
return data;
}
@Override
public String toString() {
StringBuilder result = new StringBuilder(32);
result.append("{Relation id=")
.append(getUniqueId())
.append(" version=")
.append(getVersion())
.append(' ')
.append(getFlagsAsString())
.append(" [");
for (RelationMember rm:getMembers()) {
result.append(OsmPrimitiveType.from(rm.getMember()))
.append(' ')
.append(rm.getMember().getUniqueId())
.append(", ");
}
result.delete(result.length()-2, result.length())
.append("]}");
return result.toString();
}
@Override
public boolean hasEqualSemanticAttributes(OsmPrimitive other, boolean testInterestingTagsOnly) {
return (other instanceof Relation)
&& hasEqualSemanticFlags(other)
&& Arrays.equals(members, ((Relation) other).members)
&& super.hasEqualSemanticAttributes(other, testInterestingTagsOnly);
}
@Override
public int compareTo(OsmPrimitive o) {
return o instanceof Relation ? Long.compare(getUniqueId(), o.getUniqueId()) : -1;
}
/**
* Returns the first member.
* @return first member, or {@code null}
*/
public RelationMember firstMember() {
return (isIncomplete() || members.length == 0) ? null : members[0];
}
/**
* Returns the last member.
* @return last member, or {@code null}
*/
public RelationMember lastMember() {
return (isIncomplete() || members.length == 0) ? null : members[members.length - 1];
}
/**
* removes all members with member.member == primitive
*
* @param primitive the primitive to check for
*/
public void removeMembersFor(OsmPrimitive primitive) {
removeMembersFor(Collections.singleton(primitive));
}
@Override
public void setDeleted(boolean deleted) {
boolean locked = writeLock();
try {
for (RelationMember rm:members) {
if (deleted) {
rm.getMember().removeReferrer(this);
} else {
rm.getMember().addReferrer(this);
}
}
super.setDeleted(deleted);
} finally {
writeUnlock(locked);
}
}
/**
* Obtains all members with member.member == primitive
* @param primitives the primitives to check for
* @return all relation members for the given primitives
*/
public Collection<RelationMember> getMembersFor(final Collection<? extends OsmPrimitive> primitives) {
return SubclassFilteredCollection.filter(getMembers(), member -> primitives.contains(member.getMember()));
}
/**
* removes all members with member.member == primitive
*
* @param primitives the primitives to check for
* @since 5613
*/
public void removeMembersFor(Collection<? extends OsmPrimitive> primitives) {
if (primitives == null || primitives.isEmpty())
return;
boolean locked = writeLock();
try {
List<RelationMember> members = getMembers();
members.removeAll(getMembersFor(primitives));
setMembers(members);
} finally {
writeUnlock(locked);
}
}
@Override
public String getDisplayName(NameFormatter formatter) {
return formatter.format(this);
}
/**
* Replies the set of {@link OsmPrimitive}s referred to by at least one
* member of this relation
*
* @return the set of {@link OsmPrimitive}s referred to by at least one
* member of this relation
* @see #getMemberPrimitivesList()
*/
public Set<OsmPrimitive> getMemberPrimitives() {
return getMembers().stream().map(RelationMember::getMember).collect(Collectors.toSet());
}
/**
* Returns the {@link OsmPrimitive}s of the specified type referred to by at least one member of this relation.
* @param tClass the type of the primitive
* @param <T> the type of the primitive
* @return the primitives
*/
public <T extends OsmPrimitive> Collection<T> getMemberPrimitives(Class<T> tClass) {
return Utils.filteredCollection(getMemberPrimitivesList(), tClass);
}
/**
* Returns an unmodifiable list of the {@link OsmPrimitive}s referred to by at least one member of this relation.
* @return an unmodifiable list of the primitives
*/
public List<OsmPrimitive> getMemberPrimitivesList() {
return Utils.transform(getMembers(), RelationMember::getMember);
}
@Override
public OsmPrimitiveType getType() {
return OsmPrimitiveType.RELATION;
}
@Override
public OsmPrimitiveType getDisplayType() {
return isMultipolygon() && !isBoundary() ? OsmPrimitiveType.MULTIPOLYGON : OsmPrimitiveType.RELATION;
}
/**
* Determines if this relation is a boundary.
* @return {@code true} if a boundary relation
*/
public boolean isBoundary() {
return "boundary".equals(get("type"));
}
@Override
public boolean isMultipolygon() {
return "multipolygon".equals(get("type")) || isBoundary();
}
@Override
public BBox getBBox() {
if (getDataSet() != null && bbox != null)
return new BBox(bbox); // use cached value
BBox box = new BBox();
addToBBox(box, new HashSet<PrimitiveId>());
if (getDataSet() != null)
setBBox(box); // set cache
return new BBox(box);
}
private void setBBox(BBox bbox) {
this.bbox = bbox;
}
@Override
protected void addToBBox(BBox box, Set<PrimitiveId> visited) {
for (RelationMember rm : members) {
if (visited.add(rm.getMember()))
rm.getMember().addToBBox(box, visited);
}
}
@Override
public void updatePosition() {
setBBox(null); // make sure that it is recalculated
setBBox(getBBox());
}
@Override
void setDataset(DataSet dataSet) {
super.setDataset(dataSet);
checkMembers();
setBBox(null); // bbox might have changed if relation was in ds, was removed, modified, added back to dataset
}
/**
* Checks that members are part of the same dataset, and that they're not deleted.
* @throws DataIntegrityProblemException if one the above conditions is not met
*/
private void checkMembers() {
DataSet dataSet = getDataSet();
if (dataSet != null) {
RelationMember[] members = this.members;
for (RelationMember rm: members) {
if (rm.getMember().getDataSet() != dataSet)
throw new DataIntegrityProblemException(
String.format("Relation member must be part of the same dataset as relation(%s, %s)",
getPrimitiveId(), rm.getMember().getPrimitiveId()));
}
if (Main.pref.getBoolean("debug.checkDeleteReferenced", true)) {
for (RelationMember rm: members) {
if (rm.getMember().isDeleted())
throw new DataIntegrityProblemException("Deleted member referenced: " + toString());
}
}
}
}
/**
* Fires the {@code RelationMembersChangedEvent} to listeners.
* @throws DataIntegrityProblemException if members are not valid
* @see #checkMembers
*/
private void fireMembersChanged() {
checkMembers();
if (getDataSet() != null) {
getDataSet().fireRelationMembersChanged(this);
}
}
/**
* Determines if at least one child primitive is incomplete.
*
* @return true if at least one child primitive is incomplete
*/
public boolean hasIncompleteMembers() {
RelationMember[] members = this.members;
for (RelationMember rm: members) {
if (rm.getMember().isIncomplete()) return true;
}
return false;
}
/**
* Replies a collection with the incomplete children this relation refers to.
*
* @return the incomplete children. Empty collection if no children are incomplete.
*/
public Collection<OsmPrimitive> getIncompleteMembers() {
Set<OsmPrimitive> ret = new HashSet<>();
RelationMember[] members = this.members;
for (RelationMember rm: members) {
if (!rm.getMember().isIncomplete()) {
continue;
}
ret.add(rm.getMember());
}
return ret;
}
@Override
protected void keysChangedImpl(Map<String, String> originalKeys) {
super.keysChangedImpl(originalKeys);
for (OsmPrimitive member : getMemberPrimitivesList()) {
member.clearCachedStyle();
}
}
@Override
public boolean concernsArea() {
return isMultipolygon() && hasAreaTags();
}
@Override
public boolean isOutsideDownloadArea() {
return false;
}
/**
* Returns the set of roles used in this relation.
* @return the set of roles used in this relation. Can be empty but never null
* @since 7556
*/
public Set<String> getMemberRoles() {
Set<String> result = new HashSet<>();
for (RelationMember rm : members) {
String role = rm.getRole();
if (!role.isEmpty()) {
result.add(role);
}
}
return result;
}
}