Skip to content

Commit

Permalink
Change behavior of IsInsideAreaByCode function (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickackermann authored Jun 20, 2024
2 parents 5c60b6f + 70cc737 commit 1c92390
Show file tree
Hide file tree
Showing 8 changed files with 616 additions and 87 deletions.
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ dependencies {
implementation group: 'ch.interlis', name: 'ili2c-core', version: '5.4.0'
implementation group: 'com.vividsolutions', name: 'jts-core', version: '1.14.0'

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
}

java{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

public final class IsInsideAreaByCodeIoxPlugin extends BaseInterlisFunction {
Expand Down Expand Up @@ -71,17 +67,16 @@ private Value isInsideArea(String usageScope, Collection<IomObject> objects, Pat
Geometry::union
));

List<Map.Entry<ValueKey, Geometry>> sortedGeometries;
for (Map.Entry<ValueKey, Geometry> entry : geometriesByCodeValue.entrySet()) {
entry.getValue().setUserData(entry.getKey().getStringValue());
}

List<Geometry> sortedGeometries;
ValueKey firstKey = geometriesByCodeValue.keySet().iterator().next();
Type keyType = firstKey.getType();

if (keyType instanceof EnumerationType) {
EnumerationType enumType = (EnumerationType) keyType;
if (!enumType.isOrdered()) {
logger.addEvent(logger.logErrorMsg("{0}: Enumeration type must be ordered.", usageScope));
return Value.createSkipEvaluation();
}
sortedGeometries = sortByEnumValues(geometriesByCodeValue, enumType);
sortedGeometries = prepareGeometries(geometriesByCodeValue, this::extractCodeIntKey);
} else if (keyType instanceof NumericType) {
sortedGeometries = sortByNumericValues(geometriesByCodeValue);
} else {
Expand All @@ -91,24 +86,21 @@ private Value isInsideArea(String usageScope, Collection<IomObject> objects, Pat

boolean result = true;
for (int i = 0; i < sortedGeometries.size() - 1; i++) {
Map.Entry<ValueKey, Geometry> current = sortedGeometries.get(i);
Map.Entry<ValueKey, Geometry> next = sortedGeometries.get(i + 1);
Geometry current = sortedGeometries.get(i);
Geometry next = sortedGeometries.get(i + 1);

if (!next.getValue().contains(current.getValue())) {
Geometry offendingGeometry = current.getValue().difference(next.getValue());
if (!next.contains(current)) {
Geometry offendingGeometry = current.difference(next);
Point centroid = offendingGeometry.getCentroid();
String offendingCentroidWkt = centroid.toText();

String currentCode = current.getKey().getStringValue();
String nextCode = next.getKey().getStringValue();

logger.addEvent(logger.logErrorMsg(
"IsInsideAreaByCode found an invalid overlap between code '{0}' and '{1}'. The offending geometry has it's centroid at point: {2}",
centroid.getX(),
centroid.getY(),
null,
currentCode,
nextCode,
current.getUserData().toString(),
next.getUserData().toString(),
offendingCentroidWkt));

result = false;
Expand All @@ -118,19 +110,44 @@ private Value isInsideArea(String usageScope, Collection<IomObject> objects, Pat
return new Value(result);
}

private List<Map.Entry<ValueKey, Geometry>> sortByEnumValues(Map<ValueKey, Geometry> map, EnumerationType enumType) {
List<String> enumValues = enumType.getValues();
private List<Geometry> prepareGeometries(Map<ValueKey, Geometry> map, Function<ValueKey, Integer> keySortOrder) {
Map<Integer, Geometry> combinedBySortOrderKey = map.entrySet()
.stream()
.collect(Collectors.toMap(
e -> keySortOrder.apply(e.getKey()),
Map.Entry::getValue,
(a, b) -> {
Geometry geometry = a.union(b);
geometry.setUserData(a.getUserData() + ", " + b.getUserData());
return geometry;
}
));

return map.entrySet()
return combinedBySortOrderKey.entrySet()
.stream()
.sorted(Comparator.comparingInt(entry -> enumValues.indexOf(entry.getKey().getStringValue())))
.sorted(Comparator.comparingInt(Map.Entry::getKey))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}

private List<Map.Entry<ValueKey, Geometry>> sortByNumericValues(Map<ValueKey, Geometry> map) {
private int extractCodeIntKey(ValueKey key) {
try {
return Integer.parseInt(key.getStringValue().substring(key.getStringValue().lastIndexOf("_") + 1));
} catch (NumberFormatException e) {
return Integer.MAX_VALUE;
}
}

private int getOrderedEnumIndex(ValueKey key) {
List<String> enumValues = ((EnumerationType) key.getType()).getValues();
return enumValues.indexOf(key.getStringValue());
}

private List<Geometry> sortByNumericValues(Map<ValueKey, Geometry> map) {
return map.entrySet()
.stream()
.sorted(Comparator.comparingDouble(entry -> entry.getKey().getNumericValue()))
.map(Map.Entry::getValue)
.collect(Collectors.toList());
}

Expand Down
2 changes: 1 addition & 1 deletion src/model/NGK_SO_FunctionsExt.ili
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
INTERLIS 2.4;
MODEL NGK_SO_FunctionsExt
AT "http://geo.so.ch/models/AFU" VERSION "2024-02-26" =
!!@ fn.description = "Prüft bei der Objektmenge, dass die gemäss dem geordneten Enum oder aufsteigend sortierten numerischen Wert jeweils kleineren Flächen innerhalb der grösseren Flächen liegen. Die Sortierung des Enums muss von der kleinsten zur grössten Fläche erfolgen (mittels ORDERED).";
!!@ fn.description = "Prüft ob die nach der Enumeration oder numerischen Attribut zusammengefassten Flächen eine Pyramide bilden, bei der die jeweils kleinere Fläche die grössere Fläche nicht überragt. Ist CodeAttr eine Enumeration wird beim Enumeration-Namen die Jährlichkeit extrahiert und dieser für die Sortierung verwendet. Es wird dann überprüft, dass die Geometrie mit dem kleineren Wert keine Geometrie mit einem grösseren Werte überragt.";
!!@ fn.param = "Objects: Zu prüfende Objektmenge.";
!!@ fn.param = "GeometryAttr: Pfad zur Geometrie.";
!!@ fn.param = "CodeAttr: Pfad zum Enum oder numerischen Attribut";
Expand Down
22 changes: 22 additions & 0 deletions src/test/data/IsInsideAreaByCode/SetConstraints.ili
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,28 @@ MODEL TestSuite
code_3,
code_4
) ORDERED;

CodeNumeric = 0 .. 999;

UnorderedCodeEnum = (
code_10,
code_blue_20,
code_magenta_20,
code_30,
code_40,
code_noNumber15,
code_without_number,
code_
);

!!@CRS=EPSG:2056
CHKoord = COORD 2460000.000 .. 2870000.000 [INTERLIS.m],
1045000.000 .. 1310000.000 [INTERLIS.m],
ROTATION 2 -> 1;

Coord2 = COORD 0.0 .. 100.0,
0.0 .. 100.0;

CLASS BaseClass =
codeEnum : CodeEnum;
codeNumeric : CodeNumeric;
Expand All @@ -30,6 +45,13 @@ MODEL TestSuite
SET CONSTRAINT insideAreaConstraintNumeric: NGK_SO_FunctionsExt.IsInsideAreaByCode(ALL, "surface", "codeNumeric");
END BaseClass;

CLASS TestClass =
code : UnorderedCodeEnum;
surface : SURFACE WITH (STRAIGHTS, ARCS) VERTEX Coord2 WITHOUT OVERLAPS > 0.01;

SET CONSTRAINT insideAreaConstraint: NGK_SO_FunctionsExt.IsInsideAreaByCode(ALL, "surface", "code");
END TestClass;

END FunctionTestTopic;

END TestSuite.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.List;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.fail;

public final class AssertionHelper {
Expand All @@ -14,9 +15,9 @@ private AssertionHelper() {
// Utility class
}

public static void assertConstraintErrors(ValidationTestHelper vh, int expectedCount, String oid, String constraintName) {
public static void assertConstraintErrors(LogCollector logger, int expectedCount, String oid, String constraintName) {
int errorsFound = 0;
for (IoxLogEvent err : vh.getErrs()) {
for (IoxLogEvent err : logger.getErrs()) {
if (oid.equals(err.getSourceObjectXtfId()) && err.getEventMsg().contains(String.format(".%s ", constraintName))) {
errorsFound++;
}
Expand All @@ -26,13 +27,13 @@ public static void assertConstraintErrors(ValidationTestHelper vh, int expectedC
String.format("Expected %d but found %d errors with OID <%s> and Source <%s>.", expectedCount, errorsFound, oid, constraintName));
}

public static void assertSingleConstraintError(ValidationTestHelper vh, int oid, String constraintName) {
assertConstraintErrors(vh, 1, Integer.toString(oid), constraintName);
public static void assertSingleConstraintError(LogCollector logger, int oid, String constraintName) {
assertConstraintErrors(logger, 1, Integer.toString(oid), constraintName);
}

public static void assertConstraintErrors(ValidationTestHelper vh, int expectedCount, String constraintName) {
public static void assertConstraintErrors(LogCollector logger, int expectedCount, String constraintName) {
int errorsFound = 0;
for (IoxLogEvent err : vh.getErrs()) {
for (IoxLogEvent err : logger.getErrs()) {
if (err.getEventMsg().contains(String.format(".%s ", constraintName))) {
errorsFound++;
}
Expand All @@ -42,9 +43,9 @@ public static void assertConstraintErrors(ValidationTestHelper vh, int expectedC
String.format("Expected %s errors with Source <%s> but found %d.", expectedCount, constraintName, errorsFound));
}

public static void assertNoConstraintError(ValidationTestHelper vh, String constraintName) {
public static void assertNoConstraintError(LogCollector logger, String constraintName) {
int errorsFound = 0;
for (IoxLogEvent err : vh.getErrs()) {
for (IoxLogEvent err : logger.getErrs()) {
if (err.getEventMsg().contains(String.format(".%s ", constraintName))) {
errorsFound++;
}
Expand Down Expand Up @@ -75,4 +76,13 @@ public static void assertLogEventsMessages(List<IoxLogEvent> logs, String expect
fail(String.format("Expected %d messages to match the regex <%s> but found %d.", expectedMatchCount, expectedMessageRegex, actualMatchCount));
}
}

public static void assertEventMessagesAreEqual(List<IoxLogEvent> events, String... expectedMessages) {
String[] actualMessages = new String[events.size()];
for (int i = 0; i < events.size(); i++) {
actualMessages[i] = events.get(i).getEventMsg();
}

assertArrayEquals(expectedMessages, actualMessages);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package ch.geowerkstatt.ilivalidator.extensions.functions.ngk;

import ch.interlis.iom.IomObject;
import ch.interlis.iom_j.Iom_jObject;

public final class IomObjectHelper {

private IomObjectHelper() {
// Utility class
}

/**
* Create an IomObject containing a rectangle surface geometry with the specified corner coordinates.
*/
public static IomObject createRectangleGeometry(String x1, String y1, String x2, String y2) {
return createPolygonFromBoundaries(createRectangleBoundary(x1, y1, x2, y2));
}

public static IomObject createRectangleBoundary(String x1, String y1, String x2, String y2) {
return createBoundary(
createCoord(x1, y1),
createCoord(x1, y2),
createCoord(x2, y2),
createCoord(x2, y1),
createCoord(x1, y1));
}

public static IomObject createPolygonFromBoundaries(IomObject... boundary) {
IomObject surfaceValue = new Iom_jObject("SURFACE", null);
for (IomObject b : boundary) {
surfaceValue.addattrobj("boundary", b);
}

IomObject multisurface = new Iom_jObject("MULTISURFACE", null);
multisurface.addattrobj("surface", surfaceValue);

return multisurface;
}

/**
* Create a BOUNDARY object consisting of one POLYLINE object with the specified segments.
*/
public static IomObject createBoundary(IomObject... segments) {
IomObject polyline = createPolyline(segments);

IomObject boundary = new Iom_jObject("BOUNDARY", null);
boundary.addattrobj("polyline", polyline);
return boundary;
}

/**
* Create a MULTIPOLYLINE object from the specified polylines.
*/
public static IomObject createMultiPolyline(IomObject... polylines) {
IomObject multiPolyline = new Iom_jObject("MULTIPOLYLINE", null);
for (IomObject polyline : polylines) {
multiPolyline.addattrobj("polyline", polyline);
}

return multiPolyline;
}

/**
* Create a POLYLINE object with the specified segments.
*/
public static IomObject createPolyline(IomObject... segments) {
IomObject polylineSegments = new Iom_jObject("SEGMENTS", null);
for (IomObject segment : segments) {
polylineSegments.addattrobj("segment", segment);
}

IomObject polyline = new Iom_jObject("POLYLINE", null);
polyline.addattrobj("sequence", polylineSegments);

return polyline;
}

/**
* Create a BOUNDARY object consisting of POLYLINE object for each segment.
*/
public static IomObject createMultiplePolylineBoundary(IomObject... segments) {
IomObject boundary = new Iom_jObject("BOUNDARY", null);

for (int i = 1; i < segments.length; i++) {
IomObject lineSegment = new Iom_jObject("SEGMENTS", null);

if (segments[i - 1].getattrvalue("C3") == null) {
lineSegment.addattrobj("segment", createCoord(
segments[i - 1].getattrvalue("C1"),
segments[i - 1].getattrvalue("C2")));
} else {
lineSegment.addattrobj("segment", createCoord(
segments[i - 1].getattrvalue("C1"),
segments[i - 1].getattrvalue("C2"),
segments[i - 1].getattrvalue("C3")));
}
lineSegment.addattrobj("segment", segments[i]);

IomObject polyline = new Iom_jObject("POLYLINE", null);
polyline.addattrobj("sequence", lineSegment);

boundary.addattrobj("polyline", polyline);
}

return boundary;
}

public static IomObject createCoord(String c1, String c2) {
IomObject coord = new Iom_jObject("COORD", null);
coord.setattrvalue("C1", c1);
coord.setattrvalue("C2", c2);
return coord;
}

public static IomObject createCoord(String c1, String c2, String c3) {
IomObject coord = createCoord(c1, c2);
coord.setattrvalue("C3", c3);
return coord;
}

public static IomObject createArc(String a1, String a2, String c1, String c2) {
IomObject arc = new Iom_jObject("ARC", null);
arc.setattrvalue("A1", a1);
arc.setattrvalue("A2", a2);
arc.setattrvalue("C1", c1);
arc.setattrvalue("C2", c2);
return arc;
}

public static IomObject createArc(String a1, String a2, String c1, String c2, String c3) {
IomObject arc = createArc(a1, a2, c1, c2);
arc.setattrvalue("C3", c3);
return arc;
}
}
Loading

0 comments on commit 1c92390

Please sign in to comment.