Java tutorial
/* * Copyright (C) 2015-2016 B3Partners B.V. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero 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 org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.PrecisionModel; import org.locationtech.jts.operation.overlay.snap.GeometrySnapper; import java.io.StringReader; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.After; import net.sourceforge.stripes.action.Before; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.action.StreamingResolution; import net.sourceforge.stripes.action.StrictBinding; import net.sourceforge.stripes.action.UrlBinding; import net.sourceforge.stripes.controller.LifecycleStage; import net.sourceforge.stripes.validation.Validate; import nl.b3p.viewer.config.app.Application; import nl.b3p.viewer.config.app.ApplicationLayer; import nl.b3p.viewer.config.security.Authorizations; import nl.b3p.viewer.config.services.Layer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.geotools.data.DataUtilities; import org.geotools.data.DefaultTransaction; import org.geotools.data.FeatureSource; import org.geotools.data.Transaction; import org.geotools.data.simple.SimpleFeatureStore; import org.geotools.factory.CommonFactoryFinder; import org.geotools.feature.FeatureCollection; import org.geotools.filter.identity.FeatureIdImpl; import org.geotools.geometry.jts.GeometryCollector; import org.geotools.util.Converter; import org.geotools.util.GeometryTypeConverterFactory; import org.json.JSONException; import org.json.JSONObject; 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.FilterFactory2; import org.opengis.filter.identity.FeatureId; import org.stripesstuff.stripersist.Stripersist; /** * Merge two features, A and B so that A will have the combined geometry of A * and B and B will cease to exist. A may be a new feature if that strategy is * chosen. * * @author Mark Prins mark@b3partners.nl */ @UrlBinding("/action/feature/merge") @StrictBinding public class MergeFeaturesActionBean extends LocalizableApplicationActionBean implements ActionBean { private static final Log LOG = LogFactory.getLog(MergeFeaturesActionBean.class); private static final String FID = FeatureInfoActionBean.FID; private ActionBeanContext context; @Validate private Application application; @Validate private ApplicationLayer appLayer; /** * Existing feature handling strategy. {@code replace} updates the existing * feature A with a new geometry and deletes feature B, {@code new} deletes * the existing features and creates a new feature. */ @Validate private String strategy; @Validate private String extraData; @Validate private int mergeGapDist; @Validate private String fidA; @Validate private String fidB; private SimpleFeatureStore store; private Layer layer = null; private boolean unauthorized; @After(stages = LifecycleStage.BindingAndValidation) public void loadLayer() { this.layer = appLayer.getService().getSingleLayer(appLayer.getLayerName(), Stripersist.getEntityManager()); } @Before(stages = LifecycleStage.EventHandling) public void checkAuthorization() { if (application == null || appLayer == null || !Authorizations.isLayerGeomWriteAuthorized(layer, context.getRequest(), Stripersist.getEntityManager())) { unauthorized = true; } } public Resolution merge() throws JSONException { JSONObject json = new JSONObject(); json.put("success", Boolean.FALSE); String error = null; if (appLayer == null) { error = getBundle().getString("viewer.mergefeaturesactionbean.1"); } else if (unauthorized) { error = getBundle().getString("viewer.mergefeaturesactionbean.2"); } else { FeatureSource fs = null; try { if (this.fidA == null || this.fidB == null) { throw new IllegalArgumentException(getBundle().getString("viewer.mergefeaturesactionbean.3")); } if (this.strategy == null) { throw new IllegalArgumentException(getBundle().getString("viewer.mergefeaturesactionbean.4")); } fs = this.layer.getFeatureType().openGeoToolsFeatureSource(); if (!(fs instanceof SimpleFeatureStore)) { throw new IllegalArgumentException(getBundle().getString("viewer.mergefeaturesactionbean.5")); } this.store = (SimpleFeatureStore) fs; List<FeatureId> ids = this.mergeFeatures(); if (ids.isEmpty()) { throw new IllegalArgumentException(getBundle().getString("viewer.mergefeaturesactionbean.6")); } if (ids.size() > 1) { throw new IllegalArgumentException(getBundle().getString("viewer.mergefeaturesactionbean.7")); } json.put("fids", ids); json.put("success", Boolean.TRUE); } catch (IllegalArgumentException e) { LOG.warn("Merge error", e); error = e.getLocalizedMessage(); } catch (Exception e) { LOG.error(MessageFormat.format(getBundle().getString("viewer.mergefeaturesactionbean.8"), this.fidB, this.fidA, e)); error = e.toString(); if (e.getCause() != null) { error += "; cause: " + e.getCause().toString(); } } finally { if (fs != null) { fs.getDataStore().dispose(); } } } if (error != null) { json.put("error", error); } return new StreamingResolution("application/json", new StringReader(json.toString())); } /** * Handle extra data, to be extended by subclasses. eg. to modify the * features after the split before committing. * * @param features a list of features that can be modified * @return the list of features to be committed to the database * * @throws java.lang.Exception if any * * @see #handleExtraData(org.opengis.feature.simple.SimpleFeature) */ protected List<SimpleFeature> handleExtraData(List<SimpleFeature> features) throws Exception { return features; } /** * Handle extra data, delegates to {@link #handleExtraData(java.util.List)}. * * @param feature the feature that can be modified * @return the feature to be committed to the database * * @throws java.lang.Exception if any * * @see #handleExtraData(java.util.List) */ protected SimpleFeature handleExtraData(SimpleFeature feature) throws Exception { final List<SimpleFeature> features = new ArrayList(); features.add(feature); return this.handleExtraData(features).get(0); } /** * Get features from store and merge them. The final merge resuls depend on * the chosen {@link #strategy} and optional {@code afterMerge} processing. * * @return a list of feature ids that have been updated * @throws Exception when there is an error communication with the datastore * of when the arguments are invalid. In case of an exception the * transaction will be rolled back * * @see #handleStrategy(org.opengis.feature.simple.SimpleFeature, * org.opengis.feature.simple.SimpleFeature, * org.locationtech.jts.geom.Geometry, org.opengis.filter.Filter, * org.opengis.filter.Filter, org.geotools.data.simple.SimpleFeatureStore, * java.lang.String) * @see #afterMerge(java.util.List) */ private List<FeatureId> mergeFeatures() throws Exception { List<FeatureId> ids = new ArrayList(); Transaction transaction = new DefaultTransaction("split"); try { store.setTransaction(transaction); // get the features to merge from database using the FID FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(); Filter filterA = ff.id(new FeatureIdImpl(this.fidA)); Filter filterB = ff.id(new FeatureIdImpl(this.fidB)); SimpleFeature fA = null; FeatureCollection fc = store.getFeatures(filterA); if (fc.features().hasNext()) { fA = (SimpleFeature) fc.features().next(); } else { throw new IllegalArgumentException( MessageFormat.format(getBundle().getString("viewer.mergefeaturesactionbean.9"), this.fidA)); } SimpleFeature fB = null; fc = store.getFeatures(filterB); if (fc.features().hasNext()) { fB = (SimpleFeature) fc.features().next(); } else { throw new IllegalArgumentException(MessageFormat .format(getBundle().getString("viewer.mergefeaturesactionbean.10"), this.fidB)); } String geomAttrName = store.getSchema().getGeometryDescriptor().getLocalName(); Geometry geomA = (Geometry) fA.getProperty(geomAttrName).getValue(); Geometry geomB = (Geometry) fB.getProperty(geomAttrName).getValue(); LOG.debug("input geomA: " + geomA); LOG.debug("input geomB: " + geomB); Geometry newGeom = null; GeometryCollector geoms = new GeometryCollector(); geoms.setFactory(new GeometryFactory(new PrecisionModel(), geomA.getSRID())); geoms.add(geomA); geoms.add(geomB); if (!geomB.intersects(geomA)) { // no overlap between geometries, do some smart stuff to interpolate, then use this interpolation in the union of all geoms double distance = geomA.distance(geomB); LOG.info( MessageFormat.format(getBundle().getString("viewer.mergefeaturesactionbean.11"), distance)); if (distance > this.mergeGapDist) { throw new IllegalArgumentException(MessageFormat .format(getBundle().getString("viewer.mergefeaturesactionbean.12"), distance)); } newGeom = GeometrySnapper.snapToSelf(geoms.collect(), mergeGapDist, true); geoms.add(newGeom); } newGeom = geoms.collect().union(); LOG.debug("new geometry: " + newGeom); LOG.debug("New Geometry is valid? " + newGeom.isValid()); // if invalid maybe cleanup self-intersect; // see: https://stackoverflow.com/questions/31473553/is-there-a-way-to-convert-a-self-intersecting-polygon-to-a-multipolygon-in-jts // clean up small self intersects and unioning artifacts, snapping distance of 0.01m (because rijksdriehoek) newGeom = GeometrySnapper.snapToSelf(newGeom, .01, true); newGeom.normalize(); LOG.debug("Normalized new geometry: " + newGeom); LOG.debug("Normalized new Geometry is valid? " + newGeom.isValid()); // maybe simplify? needs tolerance param // double tolerance = 1d; // TopologyPreservingSimplifier simplify = new TopologyPreservingSimplifier(newGeom); // simplify.setDistanceTolerance(tolerance); // newGeom = simplify.getResultGeometry(); ids = this.handleStrategy(fA, fB, newGeom, filterA, filterB, this.store, this.strategy); transaction.commit(); afterMerge(ids); } catch (Exception e) { transaction.rollback(); throw e; } finally { transaction.close(); } return ids; } /** * Handles the feature creation/update/deletion strategy. You may want to * override this in a subclass to handle workflow. * * @param featureA the feature that is about to be merged into * @param featureB the feature that is about to be merged to A * @param newGeom the new geometry that is the result of merging A and B * geometries * @param filterA filter to get at feature A * @param filterB filter to get at feature B * @param localStore the store we're working against * @param localStrategy the strategy in use * @return A list of FeatureIds is returned, one for each feature in the * order created. However, these might not be assigned until after a commit * has been performed. * * @throws Exception if an error occurs modifying the data source, * converting the geometry or an illegal argument was given */ protected List<FeatureId> handleStrategy(SimpleFeature featureA, SimpleFeature featureB, Geometry newGeom, Filter filterA, Filter filterB, SimpleFeatureStore localStore, String localStrategy) throws Exception { List<FeatureId> ids = new ArrayList(); String geomAttrName = localStore.getSchema().getGeometryDescriptor().getLocalName(); GeometryType type = localStore.getSchema().getGeometryDescriptor().getType(); GeometryTypeConverterFactory cf = new GeometryTypeConverterFactory(); Converter c = cf.createConverter(Geometry.class, localStore.getSchema().getGeometryDescriptor().getType().getBinding(), null); if (localStrategy.equalsIgnoreCase("replace")) { // update existing feature (A) geom, delete merge partner (B) featureA.setAttribute(geomAttrName, c.convert(newGeom, type.getBinding())); featureA = this.handleExtraData(featureA); Object[] attributevalues = featureA.getAttributes().toArray(new Object[featureA.getAttributeCount()]); AttributeDescriptor[] attributes = featureA.getFeatureType().getAttributeDescriptors() .toArray(new AttributeDescriptor[featureA.getAttributeCount()]); localStore.modifyFeatures(attributes, attributevalues, filterA); localStore.removeFeatures(filterB); ids.add(new FeatureIdImpl(this.fidA)); } else if (localStrategy.equalsIgnoreCase("new")) { // delete the source feature (A) and merge partner(B) // and create a new feature with the attributes of A but a new geom. localStore.removeFeatures(filterA); localStore.removeFeatures(filterB); SimpleFeature newFeat = DataUtilities.createFeature(featureA.getType(), DataUtilities.encodeFeature(featureA, false)); newFeat.setAttribute(geomAttrName, c.convert(newGeom, type.getBinding())); List<SimpleFeature> newFeats = new ArrayList(); newFeats.add(newFeat); newFeats = this.handleExtraData(newFeats); ids = localStore.addFeatures(DataUtilities.collection(newFeats)); } else { throw new IllegalArgumentException(MessageFormat .format(getBundle().getString("viewer.mergefeaturesactionbean.13"), localStrategy)); } return ids; } /** * Called after the merge is completed and commit was performed. Provides a * hook for postprocessing. * @param ids The list of committed feature ids */ protected void afterMerge(List<FeatureId> ids) { } //<editor-fold defaultstate="collapsed" desc="getters en setters"> @Override public ActionBeanContext getContext() { return context; } @Override public void setContext(ActionBeanContext context) { this.context = context; } public Application getApplication() { return application; } public void setApplication(Application application) { this.application = application; } public ApplicationLayer getAppLayer() { return appLayer; } public void setAppLayer(ApplicationLayer appLayer) { this.appLayer = appLayer; } public String getStrategy() { return strategy; } public void setStrategy(String strategy) { this.strategy = strategy; } public String getFidA() { return fidA; } public void setFidA(String fidA) { this.fidA = fidA; } public String getFidB() { return fidB; } public void setFidB(String fidB) { this.fidB = fidB; } public int getMergeGapDist() { return mergeGapDist; } public void setMergeGapDist(int mergeGapDist) { this.mergeGapDist = mergeGapDist; } public String getExtraData() { return extraData; } public void setExtraData(String extraData) { this.extraData = extraData; } public Layer getLayer() { return layer; } //</editor-fold> }