Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PolylinesOverlap function #36

Merged
merged 9 commits into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 -> {
domi-b marked this conversation as resolved.
Show resolved Hide resolved
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
Loading