IbisSplitFeatureActionBean.java
/*
* Copyright (C) 2015 B3Partners B.V.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package nl.b3p.viewer.stripes;
import org.locationtech.jts.geom.Geometry;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import net.sourceforge.stripes.action.StrictBinding;
import net.sourceforge.stripes.action.UrlBinding;
import nl.b3p.viewer.ibis.util.IbisConstants;
import static nl.b3p.viewer.ibis.util.IbisConstants.KAVEL_TERREIN_ID_FIELDNAME;
import static nl.b3p.viewer.ibis.util.IbisConstants.WORKFLOW_FIELDNAME;
import nl.b3p.viewer.ibis.util.WorkflowStatus;
import nl.b3p.viewer.ibis.util.WorkflowUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geotools.data.DataUtilities;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.util.Converter;
import org.geotools.data.util.GeometryTypeConverterFactory;
import org.json.JSONException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.GeometryType;
import org.opengis.filter.Filter;
import org.opengis.filter.identity.FeatureId;
import org.json.JSONObject;
import org.stripesstuff.stripersist.Stripersist;
/**
* A workflow-supporting split action bean for ibis.
*
* @author mprins
*/
@UrlBinding("/action/feature/ibissplit")
@StrictBinding
public class IbisSplitFeatureActionBean extends SplitFeatureActionBean implements IbisConstants {
private static final Log log = LogFactory.getLog(IbisSplitFeatureActionBean.class);
private Object terreinID = null;
private WorkflowStatus newWorkflowStatus = WorkflowStatus.definitief;
/**
* Thinness ratio of polygon; basically ratio of area/circumference of a
* polygon to detect slivers.
* <a href="http://gis.stackexchange.com/questions/151939/explanation-of-the-thinness-ratio-formula">Explanation
* of the Thinness ratio formula?</a> and
* <a href="https://books.google.se/books?hl=sv&id=uGWmR0f_350C&q=thinness#v=onepage&q=thinness&f=false">Microscope
* Image Processing</a>
*/
private double sliverRatio;
/**
* max. area of a sliver.
*/
private double sliverMaxArea;
/**
* force the workflow status attribute on the feature. This will handle the
* case where the {@code extraData} attribute is a piece of json with the
* workflow, eg
* {@code {workflow_status:'afgevoerd',datum_mutatie:'2015-12-01Z00:00'}}.
*
* @param features A list of features to be modified
* @return the list of modified features that are about to be committed to
* the datastore
*
* @throws JSONException if json parsing failed
*/
@Override
protected List<SimpleFeature> handleExtraData(List<SimpleFeature> features) throws JSONException {
JSONObject json = new JSONObject(this.getExtraData());
// ideally thses would be passed as regular request params, but that requires an upgrade in de base flamingo component
this.sliverMaxArea = json.getDouble("sliverMaxArea");
json.remove("sliverMaxArea");
this.sliverRatio = json.getDouble("sliverRatio");
json.remove("sliverRatio");
Iterator items = json.keys();
while (items.hasNext()) {
String key = (String) items.next();
for (SimpleFeature f : features) {
log.debug(String.format("Setting value: %s for attribute: %s on feature %s", json.get(key), key, f.getID()));
f.setAttribute(key, json.get(key));
if (key.equalsIgnoreCase(WORKFLOW_FIELDNAME)) {
newWorkflowStatus = WorkflowStatus.valueOf(json.getString(key));
}
}
}
features = resolveSlivers(features);
if (features.size() < 2) {
// sliver resolution has "cleaned" away all split features and we only have the original left.
throw new IllegalArgumentException("Splits mislukt, de oppervlakte van afgesplitste deel is kleiner dan de drempel ("
+ this.sliverMaxArea + ") of de omtrek/oppervlakte ratio is te klein.");
}
return features;
}
/**
* {@inheritDoc} Overrides deleting features by archiving them instead.
*/
@Override
protected List<FeatureId> handleStrategy(SimpleFeature feature, List<? extends Geometry> geoms,
Filter filter, SimpleFeatureStore localStore, String localStrategy) throws Exception {
if (!this.getLayer().getName().equalsIgnoreCase(KAVEL_LAYER_NAME)) {
throw new IllegalArgumentException("Aborting as split layer is not " + KAVEL_LAYER_NAME);
}
List<SimpleFeature> newFeats = new ArrayList();
GeometryTypeConverterFactory cf = new GeometryTypeConverterFactory();
Converter c = cf.createConverter(Geometry.class, localStore.getSchema().getGeometryDescriptor().getType().getBinding(), null);
GeometryType type = localStore.getSchema().getGeometryDescriptor().getType();
String geomAttribute = localStore.getSchema().getGeometryDescriptor().getLocalName();
boolean firstFeature = true;
int newID = (int) (new Date().getTime() / 1000);
for (Geometry newGeom : geoms) {
log.debug("Creating feature with geom length: " + newGeom.getLength());
if (firstFeature) {
if (localStrategy.equalsIgnoreCase("add")) {
// existing feature to "archief"
feature.setAttribute(WORKFLOW_FIELDNAME, WorkflowStatus.archief);
Object[] attributevalues = feature.getAttributes().toArray(new Object[feature.getAttributeCount()]);
String[] attributes = DataUtilities.attributeNames(feature.getFeatureType());
localStore.modifyFeatures(attributes, attributevalues, filter);
// remember terreinID
this.terreinID = feature.getAttribute(KAVEL_TERREIN_ID_FIELDNAME);
// create the first new feature
SimpleFeature newFeat = DataUtilities.createFeature(feature.getType(),
DataUtilities.encodeFeature(feature, false));
newFeat.setAttribute(geomAttribute, c.convert(newGeom, type.getBinding()));
log.debug("First split feature has ibis id: " + newFeat.getAttribute(ID_FIELDNAME));
newFeats.add(newFeat);
firstFeature = false;
continue;
} else {
throw new IllegalArgumentException("Unknown or unsupported strategy '" + localStrategy + "', cannot split.");
}
}
// create + add new features using the rest of the geometries
SimpleFeature newFeat = DataUtilities.createFeature(feature.getType(),
DataUtilities.encodeFeature(feature, false));
newFeat.setAttribute(geomAttribute, c.convert(newGeom, type.getBinding()));
newFeat.setAttribute(ID_FIELDNAME, newID++);
log.debug("Created new feature with ibis id: " + newID);
newFeats.add(newFeat);
}
// update specified fields on the new features
newFeats = this.handleExtraData(newFeats);
return localStore.addFeatures(DataUtilities.collection(newFeats));
}
/**
* Called after the split is completed and commit was performed. update
* terrein geometry
*/
@Override
protected void afterSplit(List<FeatureId> ids) {
if (this.terreinID != null) {
WorkflowUtil.updateTerreinGeometry(Integer.parseInt(this.terreinID.toString()), this.getLayer(),
this.newWorkflowStatus, this.getApplication(), Stripersist.getEntityManager());
}
}
private List<SimpleFeature> resolveSlivers(List<SimpleFeature> features) {
List<SimpleFeature> nonSlivers = features;
if (this.sliverRatio > 0) {
ArrayList<SimpleFeature> slivers = new ArrayList();
nonSlivers = new ArrayList();
// determine slivers and 'real' polygons
for (SimpleFeature f : features) {
Geometry g = (Geometry) f.getDefaultGeometry();
// use minimum area and thinness ratio formula: 4 * pi * area/(length*length)
if (g.getArea() <= sliverMaxArea && 4 * Math.PI * (g.getArea() / (g.getLength() * g.getLength())) < sliverRatio) {
log.debug("Sliver gevonden: " + f);
slivers.add(f);
} else {
nonSlivers.add(f);
}
}
// merge slivers to non-slivers..
for (SimpleFeature f : nonSlivers) {
Geometry g = (Geometry) f.getDefaultGeometry();
ArrayList<SimpleFeature> sliversToRemove = new ArrayList();
for (SimpleFeature s : slivers) {
Geometry sg = (Geometry) s.getDefaultGeometry();
if (sg.touches(g)) {
log.debug("Samenvoegen van sliver: " + s + " met: " + g);
g = sg.union(g);
sliversToRemove.add(s);
}
}
f.setDefaultGeometry(g);
slivers.removeAll(sliversToRemove);
}
// slivers should be empty
if (!slivers.isEmpty()) {
log.warn("Niet alle slivers zijn opgelost. Overblijvers:");
for (SimpleFeature s : slivers) {
log.warn(" overblijvende sliver: " + s);
}
}
}
return nonSlivers;
}
//<editor-fold defaultstate="collapsed" desc="getters en setters">
public double getSliverRatio() {
return sliverRatio;
}
public void setSliverRatio(double sliverRatio) {
this.sliverRatio = sliverRatio;
}
public double getSliverMaxArea() {
return sliverMaxArea;
}
public void setSliverMaxArea(double sliverMaxArea) {
this.sliverMaxArea = sliverMaxArea;
}
//</editor-fold>
}