WorkflowUtil.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.ibis.util;

import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.operation.overlay.snap.GeometrySnapper;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
import javax.persistence.EntityManager;
import nl.b3p.viewer.config.app.Application;
import nl.b3p.viewer.config.app.ApplicationLayer;
import nl.b3p.viewer.config.services.Layer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.feature.collection.AbstractFeatureVisitor;
import org.geotools.geometry.jts.JTSFactoryFinder;
import org.opengis.feature.Feature;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;

/**
 * Utility method that come in handy hadling workflow.
 *
 * @author mprins
 */
public class WorkflowUtil implements IbisConstants {

    private static final Log log = LogFactory.getLog(WorkflowUtil.class);

    /**
     * private constructor for utility class.
     */
    private WorkflowUtil() {
    }

    /**
     * Update the geometry of the TERREIN. Must be called after the kavels
     * transaction. Voor definitief terrein definitief kavels gebruiken, voor
     * bewerkt terrein definitief en bewerkt kavel gebruiken.
     * <p>
     * Als een terrein "per ongeluk" is afgevoerd moet het terugkomen mits kavel definitief is
     * De meest recente terrein versie - per definitie (workflow) is dat het "afgevoerde" - moet dan definitief worden.
     *
     * @param terreinID   feature id van het terrein
     * @param layer       kavels layer
     * @param kavelStatus workflow status van te gebruiken kavels
     * @param application flamingo applicatie
     * @param em          persistence manager
     */
    public static void updateTerreinGeometry(Integer terreinID, Layer layer, WorkflowStatus kavelStatus, Application application, EntityManager em) {
        log.debug("Updating terrein geometry for " + terreinID);
        SimpleFeatureStore terreinStore = null;
        SimpleFeatureStore kavelStore = null;
        FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();
        Transaction terreinTransaction = new DefaultTransaction("edit-terrein-geom");
        Transaction kavelTransaction = new DefaultTransaction("get-kavel-geom");
        try {
            // determine whichs kavels to use for calcutating new geometry
            Filter kavelFilter = Filter.EXCLUDE;
            switch (kavelStatus) {
                case bewerkt:
                    // find all "definitief" and "bewerkt" kavel for terreinID
                    kavelFilter = ff.and(
                            ff.equals(ff.property(KAVEL_TERREIN_ID_FIELDNAME), ff.literal(terreinID)),
                            ff.or(
                                    ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.bewerkt.toString()), false),
                                    ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.definitief.toString()), false)
                            ));
                    break;
                case archief:
                case afgevoerd:
                case definitief:
                    // find all "definitief" kavel for terreinID
                    kavelFilter = ff.and(
                            ff.equals(ff.property(KAVEL_TERREIN_ID_FIELDNAME), ff.literal(terreinID)),
                            ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.definitief.toString()), false)
                    );
                    break;
                default:
                // do nothing / should not happen
            }
            log.debug("Looking for kavel(s) with filter: " + kavelFilter);

            kavelStore = (SimpleFeatureStore) layer.getFeatureType().openGeoToolsFeatureSource();
            kavelStore.setTransaction(kavelTransaction);
            SimpleFeatureCollection kavels = kavelStore.getFeatures(kavelFilter);

            // dissolve all kavel geometries
            final ArrayList<Geometry> kavelGeoms = new ArrayList();
            kavels.accepts(new AbstractFeatureVisitor() {
                @Override
                public void visit(Feature feature) {
                    Geometry geom = (Geometry) feature.getDefaultGeometryProperty().getValue();
                    if (geom != null) {
                        kavelGeoms.add(geom);
                    }
                }
            }, null);
            log.debug("Kavels found for this terrein: " + kavelGeoms.size());

            Geometry newTerreinGeom;
            if (kavelGeoms.size() == 1) {
                // maar 1 (vlak) geom gevonden, dus geom overzetten naar terrein
                newTerreinGeom = kavelGeoms.get(0);
            } else {
                GeometryFactory factory = JTSFactoryFinder.getGeometryFactory(null);
                GeometryCollection geometryCollection = (GeometryCollection) factory.buildGeometry(kavelGeoms);
                newTerreinGeom = geometryCollection.union();
            }
            // buffer + en -0.001m (because rijksdriehoek) om interne slivers in terrein op te lossen,
            // zie https://github.com/B3Partners/flamingo-ibis/issues/64
            log.debug("terrein geom : " + newTerreinGeom);
            newTerreinGeom = newTerreinGeom.buffer(.001, 1);
            log.trace("buffer+.001   : " + newTerreinGeom);
            newTerreinGeom = newTerreinGeom.buffer(-.001, 1);
            log.trace("buffer-.001   : " + newTerreinGeom);
            // en ook nog een snap-to-self met een centimeter tolerantie
            newTerreinGeom = GeometrySnapper.snapToSelf(newTerreinGeom, .01, true);
            log.debug("snapped+clean: " + newTerreinGeom);

            if (!newTerreinGeom.getGeometryType().equalsIgnoreCase("MultiPolygon")) {
                GeometryFactory f = JTSFactoryFinder.getGeometryFactory();
                newTerreinGeom = f.createMultiPolygon(new Polygon[]{(Polygon) newTerreinGeom});
            }

            // find terrein appLayer
            ApplicationLayer terreinAppLyr = null;
            List<ApplicationLayer> lyrs = application.loadTreeCache(em).getApplicationLayers();
            for (ListIterator<ApplicationLayer> it = lyrs.listIterator(); it.hasNext();) {
                terreinAppLyr = it.next();
                if (terreinAppLyr.getLayerName().equalsIgnoreCase(TERREIN_LAYER_NAME)) {
                    break;
                }
            }
            Layer l = terreinAppLyr.getService().getLayer(TERREIN_LAYER_NAME, em);
            terreinStore = (SimpleFeatureStore) l.getFeatureType().openGeoToolsFeatureSource();
            terreinStore.setTransaction(terreinTransaction);

            // determine which terrein to update
            Filter terreinFilter = Filter.EXCLUDE;
            switch (kavelStatus) {
                case bewerkt:
                    terreinFilter = ff.and(
                            ff.equals(ff.property(ID_FIELDNAME), ff.literal(terreinID)),
                            ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.bewerkt.name()), false)
                    );
                    break;
                case archief:
                case afgevoerd:
                case definitief:
                    terreinFilter = ff.and(
                            ff.equals(ff.property(ID_FIELDNAME), ff.literal(terreinID)),
                            ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.definitief.name()), false)
                    );
                    break;
                default:
                // won't do anything
            }

            if (kavelStatus.equals(WorkflowStatus.definitief)) {
                // check of terrein "per ongeluk" afgevoerd, maar kavel definitief
                final Filter perOngelukFilter = ff.and(
                        ff.equals(ff.property(ID_FIELDNAME), ff.literal(terreinID)),
                        ff.equal(ff.property(WORKFLOW_FIELDNAME), ff.literal(WorkflowStatus.afgevoerd.name()), false)
                );
                log.debug("Herstel 'per ongeluk' 'afgevoerd' terrein naar 'definitief' met filter: " + terreinFilter);
                terreinStore.modifyFeatures(WORKFLOW_FIELDNAME, WorkflowStatus.definitief.name(), perOngelukFilter);
            }

            // update terrein with new geom
            log.debug("Update terrein geom for kavels filtered by: " + terreinFilter);
            String geomAttrName = terreinStore.getSchema().getGeometryDescriptor().getLocalName();
            terreinStore.modifyFeatures(geomAttrName, newTerreinGeom, terreinFilter);
            terreinTransaction.commit();
            log.debug("Done updating terrein geometry for " + terreinFilter + " with new geom: " + newTerreinGeom + " on attribute " + geomAttrName);
        } catch (Exception e) {
            log.error(String.format("Update van terrein geometrie %s is mislukt", terreinID), e);
        } finally {
            try {
                terreinTransaction.close();
            } catch (IOException io) {
                log.warn("Error closing terrein transaction", io);
            }
            if (terreinStore != null) {
                terreinStore.getDataStore().dispose();
            }
            try {
                kavelTransaction.close();
            } catch (IOException io) {
                log.warn("Error closing kavel transaction", io);
            }
            if (kavelStore != null) {
                kavelStore.getDataStore().dispose();
            }
        }
    }
}