Skip to content

Commit

Permalink
Merge pull request #36 Add PolylinesOverlap function
Browse files Browse the repository at this point in the history
  • Loading branch information
domi-b committed Dec 20, 2023
2 parents 72187ba + 7e21c70 commit 00f1cca
Show file tree
Hide file tree
Showing 6 changed files with 553 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<HasEqualLinePartKey, Boolean> 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<IomObject> 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<IomObject> inputObjects = argObjects.getComplexObjects();
List<String> objectIds = inputObjects.stream().map(IomObject::getobjectoid).collect(Collectors.toList());
boolean hasObjectIds = objectIds.stream().allMatch(Objects::nonNull);
if (!hasObjectIds) {
List<CompoundCurve> 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<CompoundCurve> lines = convertToJTSLines(polylineObjects);
return hasEqualLinePart(lines);
});
return new Value(hasOverlap);
}

private List<CompoundCurve> convertToJTSLines(Collection<IomObject> 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<CompoundCurve> 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<String> objectIds;
private final String attributeName;

HasEqualLinePartKey(List<String> 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);
}
}
}
6 changes: 6 additions & 0 deletions src/model/GeoW_FunctionsExt.ili
Original file line number Diff line number Diff line change
Expand Up @@ -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.
29 changes: 29 additions & 0 deletions src/test/data/PolylinesOverlap/PolylinesOverlap.ili
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
INTERLIS 2.4;

MODEL TestSuite
AT "mailto:[email protected]" 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.
Loading

0 comments on commit 00f1cca

Please sign in to comment.