kopia lustrzana https://github.com/JOSM/MapWithAI
Initial commit for checking street address order
Signed-off-by: Taylor Smock <taylor.smock@kaart.com>pull/1/head
rodzic
c1bc8ce95f
commit
d0b7cc470d
|
@ -38,6 +38,7 @@ import org.openstreetmap.josm.plugins.mapwithai.backend.MapWithAIUploadHook;
|
||||||
import org.openstreetmap.josm.plugins.mapwithai.backend.MergeDuplicateWaysAction;
|
import org.openstreetmap.josm.plugins.mapwithai.backend.MergeDuplicateWaysAction;
|
||||||
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.ConnectingNodeInformationTest;
|
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.ConnectingNodeInformationTest;
|
||||||
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.RoutingIslandsTest;
|
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.RoutingIslandsTest;
|
||||||
|
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressOrder;
|
||||||
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressTest;
|
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StreetAddressTest;
|
||||||
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StubEndsTest;
|
import org.openstreetmap.josm.plugins.mapwithai.data.validation.tests.StubEndsTest;
|
||||||
import org.openstreetmap.josm.plugins.mapwithai.frontend.MapWithAIDownloadReader;
|
import org.openstreetmap.josm.plugins.mapwithai.frontend.MapWithAIDownloadReader;
|
||||||
|
@ -66,7 +67,7 @@ public final class MapWithAIPlugin extends Plugin implements Destroyable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static List<Class<? extends Test>> VALIDATORS = Arrays.asList(RoutingIslandsTest.class,
|
private final static List<Class<? extends Test>> VALIDATORS = Arrays.asList(RoutingIslandsTest.class,
|
||||||
ConnectingNodeInformationTest.class, StubEndsTest.class, StreetAddressTest.class);
|
ConnectingNodeInformationTest.class, StubEndsTest.class, StreetAddressTest.class, StreetAddressOrder.class);
|
||||||
|
|
||||||
public MapWithAIPlugin(PluginInformation info) {
|
public MapWithAIPlugin(PluginInformation info) {
|
||||||
super(info);
|
super(info);
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
package org.openstreetmap.josm.plugins.mapwithai.data.validation.tests;
|
||||||
|
|
||||||
|
import static org.openstreetmap.josm.tools.I18n.marktr;
|
||||||
|
import static org.openstreetmap.josm.tools.I18n.tr;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.openstreetmap.josm.data.osm.IPrimitive;
|
||||||
|
import org.openstreetmap.josm.data.osm.IWay;
|
||||||
|
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.RelationMember;
|
||||||
|
import org.openstreetmap.josm.data.osm.Way;
|
||||||
|
import org.openstreetmap.josm.data.osm.WaySegment;
|
||||||
|
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.data.validation.tests.SharpAngles;
|
||||||
|
import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
|
||||||
|
import org.openstreetmap.josm.plugins.mapwithai.MapWithAIPlugin;
|
||||||
|
import org.openstreetmap.josm.tools.Geometry;
|
||||||
|
import org.openstreetmap.josm.tools.Logging;
|
||||||
|
|
||||||
|
public class StreetAddressOrder extends Test {
|
||||||
|
private static final SharpAngles ANGLES_TEST = new SharpAngles();
|
||||||
|
|
||||||
|
public StreetAddressOrder() {
|
||||||
|
super(tr("Address order ({0})", MapWithAIPlugin.NAME), tr("Check that street address order makes sense"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Way way) {
|
||||||
|
if (way.isUsable() && way.hasTag("highway", StreetAddressTest.CLASSIFIED_HIGHWAYS) && way.hasTag("name")) {
|
||||||
|
String name = way.get("name");
|
||||||
|
List<IPrimitive> addresses = StreetAddressTest.getNearbyAddresses(way).stream().filter(Objects::nonNull)
|
||||||
|
.filter(w -> w.hasTag("addr:housenumber")).filter(w -> name.equals(w.get("addr:street")))
|
||||||
|
.sorted(Comparator.comparing(p -> convertAddrHouseNumberToDouble(p.get("addr:housenumber"))))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<IPrimitive> leftAddresses = getAddressesInDirection(true, addresses, way);
|
||||||
|
List<IPrimitive> rightAddresses = getAddressesInDirection(false, addresses, way);
|
||||||
|
List<IPrimitive> potentialBadAddresses = new ArrayList<>(checkOrdering(leftAddresses));
|
||||||
|
potentialBadAddresses.addAll(checkOrdering(rightAddresses));
|
||||||
|
potentialBadAddresses.forEach(this::createError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a housenumber (addr:housenumber) to a double
|
||||||
|
*
|
||||||
|
* @param housenumber The housenumber to convert
|
||||||
|
* @return The double representation, or {@link Double#NaN} if not convertible.
|
||||||
|
*/
|
||||||
|
public static double convertAddrHouseNumberToDouble(String housenumber) {
|
||||||
|
String[] parts = housenumber.split(" ");
|
||||||
|
double number = 0;
|
||||||
|
for (String part : parts) {
|
||||||
|
try {
|
||||||
|
if (part.contains("/")) {
|
||||||
|
String[] fractional = part.split("/");
|
||||||
|
double tmp = Double.parseDouble(fractional[0]);
|
||||||
|
for (int i = 1; i < fractional.length; i++) {
|
||||||
|
tmp = tmp / Double.parseDouble(fractional[i]);
|
||||||
|
}
|
||||||
|
number += tmp;
|
||||||
|
} else {
|
||||||
|
number += Double.parseDouble(part);
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Logging.debug("{0} found a malformed number {1}", MapWithAIPlugin.NAME, part);
|
||||||
|
Logging.debug(e);
|
||||||
|
number = Double.NaN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createError(IPrimitive potentialBadAddress) {
|
||||||
|
if (potentialBadAddress instanceof OsmPrimitive) {
|
||||||
|
errors.add(TestError.builder(this, Severity.OTHER, 58542100).primitives((OsmPrimitive) potentialBadAddress)
|
||||||
|
.message(MapWithAIPlugin.NAME, marktr("Potential bad address")).build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the ordering of primitives by creating nodes at their centroids and
|
||||||
|
* checking to see if a sharp angle is created.
|
||||||
|
*
|
||||||
|
* @param primitives The primitives to check the order of
|
||||||
|
* @return Primitives that are out of order
|
||||||
|
* @see SharpAngles
|
||||||
|
*/
|
||||||
|
public static <T extends IPrimitive> List<T> checkOrdering(List<T> primitives) {
|
||||||
|
if (primitives.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<Node> centroids = primitives.stream().map(StreetAddressOrder::getCentroid).filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Way way = new Way();
|
||||||
|
way.setNodes(centroids);
|
||||||
|
/*
|
||||||
|
* if (primitives.get(0) instanceof OsmPrimitive) { DataSet ds = ((OsmPrimitive)
|
||||||
|
* primitives.get(0)).getDataSet(); // TODO remove after finishing debug
|
||||||
|
* way.getNodes().stream().filter(p -> p.getDataSet() == null &&
|
||||||
|
* !p.isDeleted()).forEach(ds::addPrimitive); ds.addPrimitive(way); }
|
||||||
|
*/
|
||||||
|
double maxDistance = 100;
|
||||||
|
Node previousCentroid = centroids.get(0);
|
||||||
|
for (Node centroid : centroids) {
|
||||||
|
if (previousCentroid.equals(centroid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double tDistance = Geometry.getDistance(centroid, previousCentroid);
|
||||||
|
previousCentroid = centroid;
|
||||||
|
if (tDistance > maxDistance) {
|
||||||
|
maxDistance = tDistance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
way.put("highway", "residential"); // Required for the SharpAngles test
|
||||||
|
ANGLES_TEST.startTest(NullProgressMonitor.INSTANCE);
|
||||||
|
ANGLES_TEST.setMaxLength(maxDistance);
|
||||||
|
ANGLES_TEST.visit(way);
|
||||||
|
ANGLES_TEST.endTest();
|
||||||
|
List<Node> issueCentroids = ANGLES_TEST.getErrors().stream().flatMap(e -> e.getHighlighted().stream())
|
||||||
|
.filter(Node.class::isInstance).map(Node.class::cast).collect(Collectors.toList());
|
||||||
|
ANGLES_TEST.clear();
|
||||||
|
way.setNodes(Collections.emptyList());
|
||||||
|
return issueCentroids.stream().map(centroids::indexOf).map(primitives::get).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get addresses on different sides of the road
|
||||||
|
*
|
||||||
|
* @param left If {@code true}, get addresses on the "left" side of the
|
||||||
|
* road
|
||||||
|
* @param addresses Addresses to filter for the side on the road
|
||||||
|
* @param way The road way
|
||||||
|
* @return Addresses on the appropriate side of the road
|
||||||
|
*/
|
||||||
|
public static <T extends IPrimitive> List<T> getAddressesInDirection(boolean left, Collection<T> addresses,
|
||||||
|
IWay<?> way) {
|
||||||
|
List<T> addressesToReturn = new ArrayList<>();
|
||||||
|
for (T address : addresses) {
|
||||||
|
if (address instanceof OsmPrimitive && way instanceof Way) {
|
||||||
|
Node centroid = getCentroid(address);
|
||||||
|
WaySegment seg = Geometry.getClosestWaySegment((Way) way, (OsmPrimitive) address);
|
||||||
|
boolean right = Geometry.angleIsClockwise(seg.getFirstNode(), seg.getSecondNode(), centroid);
|
||||||
|
if (left != right) {
|
||||||
|
addressesToReturn.add(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addressesToReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the centroid of a primitive
|
||||||
|
*
|
||||||
|
* @param primitive The primitive to get a centroid for
|
||||||
|
* @return The node that represents the centroid, or {@code null} if no centroid
|
||||||
|
* can be determined
|
||||||
|
*/
|
||||||
|
public static Node getCentroid(IPrimitive primitive) {
|
||||||
|
if (primitive instanceof Node) {
|
||||||
|
return (Node) primitive;
|
||||||
|
} else if (primitive instanceof Way) {
|
||||||
|
return new Node(Geometry.getCentroid(((Way) primitive).getNodes()));
|
||||||
|
} else if (primitive instanceof Relation && "multipolygon".equals(((Relation) primitive).get("type"))) {
|
||||||
|
// This is not perfect by any stretch of the imagination
|
||||||
|
List<Node> nodes = new ArrayList<>();
|
||||||
|
for (RelationMember member : ((Relation) primitive).getMembers()) {
|
||||||
|
if (member.hasRole("outer")) {
|
||||||
|
nodes.add(getCentroid(member.getMember()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!nodes.isEmpty()) {
|
||||||
|
return new Node(Geometry.getCentroid(nodes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,160 @@
|
||||||
|
package org.openstreetmap.josm.plugins.mapwithai.data.validation.tests;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.openstreetmap.josm.TestUtils;
|
||||||
|
import org.openstreetmap.josm.data.coor.EastNorth;
|
||||||
|
import org.openstreetmap.josm.data.coor.LatLon;
|
||||||
|
import org.openstreetmap.josm.data.osm.DataSet;
|
||||||
|
import org.openstreetmap.josm.data.osm.IPrimitive;
|
||||||
|
import org.openstreetmap.josm.data.osm.Node;
|
||||||
|
import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
||||||
|
import org.openstreetmap.josm.data.osm.PrimitiveData;
|
||||||
|
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.testutils.JOSMTestRules;
|
||||||
|
import org.openstreetmap.josm.tools.Geometry;
|
||||||
|
|
||||||
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
|
|
||||||
|
public class StreetAddressOrderTest {
|
||||||
|
@Rule
|
||||||
|
@SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
|
||||||
|
public JOSMTestRules test = new JOSMTestRules().projection();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testVisitWay() {
|
||||||
|
Way way = TestUtils.newWay("", new Node(new LatLon(0, 0)), new Node(new LatLon(1, 1)));
|
||||||
|
DataSet ds = new DataSet();
|
||||||
|
way.getNodes().forEach(ds::addPrimitive);
|
||||||
|
ds.addPrimitive(way);
|
||||||
|
Node tNode = new Node(new LatLon(0, 0.00001));
|
||||||
|
ds.addPrimitive(tNode);
|
||||||
|
tNode = new Node(tNode, true);
|
||||||
|
tNode.put("addr:street", "Test Road");
|
||||||
|
ds.addPrimitive(tNode);
|
||||||
|
|
||||||
|
tNode = new Node(tNode, true);
|
||||||
|
tNode.setCoor(new LatLon(0.00001, 0.00001));
|
||||||
|
tNode.put("addr:housenumber", "1");
|
||||||
|
ds.addPrimitive(tNode);
|
||||||
|
|
||||||
|
StreetAddressOrder test = new StreetAddressOrder();
|
||||||
|
test.visit(way);
|
||||||
|
assertTrue(test.getErrors().isEmpty());
|
||||||
|
way.put("highway", "residential");
|
||||||
|
|
||||||
|
test.visit(way);
|
||||||
|
assertTrue(test.getErrors().isEmpty());
|
||||||
|
|
||||||
|
way.put("name", "Test Road");
|
||||||
|
test.visit(way);
|
||||||
|
assertTrue(test.getErrors().isEmpty());
|
||||||
|
|
||||||
|
tNode = new Node(tNode, true);
|
||||||
|
tNode.setCoor(new LatLon(0.00002, 0.00002));
|
||||||
|
tNode.put("addr:housenumber", "2");
|
||||||
|
ds.addPrimitive(tNode);
|
||||||
|
test.visit(way);
|
||||||
|
assertTrue(test.getErrors().isEmpty());
|
||||||
|
|
||||||
|
tNode = new Node(tNode, true);
|
||||||
|
tNode.setCoor(new LatLon(0.000015, 0.000015));
|
||||||
|
tNode.put("addr:housenumber", "20");
|
||||||
|
ds.addPrimitive(tNode);
|
||||||
|
test.visit(way);
|
||||||
|
assertEquals(1, test.getErrors().size());
|
||||||
|
|
||||||
|
test.clear();
|
||||||
|
way.setDeleted(true);
|
||||||
|
test.visit(way);
|
||||||
|
assertTrue(test.getErrors().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateError() {
|
||||||
|
StreetAddressOrder test = new StreetAddressOrder();
|
||||||
|
test.createError(new Node(new LatLon(0, 0)).save());
|
||||||
|
assertTrue(test.getErrors().isEmpty());
|
||||||
|
|
||||||
|
test.createError(new Node(new LatLon(0, 0)));
|
||||||
|
assertEquals(1, test.getErrors().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConvertAddrHouseNumberToDouble() {
|
||||||
|
assertEquals(24.5, StreetAddressOrder.convertAddrHouseNumberToDouble("24 1/2"));
|
||||||
|
assertEquals(24.5, StreetAddressOrder.convertAddrHouseNumberToDouble("24.5"));
|
||||||
|
assertEquals(24, StreetAddressOrder.convertAddrHouseNumberToDouble("24"));
|
||||||
|
|
||||||
|
assertEquals(25.5, StreetAddressOrder.convertAddrHouseNumberToDouble("24 3/2"));
|
||||||
|
assertTrue(Double.isNaN(StreetAddressOrder.convertAddrHouseNumberToDouble("Not a number")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCheckOrdering() {
|
||||||
|
List<IPrimitive> primitives = new ArrayList<>();
|
||||||
|
primitives.add(new Node(new LatLon(0, 0)));
|
||||||
|
primitives.add(new Node(new LatLon(1, 1)));
|
||||||
|
primitives.add(new Node(new LatLon(2, 2)));
|
||||||
|
assertTrue(StreetAddressOrder.checkOrdering(primitives).isEmpty());
|
||||||
|
primitives.add(primitives.remove(1));
|
||||||
|
assertFalse(StreetAddressOrder.checkOrdering(primitives).isEmpty());
|
||||||
|
|
||||||
|
assertTrue(StreetAddressOrder.checkOrdering(Collections.emptyList()).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetAddressesInDirection() {
|
||||||
|
Way way1 = TestUtils.newWay("", new Node(new LatLon(-1, -1)), new Node(new LatLon(1, 1)));
|
||||||
|
|
||||||
|
List<OsmPrimitive> addresses = new ArrayList<>();
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(true, addresses, way1).isEmpty());
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(false, addresses, way1).isEmpty());
|
||||||
|
addresses.add(new Node(new LatLon(1, 0)));
|
||||||
|
assertSame(addresses.get(0), StreetAddressOrder.getAddressesInDirection(true, addresses, way1).get(0));
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(false, addresses, way1).isEmpty());
|
||||||
|
((Node) addresses.get(0)).setCoor(new LatLon(0, 1));
|
||||||
|
assertSame(addresses.get(0), StreetAddressOrder.getAddressesInDirection(false, addresses, way1).get(0));
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(true, addresses, way1).isEmpty());
|
||||||
|
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(true, addresses, way1.save()).isEmpty());
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(false, addresses, way1.save()).isEmpty());
|
||||||
|
|
||||||
|
List<PrimitiveData> primitiveData = addresses.stream().map(OsmPrimitive::save).collect(Collectors.toList());
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(true, primitiveData, way1).isEmpty());
|
||||||
|
assertTrue(StreetAddressOrder.getAddressesInDirection(false, primitiveData, way1).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetCentroid() {
|
||||||
|
Node node1 = new Node(new LatLon(0, 0));
|
||||||
|
assertSame(node1, StreetAddressOrder.getCentroid(node1));
|
||||||
|
assertNull(StreetAddressOrder.getCentroid(node1.save()));
|
||||||
|
|
||||||
|
Way way1 = TestUtils.newWay("", node1, new Node(new LatLon(1, 1)), new Node(new LatLon(0, 1)), node1);
|
||||||
|
EastNorth way1Centroid = Geometry.getCentroid(way1.getNodes());
|
||||||
|
assertEquals(way1Centroid, StreetAddressOrder.getCentroid(way1).getEastNorth());
|
||||||
|
|
||||||
|
Relation relation1 = TestUtils.newRelation("", new RelationMember("", way1));
|
||||||
|
assertNull(StreetAddressOrder.getCentroid(relation1));
|
||||||
|
relation1.put("type", "multipolygon");
|
||||||
|
assertNull(StreetAddressOrder.getCentroid(relation1));
|
||||||
|
relation1.removeMember(0);
|
||||||
|
relation1.addMember(new RelationMember("outer", way1));
|
||||||
|
assertEquals(way1Centroid, StreetAddressOrder.getCentroid(relation1).getEastNorth());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Ładowanie…
Reference in New Issue