Skip to content

Commit

Permalink
Change behavior of IsInsideAreaByCode function (#10)
Browse files Browse the repository at this point in the history
patrickackermann authored Jun 20, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2 parents 5c60b6f + 70cc737 commit 1c92390
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
@@ -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{
Original file line number Diff line number Diff line change
@@ -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 {
@@ -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 {
@@ -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;
@@ -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());
}

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";
22 changes: 22 additions & 0 deletions src/test/data/IsInsideAreaByCode/SetConstraints.ili
Original file line number Diff line number Diff line change
@@ -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;
@@ -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
@@ -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 {
@@ -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++;
}
@@ -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++;
}
@@ -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++;
}
@@ -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;
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
package ch.geowerkstatt.ilivalidator.extensions.functions.ngk;

import ch.ehi.basics.settings.Settings;
import ch.interlis.ili2c.Ili2c;
import ch.interlis.ili2c.Ili2cFailure;
import ch.interlis.ili2c.metamodel.TransferDescription;
import ch.interlis.iom.IomObject;
import ch.interlis.iox.EndTransferEvent;
import ch.interlis.iox.IoxEvent;
import ch.interlis.iox.IoxException;
import ch.interlis.iox.IoxLogEvent;
import ch.interlis.iox.IoxReader;
import ch.interlis.iox_j.EndBasketEvent;
import ch.interlis.iox_j.IoxIliReader;
import ch.interlis.iox_j.ObjectEvent;
import ch.interlis.iox_j.PipelinePool;
import ch.interlis.iox_j.StartBasketEvent;
import ch.interlis.iox_j.StartTransferEvent;
import ch.interlis.iox_j.logging.LogEventFactory;
import ch.interlis.iox_j.utility.ReaderFactory;
import ch.interlis.iox_j.validator.InterlisFunction;
@@ -22,29 +27,33 @@
import java.util.HashMap;

public final class ValidationTestHelper {
private static final String FUNCTIONS_EXT_ILI_PATH = "src/model/NGK_SO_FunctionsExt.ili";
private static final String FUNCTIONS_EXT_23_ILI_PATH = "src/model/NGK_SO_FunctionsExt_23.ili";

private final HashMap<String, Class<InterlisFunction>> userFunctions = new HashMap<>();
private LogCollector logCollector;
private final HashMap<String, Class<? extends InterlisFunction>> userFunctions = new HashMap<>();

public void runValidation(String[] dataFiles, String[] modelFiles) throws IoxException, Ili2cFailure {
public ValidationTestHelper(InterlisFunction... userFunctions) {
for (InterlisFunction function : userFunctions) {
this.userFunctions.put(function.getQualifiedIliName(), function.getClass());
}
}

public LogCollector runValidation(String[] dataFiles, String[] modelFiles) throws IoxException, Ili2cFailure {
dataFiles = addLeadingTestDataDirectory(dataFiles);
modelFiles = addLeadingTestDataDirectory(modelFiles);
modelFiles = appendFunctionsExtIli(modelFiles);
modelFiles = prependFunctionsExtIli(modelFiles);

logCollector = new LogCollector();
LogEventFactory errFactory = new LogEventFactory();
errFactory.setLogger(logCollector);
TransferDescription td = Ili2c.compileIliFiles(new ArrayList<>(Arrays.asList(modelFiles)), new ArrayList<String>());

LogCollector logger = new LogCollector();
LogEventFactory errFactory = new LogEventFactory();
PipelinePool pool = new PipelinePool();
Settings settings = new Settings();
settings.setTransientObject(ch.interlis.iox_j.validator.Validator.CONFIG_CUSTOM_FUNCTIONS, userFunctions);

TransferDescription td = ch.interlis.ili2c.Ili2c.compileIliFiles(new ArrayList<>(Arrays.asList(modelFiles)), new ArrayList<String>());

ValidationConfig modelConfig = new ValidationConfig();
modelConfig.mergeIliMetaAttrs(td);

PipelinePool pool = new PipelinePool();
Validator validator = new ch.interlis.iox_j.validator.Validator(td, modelConfig, logCollector, errFactory, pool, settings);
settings.setTransientObject(ch.interlis.iox_j.validator.Validator.CONFIG_CUSTOM_FUNCTIONS, userFunctions);
modelConfig.mergeIliMetaAttrs(td);
Validator validator = new Validator(td, modelConfig, logger, errFactory, pool, settings);

for (String filename : dataFiles) {
IoxReader ioxReader = new ReaderFactory().createReader(new java.io.File(filename), errFactory, settings);
@@ -64,34 +73,49 @@ public void runValidation(String[] dataFiles, String[] modelFiles) throws IoxExc
}
}
}

return logger;
}

private String[] appendFunctionsExtIli(String[] modelDirs) {
System.out.println("Working Directory = " + System.getProperty("user.dir"));
String functionsExtIliPath = "src/model/NGK_SO_FunctionsExt.ili";
ArrayList<String> result = new ArrayList<>();
result.add(functionsExtIliPath);
result.addAll(Arrays.asList(modelDirs));
return result.toArray(new String[0]);
public LogCollector runValidation(String[] modelFiles, String topic, IomObject... objects) throws Ili2cFailure {
modelFiles = addLeadingTestDataDirectory(modelFiles);
modelFiles = prependFunctionsExtIli(modelFiles);
TransferDescription td = Ili2c.compileIliFiles(new ArrayList<>(Arrays.asList(modelFiles)), new ArrayList<String>());

LogCollector logger = new LogCollector();
LogEventFactory errFactory = new LogEventFactory();
PipelinePool pool = new PipelinePool();
Settings settings = new Settings();
ValidationConfig modelConfig = new ValidationConfig();

settings.setTransientObject(ch.interlis.iox_j.validator.Validator.CONFIG_CUSTOM_FUNCTIONS, userFunctions);
modelConfig.mergeIliMetaAttrs(td);
Validator validator = new Validator(td, modelConfig, logger, errFactory, pool, settings);

validator.validate(new StartTransferEvent());
validator.validate(new StartBasketEvent(topic, "b1"));
for (IomObject object : objects) {
validator.validate(new ObjectEvent(object));
}
validator.validate(new EndBasketEvent());
validator.validate(new ch.interlis.iox_j.EndTransferEvent());
return logger;
}

@SuppressWarnings("unchecked")
public void addFunction(InterlisFunction function) {
userFunctions.put(function.getQualifiedIliName(), (Class<InterlisFunction>) function.getClass());
private String[] prependFunctionsExtIli(String[] modelDirs) {
System.out.println("Working Directory = " + System.getProperty("user.dir"));

String[] result = new String[modelDirs.length + 1];
result[0] = FUNCTIONS_EXT_ILI_PATH;
System.arraycopy(modelDirs, 0, result, 1, modelDirs.length);

return result;
}

public String[] addLeadingTestDataDirectory(String[] files) {
private String[] addLeadingTestDataDirectory(String[] files) {
return Arrays
.stream(files).map(file -> Paths.get("src/test/data", file).toString())
.distinct()
.toArray(String[]::new);
}

public ArrayList<IoxLogEvent> getErrs() {
return logCollector.getErrs();
}

public ArrayList<IoxLogEvent> getWarn() {
return logCollector.getWarn();
}
}

0 comments on commit 1c92390

Please sign in to comment.