kopia lustrzana https://github.com/JOSM/MapWithAI
Fix #22760: IAE when adding a node inside two buildings
Signed-off-by: Taylor Smock <tsmock@meta.com>pull/19/head v782
rodzic
68907f543c
commit
c88b05021c
|
@ -15,10 +15,8 @@ jobs:
|
||||||
uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v1
|
uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v1
|
||||||
with:
|
with:
|
||||||
josm-revision: "r18589"
|
josm-revision: "r18589"
|
||||||
update-pluginssource: true
|
|
||||||
plugin-jar-name: 'mapwithai'
|
plugin-jar-name: 'mapwithai'
|
||||||
|
|
||||||
|
|
||||||
createrelease:
|
createrelease:
|
||||||
needs: call-workflow
|
needs: call-workflow
|
||||||
name: Create Release
|
name: Create Release
|
||||||
|
|
|
@ -22,6 +22,7 @@ jobs:
|
||||||
uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v1
|
uses: JOSM/JOSMPluginAction/.github/workflows/ant.yml@v1
|
||||||
with:
|
with:
|
||||||
josm-revision: ${{ matrix.josm-revision }}
|
josm-revision: ${{ matrix.josm-revision }}
|
||||||
|
plugin-jar-name: 'mapwithai'
|
||||||
createtag:
|
createtag:
|
||||||
if: ${{ github.repository == 'JOSM/MapWithAI' && github.ref_type == 'branch' && github.ref_name == 'master' }}
|
if: ${{ github.repository == 'JOSM/MapWithAI' && github.ref_type == 'branch' && github.ref_name == 'master' }}
|
||||||
needs: call-workflow
|
needs: call-workflow
|
||||||
|
|
|
@ -59,13 +59,14 @@ public class MergeAddressBuildings extends AbstractConflationCommand {
|
||||||
public Command getRealCommand() {
|
public Command getRealCommand() {
|
||||||
List<Command> commands = new ArrayList<>();
|
List<Command> commands = new ArrayList<>();
|
||||||
if (MapWithAIPreferenceHelper.isMergeBuildingAddress()) {
|
if (MapWithAIPreferenceHelper.isMergeBuildingAddress()) {
|
||||||
|
final List<Node> mergedNodes = new ArrayList<>();
|
||||||
possiblyAffectedPrimitives.stream().filter(Way.class::isInstance).map(Way.class::cast)
|
possiblyAffectedPrimitives.stream().filter(Way.class::isInstance).map(Way.class::cast)
|
||||||
.filter(way -> way.hasKey(KEY)).filter(Way::isClosed)
|
.filter(way -> way.hasKey(KEY)).filter(Way::isClosed)
|
||||||
.forEach(way -> commands.addAll(mergeAddressBuilding(getAffectedDataSet(), way)));
|
.forEach(way -> commands.addAll(mergeAddressBuilding(getAffectedDataSet(), way, mergedNodes)));
|
||||||
|
|
||||||
possiblyAffectedPrimitives.stream().filter(Relation.class::isInstance).map(Relation.class::cast)
|
possiblyAffectedPrimitives.stream().filter(Relation.class::isInstance).map(Relation.class::cast)
|
||||||
.filter(rel -> rel.hasKey(KEY)).filter(Relation::isMultipolygon)
|
.filter(rel -> rel.hasKey(KEY)).filter(Relation::isMultipolygon)
|
||||||
.forEach(rel -> commands.addAll(mergeAddressBuilding(getAffectedDataSet(), rel)));
|
.forEach(rel -> commands.addAll(mergeAddressBuilding(getAffectedDataSet(), rel, mergedNodes)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Command returnCommand = null;
|
Command returnCommand = null;
|
||||||
|
@ -77,19 +78,30 @@ public class MergeAddressBuildings extends AbstractConflationCommand {
|
||||||
return returnCommand;
|
return returnCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Collection<? extends Command> mergeAddressBuilding(DataSet affectedDataSet, OsmPrimitive object) {
|
/**
|
||||||
|
* Merge a building with an address node
|
||||||
|
*
|
||||||
|
* @param affectedDataSet The dataset to use
|
||||||
|
* @param object The object to merge with an address node
|
||||||
|
* @param mergedNodes The nodes already merged. <i>This will be modified in
|
||||||
|
* this method!</i>
|
||||||
|
* @return The command to merge an address onto a building
|
||||||
|
*/
|
||||||
|
private static Collection<? extends Command> mergeAddressBuilding(DataSet affectedDataSet, OsmPrimitive object,
|
||||||
|
Collection<Node> mergedNodes) {
|
||||||
final List<IPrimitive> toCheck = new ArrayList<>(affectedDataSet.searchNodes(object.getBBox()));
|
final List<IPrimitive> toCheck = new ArrayList<>(affectedDataSet.searchNodes(object.getBBox()));
|
||||||
toCheck.removeIf(IPrimitive::isDeleted);
|
toCheck.removeIf(IPrimitive::isDeleted);
|
||||||
final Collection<IPrimitive> nodesInside = Geometry.filterInsideAnyPolygon(toCheck, object);
|
final Collection<IPrimitive> nodesInside = Geometry.filterInsideAnyPolygon(toCheck, object);
|
||||||
|
|
||||||
final List<Node> nodesWithAddresses = nodesInside.stream().filter(Node.class::isInstance).map(Node.class::cast)
|
final List<Node> nodesWithAddresses = nodesInside.stream().filter(Node.class::isInstance).map(Node.class::cast)
|
||||||
.filter(node -> node.keySet().stream().anyMatch(str -> str.startsWith("addr:")))
|
.filter(node -> node.keySet().stream().anyMatch(str -> str.startsWith("addr:")))
|
||||||
.collect(Collectors.toList());
|
.filter(node -> !mergedNodes.contains(node)).collect(Collectors.toList());
|
||||||
|
|
||||||
final List<Command> commandList = new ArrayList<>();
|
final List<Command> commandList = new ArrayList<>();
|
||||||
if (nodesWithAddresses.size() == 1 && nodesWithAddresses.stream().allMatch(n -> n.getParentWays().isEmpty())) {
|
if (nodesWithAddresses.size() == 1 && nodesWithAddresses.stream().allMatch(n -> n.getParentWays().isEmpty())) {
|
||||||
String currentKey = null;
|
String currentKey = null;
|
||||||
Node node = nodesWithAddresses.get(0);
|
Node node = nodesWithAddresses.get(0);
|
||||||
|
mergedNodes.add(node);
|
||||||
List<String> sources = new ArrayList<>();
|
List<String> sources = new ArrayList<>();
|
||||||
try {
|
try {
|
||||||
// Remove the key to avoid the popup from utilsplugin2
|
// Remove the key to avoid the popup from utilsplugin2
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
// License: GPL. For details, see LICENSE file.
|
// License: GPL. For details, see LICENSE file.
|
||||||
package org.openstreetmap.josm.plugins.mapwithai.backend;
|
package org.openstreetmap.josm.plugins.mapwithai.backend;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.Future;
|
import java.util.logging.Handler;
|
||||||
|
import java.util.logging.LogRecord;
|
||||||
|
|
||||||
import org.awaitility.Awaitility;
|
import org.awaitility.Awaitility;
|
||||||
import org.awaitility.Durations;
|
import org.awaitility.Durations;
|
||||||
|
@ -18,8 +23,8 @@ import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.junit.jupiter.api.function.ThrowingSupplier;
|
|
||||||
import org.openstreetmap.josm.TestUtils;
|
import org.openstreetmap.josm.TestUtils;
|
||||||
|
import org.openstreetmap.josm.command.DeleteCommand;
|
||||||
import org.openstreetmap.josm.data.UndoRedoHandler;
|
import org.openstreetmap.josm.data.UndoRedoHandler;
|
||||||
import org.openstreetmap.josm.data.coor.LatLon;
|
import org.openstreetmap.josm.data.coor.LatLon;
|
||||||
import org.openstreetmap.josm.data.osm.DataSet;
|
import org.openstreetmap.josm.data.osm.DataSet;
|
||||||
|
@ -38,6 +43,7 @@ import org.openstreetmap.josm.plugins.mapwithai.testutils.annotations.Wiremock;
|
||||||
import org.openstreetmap.josm.spi.preferences.Config;
|
import org.openstreetmap.josm.spi.preferences.Config;
|
||||||
import org.openstreetmap.josm.testutils.JOSMTestRules;
|
import org.openstreetmap.josm.testutils.JOSMTestRules;
|
||||||
import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
|
import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
|
||||||
|
import org.openstreetmap.josm.testutils.annotations.Projection;
|
||||||
import org.openstreetmap.josm.testutils.mockers.WindowMocker;
|
import org.openstreetmap.josm.testutils.mockers.WindowMocker;
|
||||||
import org.openstreetmap.josm.tools.Logging;
|
import org.openstreetmap.josm.tools.Logging;
|
||||||
import org.openstreetmap.josm.tools.Territories;
|
import org.openstreetmap.josm.tools.Territories;
|
||||||
|
@ -47,6 +53,7 @@ import mockit.Mock;
|
||||||
import mockit.MockUp;
|
import mockit.MockUp;
|
||||||
|
|
||||||
@BasicPreferences
|
@BasicPreferences
|
||||||
|
@Projection
|
||||||
@Wiremock
|
@Wiremock
|
||||||
class MapWithAIMoveActionTest {
|
class MapWithAIMoveActionTest {
|
||||||
private MapWithAIMoveAction moveAction;
|
private MapWithAIMoveAction moveAction;
|
||||||
|
@ -59,11 +66,13 @@ class MapWithAIMoveActionTest {
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
|
@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
|
||||||
static JOSMTestRules test = new MapWithAITestRules().main().projection().territories().assertionsInEDT();
|
JOSMTestRules test = new MapWithAITestRules().main().territories().assertionsInEDT();
|
||||||
|
|
||||||
@BeforeAll
|
@BeforeAll
|
||||||
static void beforeAll() {
|
static void beforeAll() {
|
||||||
new MapWithAIPluginMock();
|
new MapWithAIPluginMock();
|
||||||
|
// TODO remove with @Territories annotation
|
||||||
|
Territories.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
|
@ -201,9 +210,6 @@ class MapWithAIMoveActionTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testBuildingAndAddressAdd() {
|
void testBuildingAndAddressAdd() {
|
||||||
// Required to avoid an NPE in Territories.getRegionalTaginfoUrls. TODO remove
|
|
||||||
// with @Territories
|
|
||||||
Future<?> territoriesRegionalTaginfo = MainApplication.worker.submit(Territories::initialize);
|
|
||||||
DataSet ds = MapWithAIDataUtils.getLayer(true).getDataSet();
|
DataSet ds = MapWithAIDataUtils.getLayer(true).getDataSet();
|
||||||
Way building = TestUtils.newWay("building=yes", new Node(new LatLon(38.236811, -104.62571)),
|
Way building = TestUtils.newWay("building=yes", new Node(new LatLon(38.236811, -104.62571)),
|
||||||
new Node(new LatLon(38.236811, -104.625493)), new Node(new LatLon(38.236716, -104.625493)),
|
new Node(new LatLon(38.236811, -104.625493)), new Node(new LatLon(38.236716, -104.625493)),
|
||||||
|
@ -223,8 +229,6 @@ class MapWithAIMoveActionTest {
|
||||||
// This is due to only allowing 1 additional selection at a time.
|
// This is due to only allowing 1 additional selection at a time.
|
||||||
ds.setSelected(building);
|
ds.setSelected(building);
|
||||||
ds.addSelected(address);
|
ds.addSelected(address);
|
||||||
// Wait for territories to finish
|
|
||||||
assertDoesNotThrow((ThrowingSupplier<?>) territoriesRegionalTaginfo::get);
|
|
||||||
GuiHelper.runInEDTAndWaitWithException(() -> moveAction.actionPerformed(null));
|
GuiHelper.runInEDTAndWaitWithException(() -> moveAction.actionPerformed(null));
|
||||||
while (UndoRedoHandler.getInstance().hasUndoCommands()) {
|
while (UndoRedoHandler.getInstance().hasUndoCommands()) {
|
||||||
assertDoesNotThrow(() -> UndoRedoHandler.getInstance().undo());
|
assertDoesNotThrow(() -> UndoRedoHandler.getInstance().undo());
|
||||||
|
@ -269,7 +273,6 @@ class MapWithAIMoveActionTest {
|
||||||
MainApplication.getLayerManager().addLayer(osmDataLayer);
|
MainApplication.getLayerManager().addLayer(osmDataLayer);
|
||||||
MainApplication.getLayerManager().addLayer(mapwithaiLayer);
|
MainApplication.getLayerManager().addLayer(mapwithaiLayer);
|
||||||
MainApplication.getLayerManager().setActiveLayer(mapwithaiLayer);
|
MainApplication.getLayerManager().setActiveLayer(mapwithaiLayer);
|
||||||
Territories.initialize();
|
|
||||||
mapwithai.setSelected(Collections.singleton(ma1w));
|
mapwithai.setSelected(Collections.singleton(ma1w));
|
||||||
|
|
||||||
Config.getPref().put("simplify-way.auto.remember", "yes");
|
Config.getPref().put("simplify-way.auto.remember", "yes");
|
||||||
|
@ -280,4 +283,84 @@ class MapWithAIMoveActionTest {
|
||||||
assertTrue(osm.allPrimitives().size() > 4);
|
assertTrue(osm.allPrimitives().size() > 4);
|
||||||
assertTrue(osm.allPrimitives().stream().anyMatch(p -> p.hasTag("source", "digitalglobe")));
|
assertTrue(osm.allPrimitives().stream().anyMatch(p -> p.hasTag("source", "digitalglobe")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Non-regression test for #22760: IAE when merging address and building
|
||||||
|
* features
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testNonRegression22760() {
|
||||||
|
final Node originalAddrNode = TestUtils.newNode("addr:city=Topeka\naddr:housenumber=1824\naddr:postcode=66615\n"
|
||||||
|
+ "addr:street=Southwest Stutley Road\nbuilding=residential\nlbcs:activity:code=1100\n"
|
||||||
|
+ "lbcs:activity:name=Household activities\nlbcs:function:code=1101\nlbcs:function:name=Single family residence (detached)");
|
||||||
|
final Way mwaiBuilding1 = TestUtils.newWay(
|
||||||
|
"building=yes mapwithai:source=MapWithAI source=microsoft/BuildingFootprints",
|
||||||
|
new Node(new LatLon(39.0331214, -95.7912026)), new Node(new LatLon(39.0331129, -95.7909617)),
|
||||||
|
new Node(new LatLon(39.0331516, -95.7909595)), new Node(new LatLon(39.0331495, -95.7909006)),
|
||||||
|
new Node(new LatLon(39.0332057, -95.7908973)), new Node(new LatLon(39.0332074, -95.7909467)),
|
||||||
|
new Node(new LatLon(39.0332864, -95.7909421)), new Node(new LatLon(39.0332909, -95.7910693)),
|
||||||
|
new Node(new LatLon(39.0331869, -95.7910754)), new Node(new LatLon(39.0331912, -95.7911985)));
|
||||||
|
// This is an exact duplicate of building 1
|
||||||
|
final Way mwaiBuilding2 = TestUtils.newWay("", mwaiBuilding1.getNodes().toArray(new Node[0]));
|
||||||
|
final Node mwaiAddrNode = TestUtils.newNode(
|
||||||
|
"addr:city=Unincorporated\n" + "addr:housenumber=1824\n" + "addr:postcode=66615\n" + "addr:state=KS\n"
|
||||||
|
+ "addr:street=Southwest Stutley Road\n" + "building=yes\n" + "source=esri_USDOT_Kansas");
|
||||||
|
final DataSet osm = new DataSet();
|
||||||
|
final DataSet mwai = new DataSet();
|
||||||
|
final OsmDataLayer osmLayer = new OsmDataLayer(osm, "testNonRegression22760", null);
|
||||||
|
final MapWithAILayer mapWithAILayer = new MapWithAILayer(mwai, "testNonRegression22760", null);
|
||||||
|
MainApplication.getLayerManager().addLayer(osmLayer);
|
||||||
|
MainApplication.getLayerManager().addLayer(mapWithAILayer);
|
||||||
|
MainApplication.getLayerManager().setActiveLayer(mapWithAILayer);
|
||||||
|
originalAddrNode.setCoor(new LatLon(39.0331818, -95.7910286));
|
||||||
|
originalAddrNode.setOsmId(2081834687, 3);
|
||||||
|
mwaiAddrNode.setCoor(new LatLon(39.0331883, -95.7910057));
|
||||||
|
mwaiBuilding1.addNode(mwaiBuilding1.firstNode());
|
||||||
|
mwaiBuilding2.setNodes(mwaiBuilding1.getNodes());
|
||||||
|
mwaiBuilding2.putAll(mwaiBuilding1.getKeys());
|
||||||
|
osm.addPrimitive(originalAddrNode);
|
||||||
|
mwai.addPrimitiveRecursive(mwaiBuilding1);
|
||||||
|
mwai.addPrimitive(mwaiBuilding2);
|
||||||
|
mwai.addPrimitive(mwaiAddrNode);
|
||||||
|
UndoRedoHandler.getInstance()
|
||||||
|
.add(DeleteCommand.delete(Collections.singletonList(originalAddrNode), true, true));
|
||||||
|
while (mwai.getSelected().size() < mwai.allNonDeletedPrimitives().size()) {
|
||||||
|
GuiHelper.runInEDTAndWait(() -> mwai.setSelected(mwai.allPrimitives()));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<LogRecord> logs = new ArrayList<>();
|
||||||
|
final Handler testHandler = new Handler() {
|
||||||
|
@Override
|
||||||
|
public void publish(LogRecord record) {
|
||||||
|
logs.add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Logging.getLogger().addHandler(testHandler);
|
||||||
|
try {
|
||||||
|
this.moveAction.actionPerformed(null);
|
||||||
|
assertAll(logs.stream().map(logRecord -> () -> fail(logRecord.getThrown())));
|
||||||
|
} finally {
|
||||||
|
Logging.getLogger().removeHandler(testHandler);
|
||||||
|
}
|
||||||
|
assertEquals(2, osm.getWays().size(), "Both buildings should be added, even though one is a duplicate. "
|
||||||
|
+ "If deduplicate buildings in the future, this test should be updated.");
|
||||||
|
final Way actualWay = osm.getWays().stream().filter(way -> way.getNumKeys() > 3).findFirst()
|
||||||
|
.orElseThrow(() -> new AssertionError("Could not find way"));
|
||||||
|
// Just spot check the tags.
|
||||||
|
assertEquals("Unincorporated", actualWay.get("addr:city"));
|
||||||
|
assertEquals("yes", actualWay.get("building"));
|
||||||
|
assertEquals(10, osm.getNodes().stream().filter(node -> !node.isDeleted()).count());
|
||||||
|
assertAll(osm.getNodes().stream().filter(node -> !node.isDeleted())
|
||||||
|
.map(node -> () -> assertTrue(node.getParentWays().contains(actualWay))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Ładowanie…
Reference in New Issue