diff --git a/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/PolylinesOverlapIoxPlugin.java b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/PolylinesOverlapIoxPlugin.java new file mode 100644 index 0000000..9d7f2e0 --- /dev/null +++ b/src/main/java/ch/geowerkstatt/ilivalidator/extensions/functions/PolylinesOverlapIoxPlugin.java @@ -0,0 +1,145 @@ +package ch.geowerkstatt.ilivalidator.extensions.functions; + +import ch.interlis.ili2c.metamodel.PathEl; +import ch.interlis.ili2c.metamodel.Viewable; +import ch.interlis.iom.IomObject; +import ch.interlis.iom_j.itf.impl.jtsext.geom.CompoundCurve; +import ch.interlis.iox.IoxException; +import ch.interlis.iox_j.jts.Iox2jtsext; +import ch.interlis.iox_j.validator.Value; +import com.vividsolutions.jts.geom.IntersectionMatrix; +import com.vividsolutions.jts.index.strtree.STRtree; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +public final class PolylinesOverlapIoxPlugin extends BaseInterlisFunction { + private static final Map HAS_EQUAL_LINE_PART_CACHE = new HashMap<>(); + + @Override + public String getQualifiedIliName() { + return "GeoW_FunctionsExt.PolylinesOverlap"; + } + + @Override + protected Value evaluateInternal(String validationKind, String usageScope, IomObject contextObject, Value[] arguments) { + Value argObjects = arguments[0]; + Value argPath = arguments[1]; + + if (argObjects.isUndefined()) { + return Value.createSkipEvaluation(); + } + + if (argObjects.getComplexObjects() == null) { + return Value.createUndefined(); + } + + Collection polylineObjects; + + if (argPath.isUndefined()) { + polylineObjects = argObjects.getComplexObjects(); + } else { + Viewable contextClass = EvaluationHelper.getContextClass(td, contextObject, argObjects); + if (contextClass == null) { + throw new IllegalStateException("unknown class in " + usageScope); + } + + PathEl[] attributePath = EvaluationHelper.getAttributePathEl(validator, contextClass, argPath); + polylineObjects = EvaluationHelper.evaluateAttributes(validator, argObjects, attributePath); + } + + Collection inputObjects = argObjects.getComplexObjects(); + List objectIds = inputObjects.stream().map(IomObject::getobjectoid).collect(Collectors.toList()); + boolean hasObjectIds = objectIds.stream().allMatch(Objects::nonNull); + if (!hasObjectIds) { + List lines = convertToJTSLines(polylineObjects); + return new Value(hasEqualLinePart(lines)); + } + + HasEqualLinePartKey key = new HasEqualLinePartKey(objectIds, argPath.isUndefined() ? null : argPath.getValue()); + + boolean hasOverlap = HAS_EQUAL_LINE_PART_CACHE.computeIfAbsent(key, k -> { + List lines = convertToJTSLines(polylineObjects); + return hasEqualLinePart(lines); + }); + return new Value(hasOverlap); + } + + private List convertToJTSLines(Collection polylines) { + return polylines.stream() + .map(line -> { + try { + return Iox2jtsext.polyline2JTS(line, false, 0); + } catch (IoxException e) { + logger.addEvent(logger.logErrorMsg("Could not calculate {0}", getQualifiedIliName())); + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static boolean hasEqualLinePart(List lines) { + if (lines.size() <= 1) { + return false; + } + + STRtree tree = new STRtree(); + for (CompoundCurve line : lines) { + tree.insert(line.getEnvelopeInternal(), line); + } + + AtomicBoolean hasOverlap = new AtomicBoolean(false); + for (CompoundCurve line : lines) { + if (hasOverlap.get()) { + break; + } + tree.query(line.getEnvelopeInternal(), o -> { + if (!hasOverlap.get() && o != line && linesHaveEqualPart(line, (CompoundCurve) o)) { + hasOverlap.set(true); + } + }); + } + return hasOverlap.get(); + } + + private static boolean linesHaveEqualPart(CompoundCurve a, CompoundCurve b) { + IntersectionMatrix relation = a.relate(b); + + // If the intersection of the interiors is a line, they have at least one part of a section in common + int interiorIntersection = relation.get(0, 0); + return interiorIntersection == 1; + } + + private static final class HasEqualLinePartKey { + private final List objectIds; + private final String attributeName; + + HasEqualLinePartKey(List objectIds, String attributeName) { + this.objectIds = objectIds; + this.attributeName = attributeName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HasEqualLinePartKey that = (HasEqualLinePartKey) o; + return Objects.equals(objectIds, that.objectIds) && Objects.equals(attributeName, that.attributeName); + } + + @Override + public int hashCode() { + return Objects.hash(objectIds, attributeName); + } + } +} diff --git a/src/model/GeoW_FunctionsExt.ili b/src/model/GeoW_FunctionsExt.ili index b2104f9..d34b8d4 100644 --- a/src/model/GeoW_FunctionsExt.ili +++ b/src/model/GeoW_FunctionsExt.ili @@ -39,4 +39,10 @@ MODEL GeoW_FunctionsExt !!@ fn.return = "Zusammengefasste Flächen-Geometrie"; !!@ fn.since = "2023-12-13"; FUNCTION Union (Geometries: ANYSTRUCTURE): MULTIAREA; + + !!@ fn.description = "Prüft, ob sich die Linien-Geometrien überlappen oder eine gemeinsame Teilstrecke vorhanden ist (wenn die Schnittmenge der Innenbereiche einer Linie entspricht). Für 'Objects' können Objekte oder Geometrien angegeben werden. Für 'LineAttr' soll der Pfad zur Linien-Geometrie in INTERLIS 2 Syntax angegeben werden. Falls 'Objects' bereits die Geometrien enthält, soll für 'LineAttr' 'UNDEFINED' übergeben werden."; + !!@ fn.param = "Objects: Ausgangsobjekte oder Geometrien. LineAttr: Pfad zum Geometrieattribut oder UNDEFINED"; + !!@ fn.return = "TRUE, wenn sich zwei Linien überlappen oder zwischen zwei Linien eine gemeinsame Teilstrecke vorhanden ist"; + !!@ fn.since = "2023-12-18"; + FUNCTION PolylinesOverlap (Objects: OBJECTS OF ANYCLASS; LineAttr: TEXT): BOOLEAN; END GeoW_FunctionsExt. \ No newline at end of file diff --git a/src/test/data/PolylinesOverlap/PolylinesOverlap.ili b/src/test/data/PolylinesOverlap/PolylinesOverlap.ili new file mode 100644 index 0000000..76435c9 --- /dev/null +++ b/src/test/data/PolylinesOverlap/PolylinesOverlap.ili @@ -0,0 +1,29 @@ +INTERLIS 2.4; + +MODEL TestSuite + AT "mailto:info@geowerkstatt.ch" VERSION "2023-12-14" = + IMPORTS GeoW_FunctionsExt; + + DOMAIN + !!@CRS=EPSG:2056 + CHKoord = COORD 2460000.000 .. 2870000.000 [INTERLIS.m], + 1045000.000 .. 1310000.000 [INTERLIS.m], + ROTATION 2 -> 1; + + TOPIC FunctionTestTopic = + + CLASS TestClass = + geometry : POLYLINE WITH (STRAIGHTS) VERTEX CHKoord WITHOUT OVERLAPS > 0.001; + type : (t1,t2,t3); + + SET CONSTRAINT setConstraintAllNoOverlaps : NOT(GeoW_FunctionsExt.PolylinesOverlap(ALL, "geometry")); + SET CONSTRAINT setConstraintT1 : WHERE type == #t1 : GeoW_FunctionsExt.PolylinesOverlap(ALL, "geometry"); + SET CONSTRAINT setConstraintT2 : WHERE type == #t2 : GeoW_FunctionsExt.PolylinesOverlap(ALL, "geometry"); + SET CONSTRAINT setConstraintT3 : WHERE type == #t3 : GeoW_FunctionsExt.PolylinesOverlap(ALL, "geometry"); + SET CONSTRAINT setConstraintT1or2 : WHERE type == #t1 OR type == #t2 : GeoW_FunctionsExt.PolylinesOverlap(ALL, "geometry"); + SET CONSTRAINT setConstraintT2or3 : WHERE type == #t2 OR type == #t3 : GeoW_FunctionsExt.PolylinesOverlap(ALL, "geometry"); + END TestClass; + + END FunctionTestTopic; + +END TestSuite. diff --git a/src/test/data/PolylinesOverlap/TestData.xtf b/src/test/data/PolylinesOverlap/TestData.xtf new file mode 100644 index 0000000..eeec078 --- /dev/null +++ b/src/test/data/PolylinesOverlap/TestData.xtf @@ -0,0 +1,231 @@ + + + + + TestSuite + + ili2gpkg-5.0.0-20f2bb62307ba6329a125fc6f10965ad9a4e6300 + + + + + + + + 2645657.466 + 1249752.542 + + + 2645626.782 + 1249819.826 + + + 2645687.041 + 1249842.746 + + + 2645691.702 + 1249830.036 + + + 2645693.141 + 1249826.110 + + + + t1 + + + + + + 2645542.677 + 1249774.538 + + + 2645543.027 + 1249774.761 + + + 2645564.119 + 1249788.217 + + + 2645584.822 + 1249800.602 + + + 2645613.103 + 1249812.986 + + + 2645625.673 + 1249785.629 + + + 2645629.624 + 1249777.049 + + + 2645637.503 + 1249759.936 + + + + t1 + + + + + + 2645691.702 + 1249830.036 + + + 2645693.141 + 1249826.110 + + + 2645699.241 + 1249810.722 + + + 2645704.139 + 1249797.921 + + + + t1 + + + + + + 2645543.027 + 1249774.761 + + + 2645564.119 + 1249788.217 + + + 2645565.875 + 1249785.306 + + + 2645572.530 + 1249789.557 + + + 2645581.032 + 1249773.106 + + + + t2 + + + + + + 2645549.805 + 1249782.568 + + + 2645564.778 + 1249757.798 + + + + t3 + + + + + + 2645657.466 + 1249752.542 + + + 2645626.782 + 1249819.826 + + + 2645687.041 + 1249842.746 + + + 2645694.446 + 1249833.030 + + + 2645689.640 + 1249826.191 + + + 2645678.735 + 1249822.125 + + + + t2 + + + + + + 2645647.126 + 1249816.209 + + + 2645641.950 + 1249828.224 + + + 2645648.790 + 1249831.367 + + + 2645651.562 + 1249825.082 + + + 2645660.065 + 1249828.409 + + + 2645657.847 + 1249834.324 + + + + t3 + + + + + + 2645629.624 + 1249777.049 + + + 2645637.503 + 1249759.936 + + + 2645646.941 + 1249738.205 + + + 2645643.060 + 1249727.669 + + + + t3 + + + + diff --git a/src/test/data/PolylinesOverlap/TestDataSameLine.xtf b/src/test/data/PolylinesOverlap/TestDataSameLine.xtf new file mode 100644 index 0000000..651db27 --- /dev/null +++ b/src/test/data/PolylinesOverlap/TestDataSameLine.xtf @@ -0,0 +1,99 @@ + + + + + TestSuite + + ili2gpkg-5.0.0-20f2bb62307ba6329a125fc6f10965ad9a4e6300 + + + + + + + + 2645626.782 + 1249819.826 + + + 2645687.041 + 1249842.746 + + + + t1 + + + + + + 2645549.805 + 1249782.568 + + + 2645564.778 + 1249757.798 + + + + t1 + + + + + + 2645657.466 + 1249752.542 + + + 2645626.782 + 1249819.826 + + + 2645687.041 + 1249842.746 + + + 2645691.702 + 1249830.036 + + + 2645693.141 + 1249826.110 + + + + t2 + + + + + + 2645657.466 + 1249752.542 + + + 2645626.782 + 1249819.826 + + + 2645687.041 + 1249842.746 + + + 2645691.702 + 1249830.036 + + + 2645693.141 + 1249826.110 + + + + t3 + + + + diff --git a/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/PolylinesOverlapIoxPluginTest.java b/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/PolylinesOverlapIoxPluginTest.java new file mode 100644 index 0000000..f6ee501 --- /dev/null +++ b/src/test/java/ch/geowerkstatt/ilivalidator/extensions/functions/PolylinesOverlapIoxPluginTest.java @@ -0,0 +1,43 @@ +package ch.geowerkstatt.ilivalidator.extensions.functions; + +import ch.interlis.ili2c.Ili2cFailure; +import ch.interlis.iox.IoxException; +import com.vividsolutions.jts.util.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public final class PolylinesOverlapIoxPluginTest { + private static final String TEST_DATA = "PolylinesOverlap/TestData.xtf"; + private static final String TEST_DATA_SAME_LINE = "PolylinesOverlap/TestDataSameLine.xtf"; + private ValidationTestHelper vh = null; + + @BeforeEach + void setUp() { + vh = new ValidationTestHelper(); + vh.addFunction(new PolylinesOverlapIoxPlugin()); + } + + @Test + void polylinesOverlap() throws Ili2cFailure, IoxException { + vh.runValidation(new String[]{TEST_DATA}, new String[]{"PolylinesOverlap/PolylinesOverlap.ili"}); + Assert.equals(4, vh.getErrs().size()); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintAllNoOverlaps"); + AssertionHelper.assertNoConstraintError(vh, "setConstraintT1"); // Some lines in T1 overlap + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintT2"); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintT3"); + AssertionHelper.assertNoConstraintError(vh, "setConstraintT1or2"); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintT2or3"); + } + + @Test + void sameLineOverlaps() throws Ili2cFailure, IoxException { + vh.runValidation(new String[]{TEST_DATA_SAME_LINE}, new String[]{"PolylinesOverlap/PolylinesOverlap.ili"}); + Assert.equals(4, vh.getErrs().size()); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintAllNoOverlaps"); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintT1"); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintT2"); + AssertionHelper.assertConstraintErrors(vh, 1, "setConstraintT3"); + AssertionHelper.assertNoConstraintError(vh, "setConstraintT1or2"); // One line in T1 is a segment of T2 + AssertionHelper.assertNoConstraintError(vh, "setConstraintT2or3"); // The lines in T2 and T3 are equal + } +}