Add new conflation command to merge address nodes onto buildings

Also some conflation commands can be undone without move the object back
to the MapWithAI layer.

Signed-off-by: Taylor Smock <taylor.smock@kaart.com>
pull/1/head
Taylor Smock 2020-01-15 16:14:42 -07:00
rodzic 41105000c0
commit d05eca89d8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 625F6A74A3E4311A
7 zmienionych plików z 217 dodań i 22 usunięć

Wyświetl plik

@ -132,4 +132,10 @@ public abstract class AbstractConflationCommand extends Command {
}
}
}
/**
* @return true if the command should show as a separate command in the
* undo/redo lists
*/
public abstract boolean allowUndo();
}

Wyświetl plik

@ -94,4 +94,9 @@ public class ConnectedCommand extends AbstractConflationCommand {
}
return returnCommand;
}
@Override
public boolean allowUndo() {
return false;
}
}

Wyświetl plik

@ -104,4 +104,9 @@ public class DuplicateCommand extends AbstractConflationCommand {
}
return returnCommand;
}
@Override
public boolean allowUndo() {
return false;
}
}

Wyświetl plik

@ -1,13 +1,17 @@
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.DataSet;
@ -19,9 +23,10 @@ import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIPreferenceHelper;
import org.openstreetmap.josm.plugins.utilsplugin2.replacegeometry.ReplaceGeometryUtils;
import org.openstreetmap.josm.tools.Geometry;
import org.openstreetmap.josm.tools.Logging;
public class MergeAddressBuildings extends AbstractConflationCommand {
private static final String BUILDING_KEY = "building";
public static final String KEY = "building";
public MergeAddressBuildings(DataSet data) {
super(data);
@ -39,7 +44,7 @@ public class MergeAddressBuildings extends AbstractConflationCommand {
@Override
public String getKey() {
return BUILDING_KEY;
return KEY;
}
@Override
@ -47,16 +52,18 @@ public class MergeAddressBuildings extends AbstractConflationCommand {
List<Command> commands = new ArrayList<>();
if (MapWithAIPreferenceHelper.isMergeBuildingAddress()) {
possiblyAffectedPrimitives.stream().filter(Way.class::isInstance).map(Way.class::cast)
.filter(way -> way.hasKey(BUILDING_KEY)).filter(Way::isClosed)
.filter(way -> way.hasKey(KEY)).filter(Way::isClosed)
.forEach(way -> commands.addAll(mergeAddressBuilding(getAffectedDataSet(), way)));
possiblyAffectedPrimitives.stream().filter(Relation.class::isInstance).map(Relation.class::cast)
.filter(rel -> rel.hasKey(BUILDING_KEY)).filter(Relation::isMultipolygon)
.filter(rel -> rel.hasKey(KEY)).filter(Relation::isMultipolygon)
.forEach(rel -> commands.addAll(mergeAddressBuilding(getAffectedDataSet(), rel)));
}
Command returnCommand = null;
if (!commands.isEmpty()) {
if (commands.size() == 1) {
returnCommand = commands.get(0);
} else if (!commands.isEmpty()) {
returnCommand = new SequenceCommand(getDescriptionText(), commands);
}
return returnCommand;
@ -83,15 +90,25 @@ public class MergeAddressBuildings extends AbstractConflationCommand {
String currentKey = null;
try {
// Remove the key to avoid the popup from utilsplugin2
currentKey = object.get(BUILDING_KEY);
object.remove(BUILDING_KEY);
commandList.add(ReplaceGeometryUtils.buildUpgradeNodeCommand(nodesWithAddresses.get(0), object));
currentKey = object.get(KEY);
object.remove(KEY);
try {
SwingUtilities.invokeAndWait(() -> commandList
.add(ReplaceGeometryUtils.buildUpgradeNodeCommand(nodesWithAddresses.get(0), object)));
} catch (InvocationTargetException | InterruptedException e) {
Logging.error(e);
}
} finally {
if (currentKey != null) {
object.put(BUILDING_KEY, currentKey);
object.put(KEY, currentKey);
}
}
}
return commandList;
}
@Override
public boolean allowUndo() {
return true;
}
}

Wyświetl plik

@ -0,0 +1,134 @@
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.DeleteCommand;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.BBox;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIPreferenceHelper;
import org.openstreetmap.josm.tools.Geometry;
public class MergeBuildingAddress extends AbstractConflationCommand {
public static final String KEY = "addr:housenumber";
public MergeBuildingAddress(DataSet data) {
super(data);
}
@Override
public String getDescriptionText() {
return tr("Merge added addresses with existing buildings");
}
@Override
public Collection<Class<? extends OsmPrimitive>> getInterestedTypes() {
return Collections.singleton(Node.class);
}
@Override
public String getKey() {
return KEY;
}
@Override
public Command getRealCommand() {
List<Command> commands = new ArrayList<>();
if (MapWithAIPreferenceHelper.isMergeBuildingAddress()) {
possiblyAffectedPrimitives.stream().filter(Node.class::isInstance).map(Node.class::cast)
.filter(n -> n.hasKey(KEY))
.forEach(n -> commands.addAll(mergeBuildingAddress(getAffectedDataSet(), n)));
}
Command returnCommand = null;
if (commands.size() == 1) {
returnCommand = commands.get(0);
} else if (!commands.isEmpty()) {
returnCommand = new SequenceCommand(getDescriptionText(), commands);
}
return returnCommand;
}
private static Collection<Command> mergeBuildingAddress(DataSet affectedDataSet, Node node) {
final List<OsmPrimitive> toCheck = new ArrayList<>();
final BBox bbox = new BBox(node.getCoor().getX(), node.getCoor().getY(), 0.001);
toCheck.addAll(affectedDataSet.searchWays(bbox));
toCheck.addAll(affectedDataSet.searchRelations(bbox));
toCheck.addAll(affectedDataSet.searchNodes(bbox));
List<OsmPrimitive> possibleDuplicates = toCheck.parallelStream().filter(prim -> prim.hasTag(KEY))
.filter(prim -> prim.get(KEY).equals(node.get(KEY))).filter(prim -> !prim.equals(node))
.collect(Collectors.toList());
for (String tag : Arrays.asList("addr:street", "addr:unit")) {
if (node.hasTag(tag)) {
possibleDuplicates = possibleDuplicates.parallelStream().filter(prim -> prim.hasTag(tag))
.filter(prim -> prim.get(tag).equals(node.get(tag))).collect(Collectors.toList());
}
}
List<OsmPrimitive> buildings = toCheck.parallelStream().filter(prim -> prim.hasTag("building"))
.filter(prim -> checkInside(node, prim)).collect(Collectors.toList());
final List<Command> commandList = new ArrayList<>();
if (possibleDuplicates.size() == 1) {
commandList.add(new ChangePropertyCommand(possibleDuplicates, node.getKeys()));
commandList.add(DeleteCommand.delete(Collections.singleton(node)));
} else if (buildings.size() == 1 && getAddressPoints(buildings.get(0)).size() == 1) {
commandList.add(new ChangePropertyCommand(buildings, node.getKeys()));
commandList.add(DeleteCommand.delete(Collections.singleton(node)));
}
return commandList;
}
private static Collection<Node> getAddressPoints(OsmPrimitive prim) {
if (prim instanceof Way && ((Way) prim).isClosed()) {
return Geometry
.filterInsidePolygon(new ArrayList<>(prim.getDataSet().allNonDeletedPrimitives()), (Way) prim)
.parallelStream().filter(Node.class::isInstance).map(Node.class::cast).filter(n -> n.hasTag(KEY))
.collect(Collectors.toList());
} else if (prim instanceof Relation) {
return Geometry
.filterInsideMultipolygon(new ArrayList<>(prim.getDataSet().allNonDeletedPrimitives()),
(Relation) prim)
.parallelStream().filter(Node.class::isInstance).map(Node.class::cast).filter(n -> n.hasKey(KEY))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
/**
* Check if the node is inside the other primitive
*
* @param node The node to check
* @param prim The primitive that the node may be inside
* @return true if the node is inside the primitive
*/
private static boolean checkInside(Node node, OsmPrimitive prim) {
if (prim instanceof Relation) {
return !Geometry.filterInsideMultipolygon(Collections.singleton(node), (Relation) prim).isEmpty();
} else if (prim instanceof Way) {
return !Geometry.filterInsidePolygon(Arrays.asList(node), (Way) prim).isEmpty();
}
return false;
}
@Override
public boolean allowUndo() {
return true;
}
}

Wyświetl plik

@ -5,6 +5,7 @@ import static org.openstreetmap.josm.tools.I18n.tr;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
@ -14,8 +15,11 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import javax.swing.SwingUtilities;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.UndoRedoHandler;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.plugins.mapwithai.MapWithAIPlugin;
@ -23,17 +27,20 @@ import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.Abst
import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.ConnectedCommand;
import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.DuplicateCommand;
import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.MergeAddressBuildings;
import org.openstreetmap.josm.plugins.mapwithai.backend.commands.conflation.MergeBuildingAddress;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Utils;
public class CreateConnectionsCommand extends Command {
private final Collection<OsmPrimitive> primitives;
private Command command;
private Command undoCommands;
private static final LinkedHashSet<Class<? extends AbstractConflationCommand>> CONFLATION_COMMANDS = new LinkedHashSet<>();
static {
CONFLATION_COMMANDS.add(ConnectedCommand.class);
CONFLATION_COMMANDS.add(DuplicateCommand.class);
CONFLATION_COMMANDS.add(MergeAddressBuildings.class);
CONFLATION_COMMANDS.add(MergeBuildingAddress.class);
}
public CreateConnectionsCommand(DataSet data, Collection<OsmPrimitive> primitives) {
@ -44,11 +51,17 @@ public class CreateConnectionsCommand extends Command {
@Override
public boolean executeCommand() {
if (command == null) {
command = createConnections(getAffectedDataSet(), primitives);
List<Command> commands = createConnections(getAffectedDataSet(), primitives);
command = commands.get(0);
undoCommands = commands.get(1);
}
if (command != null) {
command.executeCommand();
}
if (undoCommands != null) {
undoCommands.executeCommand();
SwingUtilities.invokeLater(() -> UndoRedoHandler.getInstance().add(undoCommands, false));
}
return true;
}
@ -66,10 +79,13 @@ public class CreateConnectionsCommand extends Command {
* connecting to
* @param collection The primitives with connection information (currently only
* checks Nodes)
* @return A {@link Command} to create connections with
* @return A list {@link Command} to create connections with (first is one that
* can be folded into other commands, second is one that should be
* undoable individually)
*/
public static Command createConnections(DataSet dataSet, Collection<OsmPrimitive> collection) {
final List<Command> changedKeyList = new ArrayList<>();
public static List<Command> createConnections(DataSet dataSet, Collection<OsmPrimitive> collection) {
final List<Command> permanent = new ArrayList<>();
final List<Command> undoable = new ArrayList<>();
final Collection<OsmPrimitive> realPrimitives = collection.stream().map(dataSet::getPrimitiveById)
.filter(Objects::nonNull).collect(Collectors.toList());
for (final Class<? extends AbstractConflationCommand> abstractCommandClass : getConflationCommands()) {
@ -88,18 +104,29 @@ public class CreateConnectionsCommand extends Command {
final Command actualCommand = abstractCommand.getCommand(tPrimitives.stream()
.filter(prim -> prim.hasKey(abstractCommand.getKey())).collect(Collectors.toList()));
if (Objects.nonNull(actualCommand)) {
changedKeyList.add(actualCommand);
if (abstractCommand.allowUndo()) {
undoable.add(actualCommand);
} else {
permanent.add(actualCommand);
}
}
}
Command returnSequence = null;
if (changedKeyList.size() == 1) {
returnSequence = changedKeyList.get(0);
} else if (!changedKeyList.isEmpty()) {
returnSequence = new SequenceCommand(getRealDescriptionText(), changedKeyList);
Command permanentCommand = null;
if (permanent.size() == 1) {
permanentCommand = permanent.get(0);
} else if (!permanent.isEmpty()) {
permanentCommand = new SequenceCommand(getRealDescriptionText(), permanent);
}
return returnSequence;
Command undoCommand = null;
if (undoable.size() == 1) {
undoCommand = undoable.get(0);
} else if (!undoable.isEmpty()) {
undoCommand = new SequenceCommand(getRealDescriptionText(), undoable);
}
return Arrays.asList(permanentCommand, undoCommand);
}
@Override

Wyświetl plik

@ -178,7 +178,8 @@ public class CreateConnectionsCommandTest {
final Node node1 = new Node(new LatLon(39.0674124, -108.5592645));
final DataSet dataSet = new DataSet(node1);
node1.put(DuplicateCommand.KEY, "n6146500887");
Command replaceNodeCommand = CreateConnectionsCommand.createConnections(dataSet, Collections.singleton(node1));
Command replaceNodeCommand = CreateConnectionsCommand.createConnections(dataSet, Collections.singleton(node1))
.get(0);
replaceNodeCommand.executeCommand();
assertEquals(1, dataSet.allNonDeletedPrimitives().size(), "There should be one primitive left");
@ -195,7 +196,7 @@ public class CreateConnectionsCommandTest {
final OsmDataLayer layer = new OsmDataLayer(dataSet, "temp layer", null);
MainApplication.getLayerManager().addLayer(layer);
replaceNodeCommand = CreateConnectionsCommand.createConnections(dataSet, Collections.singleton(node1));
replaceNodeCommand = CreateConnectionsCommand.createConnections(dataSet, Collections.singleton(node1)).get(0);
replaceNodeCommand.executeCommand();
assertEquals(2, dataSet.allNonDeletedPrimitives().size(), "The dupe node no longer matches with the OSM node");
assertNotNull(dataSet.getPrimitiveById(6146500887L, OsmPrimitiveType.NODE), "The OSM node should still exist");