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 IsInsideAreaByCodeEnum function #2

Merged
merged 3 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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,46 @@
package ch.geowerkstatt.ilivalidator.extensions.functions.ngk;

import ch.ehi.basics.settings.Settings;
import ch.interlis.ili2c.metamodel.TransferDescription;
import ch.interlis.iom.IomObject;
import ch.interlis.iox.IoxValidationConfig;
import ch.interlis.iox_j.logging.LogEventFactory;
import ch.interlis.iox_j.validator.InterlisFunction;
import ch.interlis.iox_j.validator.ObjectPool;
import ch.interlis.iox_j.validator.Validator;
import ch.interlis.iox_j.validator.Value;

public abstract class BaseInterlisFunction implements InterlisFunction {

protected LogEventFactory logger;
protected TransferDescription td;
protected Settings settings;
protected Validator validator;
protected ObjectPool objectPool;

@Override
public final void init(TransferDescription td, Settings settings, IoxValidationConfig validationConfig, ObjectPool objectPool, LogEventFactory logEventFactory) {
this.logger = logEventFactory;
this.logger.setValidationConfig(validationConfig);
this.td = td;
this.settings = settings;
this.validator = (Validator) settings.getTransientObject(IOX_VALIDATOR);
this.objectPool = objectPool;
}

@Override
public final Value evaluate(String validationKind, String usageScope, IomObject mainObj, Value[] actualArguments) {

for (Value arg : actualArguments) {
if (arg.skipEvaluation()) {
return arg;
}
}

logger.setDataObj(mainObj);

return evaluateInternal(validationKind, usageScope, mainObj, actualArguments);
}

protected abstract Value evaluateInternal(String validationKind, String usageScope, IomObject mainObj, Value[] actualArguments);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package ch.geowerkstatt.ilivalidator.extensions.functions.ngk;

import ch.ehi.basics.logging.EhiLogger;
import ch.interlis.ili2c.Ili2cException;
import ch.interlis.ili2c.metamodel.Element;
import ch.interlis.ili2c.metamodel.ObjectPath;
import ch.interlis.ili2c.metamodel.PathEl;
import ch.interlis.ili2c.metamodel.TransferDescription;
import ch.interlis.ili2c.metamodel.Viewable;
import ch.interlis.iom.IomObject;
import ch.interlis.iox_j.validator.Validator;
import ch.interlis.iox_j.validator.Value;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public final class EvaluationHelper {

private EvaluationHelper() {
// Utility class
}

/**
* Parse the {@code argPath} into a {@link PathEl} array.
*
* @param validator the {@link Validator} instance.
* @param contextClass the {@link Viewable} definition where the {@code argPath} starts.
* @param argPath the path string to parse. see {@link Value#getValue()}.
*
* @return the parsed {@link PathEl} array or {@code null} if the {@code argPath} could not be parsed.
*/
public static PathEl[] getAttributePathEl(Validator validator, Viewable<Element> contextClass, Value argPath) {
try {
ObjectPath objectPath = validator.parseObjectOrAttributePath(contextClass, argPath.getValue());
if (objectPath.getPathElements() != null) {
return objectPath.getPathElements();
}
} catch (Ili2cException e) {
EhiLogger.logError(e);
}
return null;
}

/**
* Get the {@link Viewable} (e.g. the class definition) from the {@link TransferDescription}.
* If the {@code iomObject} is {@code null}, the {@code argObjects} is used to retrieve the {@link Viewable}.
*/
public static Viewable getContextClass(TransferDescription td, IomObject iomObject, Value argObjects) {
if (iomObject != null) {
return (Viewable) td.getElement(iomObject.getobjecttag());
} else if (argObjects.getViewable() != null) {
return argObjects.getViewable();
} else if (argObjects.getComplexObjects() != null) {
Iterator<IomObject> it = argObjects.getComplexObjects().iterator();
if (!it.hasNext()) {
return null;
}
return (Viewable) td.getElement(it.next().getobjecttag());
}
return null;
}

/**
* Get the collection of {@link IomObject} inside {@code argObjects} by following the provided {@code attributePath}.
*/
public static Collection<IomObject> evaluateAttributes(Validator validator, Value argObjects, PathEl[] attributePath) {
Collection<IomObject> attributes = new ArrayList<>();

for (IomObject rootObject : argObjects.getComplexObjects()) {
Value surfaceAttributes = validator.getValueFromObjectPath(null, rootObject, attributePath, null);
if (!(surfaceAttributes.isUndefined() || surfaceAttributes.skipEvaluation() || surfaceAttributes.getComplexObjects() == null)) {
attributes.addAll(surfaceAttributes.getComplexObjects());
}
}

return attributes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package ch.geowerkstatt.ilivalidator.extensions.functions.ngk;

import ch.ehi.basics.types.OutParam;
import ch.interlis.ili2c.metamodel.EnumerationType;
import ch.interlis.ili2c.metamodel.PathEl;
import ch.interlis.ili2c.metamodel.Type;
import ch.interlis.ili2c.metamodel.Viewable;
import ch.interlis.iom.IomObject;
import ch.interlis.iox_j.jts.Iox2jtsext;
import ch.interlis.iox_j.validator.Value;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;

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.Objects;
import java.util.stream.Collectors;

public final class IsInsideAreaByCodeEnumIoxPlugin extends BaseInterlisFunction {
private static final Map<InsideAreaKey, Value> OBJECTS_CACHE = new HashMap<>();

@Override
public String getQualifiedIliName() {
return "NGK_SO_FunctionsExt.IsInsideAreaByCodeEnum";
}

@Override
protected Value evaluateInternal(String validationKind, String usageScope, IomObject contextObject, Value[] arguments) {
Value argObjects = arguments[0];
Value argGeometryPath = arguments[1];
Value argEnumPath = arguments[2];

if (argObjects.isUndefined() || argGeometryPath.isUndefined() || argEnumPath.isUndefined()) {
return Value.createSkipEvaluation();
}

Collection<IomObject> objects = argObjects.getComplexObjects();
if (objects.isEmpty()) {
return new Value(true);
}

List<String> objectIds = objects.stream().map(IomObject::getobjectoid).collect(Collectors.toList());
domi-b marked this conversation as resolved.
Show resolved Hide resolved
String geometryAttribute = argGeometryPath.getValue();
String enumAttribute = argEnumPath.getValue();

InsideAreaKey key = new InsideAreaKey(objectIds, geometryAttribute, enumAttribute);
return OBJECTS_CACHE.computeIfAbsent(key, k -> {
Viewable contextClass = EvaluationHelper.getContextClass(td, contextObject, argObjects);
if (contextClass == null) {
throw new IllegalStateException("unknown class in " + usageScope);
domi-b marked this conversation as resolved.
Show resolved Hide resolved
}

PathEl[] geometryPath = EvaluationHelper.getAttributePathEl(validator, contextClass, argGeometryPath);
PathEl[] enumPath = EvaluationHelper.getAttributePathEl(validator, contextClass, argEnumPath);

return isInsideArea(usageScope, objects, geometryPath, enumPath);
});
}

private Value isInsideArea(String usageScope, Collection<IomObject> objects, PathEl[] geometryPath, PathEl[] enumPath) {
Map<ValueKey, Geometry> geometriesByEnumValue = objects.stream()
.collect(Collectors.toMap(
o -> getEnumValue(o, enumPath),
o -> getGeometryValue(o, geometryPath),
Geometry::union
));

ValueKey firstKey = geometriesByEnumValue.keySet().iterator().next();
Type keyType = firstKey.getType();
if (!(keyType instanceof EnumerationType)) {
logger.addEvent(logger.logErrorMsg("{0}: Enumeration type expected.", usageScope));
return Value.createSkipEvaluation();
}
EnumerationType enumType = (EnumerationType) keyType;
if (!enumType.isOrdered()) {
logger.addEvent(logger.logErrorMsg("{0}: Enumeration type must be ordered.", usageScope));
return Value.createSkipEvaluation();
}

List<Geometry> sortedGeometries = sortByEnumValues(geometriesByEnumValue, enumType);
for (int i = 0; i < sortedGeometries.size() - 1; i++) {
Geometry current = sortedGeometries.get(i);
Geometry next = sortedGeometries.get(i + 1);

if (!next.contains(current)) {
return new Value(false);
}
}

return new Value(true);
}

private List<Geometry> sortByEnumValues(Map<ValueKey, Geometry> map, EnumerationType enumType) {
List<String> enumValues = enumType.getValues();

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

private ValueKey getEnumValue(IomObject object, PathEl[] enumPath) {
Value value = validator.getValueFromObjectPath(null, object, enumPath, null);
return new ValueKey(value);
}

private Geometry getGeometryValue(IomObject object, PathEl[] geometryPath) {
Value objects = new Value(Collections.singletonList(object));
Collection<IomObject> geometryObjects = EvaluationHelper.evaluateAttributes(validator, objects, geometryPath);

List<Geometry> geometries = geometryObjects.stream()
.map(g -> {
try {
return Iox2jtsext.multisurface2JTS(g, 0, new OutParam<>(), logger, 0, "warning");
} catch (Exception e) {
logger.addEvent(logger.logWarningMsg("{0}: Failed to convert surface to JTS: {1}", getQualifiedIliName(), e.getLocalizedMessage()));
return null;
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());

if (geometries.size() == 1) {
return geometries.get(0);
} else {
return new GeometryFactory().buildGeometry(geometries);
}
}

private static final class ValueKey {
private final Value value;

ValueKey(Value value) {
this.value = value;
}

public Type getType() {
return value.getType();
}

public String getStringValue() {
return value.getValue();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ValueKey)) {
return false;
}
ValueKey that = (ValueKey) o;
return Objects.equals(getStringValue(), that.getStringValue())
&& Objects.equals(getType(), that.getType());
}

@Override
public int hashCode() {
return Objects.hash(getStringValue());
}
}

private static final class InsideAreaKey {
private final List<String> objectIds;
private final String geometryAttribute;
private final String enumAttribute;

InsideAreaKey(List<String> objectIds, String geometryAttribute, String enumAttribute) {
this.objectIds = objectIds;
this.geometryAttribute = geometryAttribute;
this.enumAttribute = enumAttribute;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof InsideAreaKey)) {
return false;
}
InsideAreaKey that = (InsideAreaKey) o;
return objectIds.equals(that.objectIds)
&& geometryAttribute.equals(that.geometryAttribute)
&& enumAttribute.equals(that.enumAttribute);
}

@Override
public int hashCode() {
return Objects.hash(objectIds, geometryAttribute, enumAttribute);
}
}
}
6 changes: 5 additions & 1 deletion src/model/NGK_SO_FunctionsExt.ili
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
INTERLIS 2.4;
MODEL NGK_SO_FunctionsExt
AT "mailto:[email protected]" VERSION "2024-01-23" =

!!@ fn.description = "Prüft bei der Objektmenge, dass die gemäss dem geordneten Enum 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.";
!!@ fn.param = "Objects: Zu prüfende Objektmenge. GeometryAttr: Pfad zur Geometrie. CodeAttr: Pfad zum Enum";
!!@ fn.return = "Boolean";
!!@ fn.since = "2024-01-23";
FUNCTION IsInsideAreaByCodeEnum (Objects: OBJECTS OF ANYCLASS; GeometryAttr: TEXT; CodeAttr: TEXT): BOOLEAN;
END NGK_SO_FunctionsExt.
8 changes: 8 additions & 0 deletions src/model/ilimodels.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?><TRANSFER xmlns="http://www.interlis.ch/INTERLIS2.3">
<HEADERSECTION SENDER="mkilimodelsxml-5.4.1-b49d62c05c4a212d9a0aa81797efd5185ed288af" VERSION="2.3"><MODELS><MODEL NAME="IliRepository20" VERSION="2020-04-17" URI="http://models.interlis.ch/core"></MODEL></MODELS></HEADERSECTION>
<DATASECTION>
<IliRepository20.RepositoryIndex BID="b1">
<IliRepository20.RepositoryIndex.ModelMetadata TID="10"><Name>NGK_SO_FunctionsExt</Name><SchemaLanguage>ili2_4</SchemaLanguage><File>NGK_SO_FunctionsExt.ili</File><Version>2024-01-23</Version><publishingDate>2024-01-23</publishingDate><Issuer>mailto:[email protected]</Issuer><browseOnly>false</browseOnly><md5>fde4446812ef76bd0d2bde502b79ee23</md5></IliRepository20.RepositoryIndex.ModelMetadata>
</IliRepository20.RepositoryIndex>
</DATASECTION>
</TRANSFER>
31 changes: 31 additions & 0 deletions src/test/data/IsInsideAreaByCodeEnum/SetConstraints.ili
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
INTERLIS 2.4;

MODEL TestSuite
AT "mailto:[email protected]" VERSION "2024-01-23" =
IMPORTS NGK_SO_FunctionsExt;

TOPIC FunctionTestTopic =

DOMAIN
Code = (
code_1,
code_2,
code_3,
code_4
) ORDERED;

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

CLASS BaseClass =
code : Code;
surface : SURFACE WITH (STRAIGHTS) VERTEX CHKoord WITHOUT OVERLAPS > 0.001;

SET CONSTRAINT insideAreaConstraint: NGK_SO_FunctionsExt.IsInsideAreaByCodeEnum(ALL, "surface", "code");
END BaseClass;

END FunctionTestTopic;

END TestSuite.
Loading
Loading