// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.validation.tests;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.DeleteCommand;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.validation.Severity;
import org.openstreetmap.josm.data.validation.Test;
import org.openstreetmap.josm.data.validation.TestError;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.tools.MultiMap;
/**
* Tests if there are duplicate relations
*/
public class DuplicateRelation extends Test {
/**
* Class to store one relation members and information about it
*/
public static class RelMember {
/** Role of the relation member */
private final String role;
/** Type of the relation member */
private final OsmPrimitiveType type;
/** Tags of the relation member */
private Map<String, String> tags;
/** Coordinates of the relation member */
private List<LatLon> coor;
/** ID of the relation member in case it is a {@link Relation} */
private long relId;
@Override
public int hashCode() {
return Objects.hash(role, type, tags, coor, relId);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
RelMember relMember = (RelMember) obj;
return relId == relMember.relId &&
type == relMember.type &&
Objects.equals(role, relMember.role) &&
Objects.equals(tags, relMember.tags) &&
Objects.equals(coor, relMember.coor);
}
/** Extract and store relation information based on the relation member
* @param src The relation member to store information about
*/
public RelMember(RelationMember src) {
role = src.getRole();
type = src.getType();
relId = 0;
coor = new ArrayList<>();
if (src.isNode()) {
Node r = src.getNode();
tags = r.getKeys();
coor = new ArrayList<>(1);
coor.add(r.getCoor());
}
if (src.isWay()) {
Way r = src.getWay();
tags = r.getKeys();
List<Node> wNodes = r.getNodes();
coor = new ArrayList<>(wNodes.size());
for (Node wNode : wNodes) {
coor.add(wNode.getCoor());
}
}
if (src.isRelation()) {
Relation r = src.getRelation();
tags = r.getKeys();
relId = r.getId();
coor = new ArrayList<>();
}
}
}
/**
* Class to store relation members
*/
private static class RelationMembers {
/** List of member objects of the relation */
private final List<RelMember> members;
/** Store relation information
* @param members The list of relation members
*/
RelationMembers(List<RelationMember> members) {
this.members = new ArrayList<>(members.size());
for (RelationMember member : members) {
this.members.add(new RelMember(member));
}
}
@Override
public int hashCode() {
return Objects.hash(members);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
RelationMembers that = (RelationMembers) obj;
return Objects.equals(members, that.members);
}
}
/**
* Class to store relation data (keys are usually cleanup and may not be equal to original relation)
*/
private static class RelationPair {
/** Member objects of the relation */
private final RelationMembers members;
/** Tags of the relation */
private final Map<String, String> keys;
/** Store relation information
* @param members The list of relation members
* @param keys The set of tags of the relation
*/
RelationPair(List<RelationMember> members, Map<String, String> keys) {
this.members = new RelationMembers(members);
this.keys = keys;
}
@Override
public int hashCode() {
return Objects.hash(members, keys);
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
RelationPair that = (RelationPair) obj;
return Objects.equals(members, that.members) &&
Objects.equals(keys, that.keys);
}
}
/** Code number of completely duplicated relation error */
protected static final int DUPLICATE_RELATION = 1901;
/** Code number of relation with same members error */
protected static final int SAME_RELATION = 1902;
/** MultiMap of all relations */
private MultiMap<RelationPair, OsmPrimitive> relations;
/** MultiMap of all relations, regardless of keys */
private MultiMap<List<RelationMember>, OsmPrimitive> relationsNoKeys;
/** List of keys without useful information */
private final Set<String> ignoreKeys = new HashSet<>(OsmPrimitive.getUninterestingKeys());
/**
* Default constructor
*/
public DuplicateRelation() {
super(tr("Duplicated relations"),
tr("This test checks that there are no relations with same tags and same members with same roles."));
}
@Override
public void startTest(ProgressMonitor monitor) {
super.startTest(monitor);
relations = new MultiMap<>(1000);
relationsNoKeys = new MultiMap<>(1000);
}
@Override
public void endTest() {
super.endTest();
for (Set<OsmPrimitive> duplicated : relations.values()) {
if (duplicated.size() > 1) {
TestError testError = TestError.builder(this, Severity.ERROR, DUPLICATE_RELATION)
.message(tr("Duplicated relations"))
.primitives(duplicated)
.build();
errors.add(testError);
}
}
relations = null;
for (Set<OsmPrimitive> duplicated : relationsNoKeys.values()) {
if (duplicated.size() > 1) {
TestError testError = TestError.builder(this, Severity.WARNING, SAME_RELATION)
.message(tr("Relations with same members"))
.primitives(duplicated)
.build();
errors.add(testError);
}
}
relationsNoKeys = null;
}
@Override
public void visit(Relation r) {
if (!r.isUsable() || r.hasIncompleteMembers() || "tmc".equals(r.get("type")) || "TMC".equals(r.get("type")))
return;
List<RelationMember> rMembers = r.getMembers();
Map<String, String> rkeys = r.getKeys();
for (String key : ignoreKeys) {
rkeys.remove(key);
}
RelationPair rKey = new RelationPair(rMembers, rkeys);
relations.put(rKey, r);
relationsNoKeys.put(rMembers, r);
}
/**
* Fix the error by removing all but one instance of duplicate relations
* @param testError The error to fix, must be of type {@link #DUPLICATE_RELATION}
*/
@Override
public Command fixError(TestError testError) {
if (testError.getCode() == SAME_RELATION) return null;
Collection<? extends OsmPrimitive> sel = testError.getPrimitives();
Set<Relation> relFix = new HashSet<>();
for (OsmPrimitive osm : sel) {
if (osm instanceof Relation && !osm.isDeleted()) {
relFix.add((Relation) osm);
}
}
if (relFix.size() < 2)
return null;
long idToKeep = 0;
Relation relationToKeep = relFix.iterator().next();
// Find the relation that is member of one or more relations. (If any)
Relation relationWithRelations = null;
List<Relation> relRef = null;
for (Relation w : relFix) {
List<Relation> rel = OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class);
if (!rel.isEmpty()) {
if (relationWithRelations != null)
throw new AssertionError("Cannot fix duplicate relations: More than one relation is member of another relation.");
relationWithRelations = w;
relRef = rel;
}
// Only one relation will be kept - the one with lowest positive ID, if such exist
// or one "at random" if no such exists. Rest of the relations will be deleted
if (!w.isNew() && (idToKeep == 0 || w.getId() < idToKeep)) {
idToKeep = w.getId();
relationToKeep = w;
}
}
Collection<Command> commands = new LinkedList<>();
// Fix relations.
if (relationWithRelations != null && relationToKeep != relationWithRelations) {
for (Relation rel : relRef) {
Relation newRel = new Relation(rel);
for (int i = 0; i < newRel.getMembers().size(); ++i) {
RelationMember m = newRel.getMember(i);
if (relationWithRelations.equals(m.getMember())) {
newRel.setMember(i, new RelationMember(m.getRole(), relationToKeep));
}
}
commands.add(new ChangeCommand(rel, newRel));
}
}
// Delete all relations in the list
relFix.remove(relationToKeep);
commands.add(new DeleteCommand(relFix));
return new SequenceCommand(tr("Delete duplicate relations"), commands);
}
@Override
public boolean isFixable(TestError testError) {
if (!(testError.getTester() instanceof DuplicateRelation)
|| testError.getCode() == SAME_RELATION) return false;
// We fix it only if there is no more than one relation that is relation member.
Collection<? extends OsmPrimitive> sel = testError.getPrimitives();
Set<Relation> rels = new HashSet<>();
for (OsmPrimitive osm : sel) {
if (osm instanceof Relation) {
rels.add((Relation) osm);
}
}
if (rels.size() < 2)
return false;
int relationsWithRelations = 0;
for (Relation w : rels) {
List<Relation> rel = OsmPrimitive.getFilteredList(w.getReferrers(), Relation.class);
if (!rel.isEmpty()) {
++relationsWithRelations;
}
}
return relationsWithRelations <= 1;
}
}