/* Spatial Operations & Editing Tools for uDig
*
* Axios Engineering under a funding contract with:
* Diputación Foral de Gipuzkoa, Ordenación Territorial
*
* http://b5m.gipuzkoa.net
* http://www.axios.es
*
* (C) 2006, Diputación Foral de Gipuzkoa, Ordenación Territorial (DFG-OT).
* DFG-OT agrees to licence under Lesser General Public License (LGPL).
*
* You can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software
* Foundation; version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package es.axios.udig.spatialoperations.tasks;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureStore;
import org.geotools.data.Transaction;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.opengis.feature.IllegalAttributeException;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.GeometryDescriptor;
import org.opengis.filter.Id;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.IntersectionMatrix;
import com.vividsolutions.jts.geom.Point;
import es.axios.geotools.util.FeatureUtil;
import es.axios.geotools.util.GeoToolsUtils;
import es.axios.lib.geometry.util.GeometryUtil;
import es.axios.udig.spatialoperations.internal.i18n.Messages;
/**
* <p>
* Uses the feature's geometries of clipping collection to cut the features
* contained in the layer to clip.
* </p>
* <p>
* This process must:
* <ul>
* <li>split geometries saving its data.
* <li>delete geometries of features included in clip area
* <li>does the difference of that geometries that intersect the clipping
* geometry.
* <li>makes hole in feature's geometry if the clipping area is contained by
* clipping geometry .
* <ul>
* </p>
*
* @author Aritz Davila (www.axios.es)
* @author Mauricio Pazos (www.axios.es)
*/
final class ClipTask extends AbstractCommonTask implements IClipTask {
/**
* To create an instance must use
* {@link #createProcess(FeatureCollection, FeatureCollection, FeatureStore, CoordinateReferenceSystem, CoordinateReferenceSystem, CoordinateReferenceSystem)}
*/
private ClipTask() {
}
/**
* Creates the clip process implementation.
*
* @param usingFeatures
* @param clipSourceFeatures
* @param targetStore
* @param mapCrs
* @param usingCrs
* @param clipSourceCrs
* @param targetGeomAttrType
* @param isCreatingNewLayer
* @param layerToClip
* @param targetLayer
* @param targetLayerName
* @param clipSourceName
* @param clipSourceFeatureType
* @param targetCrs
* @return an instance of {@link ClipTask}
*/
public static IClipTask createProcess( final FeatureCollection<SimpleFeatureType, SimpleFeature> usingFeatures,
final FeatureCollection<SimpleFeatureType, SimpleFeature> clipSourceFeatures,
final SimpleFeatureStore targetStore,
final CoordinateReferenceSystem mapCrs,
final CoordinateReferenceSystem usingCrs,
final CoordinateReferenceSystem clipSourceCrs,
final boolean isCreatingNewLayer,
final String clipSourceName,
final String targetLayerName,
final CoordinateReferenceSystem targetCrs) {
assert usingFeatures != null;
assert clipSourceFeatures != null;
assert targetStore != null;
assert mapCrs != null;
assert usingCrs != null;
assert clipSourceCrs != null;
assert isCreatingNewLayer == true || isCreatingNewLayer == false;
assert clipSourceName != null;
assert targetLayerName != null;
assert targetCrs != null;
ClipTask task = new ClipTask();
task.usingFeatures = usingFeatures;
task.sourceFeatures = clipSourceFeatures;
task.targetStore = targetStore;
task.mapCrs = mapCrs;
task.usingCrs = usingCrs;
task.sourceCrs = clipSourceCrs;
task.isCreatingNewLayer = isCreatingNewLayer;
task.sourceName = clipSourceName;
task.targetLayerName = targetLayerName;
task.targetCrs = targetCrs;
return task;
}
@Override
protected void perform() throws SpatialOperationException {
FeatureIterator<SimpleFeature> iterSource = null;
FeatureIterator<SimpleFeature> iterUsing = null;
try {
// adds each feature to hole in the target layer
iterSource = this.sourceFeatures.features();
while (iterSource.hasNext()) {
// if a new result layer is required or was selected a target
// different of source layer,
// adds the feature without changes in target store before hole
// it.
SimpleFeature featureToHole = iterSource.next();
if (isCreatingNewLayer || (!this.sourceName.equals(this.targetLayerName))) {
featureToHole = createFeatureInStore(
this.targetStore, featureToHole,
getSourceLayerGeometry(), isCreatingNewLayer);
}
// creates the initial collection of features to hole, this
// collection
// could change if a hole feature is done (original feature
// replaced by its fragments).
this.featuresInProcessing = new LinkedList<String>();
this.featuresInProcessing.add(featureToHole.getID());
int i = 0;
while (i < this.featuresInProcessing.size()) {
String fidToHole = this.featuresInProcessing.get(i);
iterUsing = this.usingFeatures.features();
boolean numGeomToHoleModified = false;
while (iterUsing.hasNext() && !numGeomToHoleModified) {
SimpleFeature usingFeature = iterUsing.next();
numGeomToHoleModified = cutSourceWithUsingFeature(fidToHole, sourceCrs, usingFeature, usingCrs, mapCrs);
}
iterUsing.close();
i = (numGeomToHoleModified) ? 0 : i + 1;
}
}
} catch (Exception e) {
final String failedMsg = Messages.AbstractCommonTask_procces_failed;
makeException(e, failedMsg);
} finally {
if (iterUsing != null) {
iterUsing.close();
}
if (iterSource != null) {
iterSource.close();
}
}
}
/**
* Clips the feature using the geometries of the clipping feature. If the
* geometry collection of feature to clip was modified by the clip process
* this method will return true.
*
* @param fidToClip
* the feature to clip
* @param featureToClipCrs
* crs of feature to clip
* @param clippingFeature
* the feature used to clip the feature collection
* @param clippingCrs
* crs of clipping feature
* @param mapCrs
* map's crs
*
* @return true if the count of geometry to clip was modified, false in
* other case.
* @throws SpatialOperationException
*/
@Override
protected boolean cutSourceWithUsingFeature(final String fidToClip,
final CoordinateReferenceSystem featureToClipCrs,
final SimpleFeature clippingFeature,
final CoordinateReferenceSystem clippingCrs,
final CoordinateReferenceSystem mapCrs)
throws SpatialOperationException {
try {
Geometry clippingFeatureGeometry = (Geometry) clippingFeature.getDefaultGeometry();
boolean numGeomToClipModified = false;
final int numClippigGeometries = clippingFeatureGeometry.getNumGeometries();
for (int i = 0; (i < numClippigGeometries) && !numGeomToClipModified; i++) {
Geometry clippingGeometry = clippingFeatureGeometry.getGeometryN(i);
SimpleFeature featureToClip = findFeature(this.targetStore, fidToClip);
numGeomToClipModified = clipFeatureUsing(featureToClip, featureToClipCrs, clippingGeometry,
clippingCrs, mapCrs);
}
return numGeomToClipModified;
} catch (Exception e) {
final String emsg = e.getMessage();
LOGGER.severe(emsg);
throw makeException(e, emsg);
}
}
/**
* Clips the feature using the clipping geometry, and adds the change.
*
* @param featureToClip
* this feature could be the original or a partially processed
* feature (processed in previous step)
* @param featureToClipCrs
* @param clippingGeometry
* used to cut the featureToClip
* @param clippingCrs
* @param mapCrs
*
* @return true if the count of geometry to clip was modified by delete or
* split geometry operation
* @throws SpatialOperationException
*/
private boolean clipFeatureUsing( final SimpleFeature featureToClip,
final CoordinateReferenceSystem featureToClipCrs,
final Geometry clippingGeometry,
final CoordinateReferenceSystem clippingCrs,
final CoordinateReferenceSystem mapCrs) throws SpatialOperationException {
try {
Geometry featureGeometryToClip = (Geometry) featureToClip.getDefaultGeometry();
String fidToClip = featureToClip.getID();
Geometry clippingGeometryOnMap = GeoToolsUtils.reproject(clippingGeometry, clippingCrs, mapCrs);
// Iterates in the feature's geometries to clip then applies the
// delete,
// split or difference operation using the clipping geometry.
// The process is reinitialized if the geometry collection is
// modified
// by a delete or split operation.
boolean geomCollectionModified = false;
final int numGeomToClip = featureGeometryToClip.getNumGeometries();
for (int i = 0; (i < numGeomToClip) && !geomCollectionModified; i++) {
Geometry geom = featureGeometryToClip.getGeometryN(i);
Geometry currentGeomOnMap = GeoToolsUtils.reproject(geom, featureToClipCrs, mapCrs); // TODO
// could
// be
// extracted
// from
// loop
IntersectionMatrix geomRelation = clippingGeometryOnMap.relate(currentGeomOnMap);
// Does the examination of the geometry's position and applies
// the geometry operation. The partial result is saved in the
// target layer.
if (geomRelation.isContains()) {
transactionDelete(fidToClip, i);
geomCollectionModified = true;
} else if (geomRelation.isWithin()) {
Geometry diffGeomOnMap = computeGeometryDifference(currentGeomOnMap, clippingGeometryOnMap);
transactionHole(fidToClip, i, diffGeomOnMap);
} else if (splits(clippingGeometryOnMap, currentGeomOnMap, geomRelation)) {
GeometryCollection splitGeomOnMap = computeGeometrySplit(currentGeomOnMap, clippingGeometryOnMap,
featureGeometryToClip);
transactionSplit(fidToClip, i, splitGeomOnMap);
geomCollectionModified = true;
} else if (geomRelation.isIntersects()) {
Geometry diffGeomOnMap = computeGeometryDifference(currentGeomOnMap, clippingGeometryOnMap);
transactionDifference(fidToClip, i, diffGeomOnMap);
}
}
return geomCollectionModified;
} catch (Exception e) {
LOGGER.severe(e.getMessage());
throw makeException(e);
}
}
/**
* Makes the geometry difference between the feature's geometry and the
* clipping geometry. If the features type is geometry or multigeometry
* (geometry collection ) then modifies the feature's geometry. If the
* feature type has a simple geometry (point, polygon, line) then makes new
* geometries for each.
*
* @param featureCrs
* @param simpleGeometry
* @param clippingGeometryOnMapCrs
* @param mapCrs
* @return split geometry.
* @throws SpatialOperationException
*/
private GeometryCollection computeGeometrySplit(final Geometry simpleGeometry,
final Geometry clippingGeometryOnMapCrs,
final Geometry featureGeometryToClip)
throws SpatialOperationException {
try {
// does the difference
Geometry geoDiff = simpleGeometry.difference(clippingGeometryOnMapCrs);
assert geoDiff instanceof GeometryCollection;
GeometryCollection geoCollection = (GeometryCollection) geoDiff;
return geoCollection;
} catch (Exception e) {
final String msg = e.getMessage();
LOGGER.severe(msg);
throw makeException(e, msg);
}
}
/**
* Evaluates if clippingGeometry divides or not the feature's geometry. The
* params featureGeometry cannot be instance of Geometry Collection (Muli
* ....)
*
* @param clippingGeometry
* @param featureGeometry
* @param matrix
*
* @return true if clippingGeometry divides the feature's geometry, false in
* other case.
*/
private final boolean splits( final Geometry clippingGeometry,
final Geometry featureGeometry,
final IntersectionMatrix matrix) {
assert !(featureGeometry instanceof GeometryCollection);
if (featureGeometry instanceof Point) {
return false; // cannot split a point
}
// If the result of difference is a multiGeometry then the geometry is
// split.
Geometry geoDiff = featureGeometry.difference(clippingGeometry);
boolean isGeometryCollection = geoDiff instanceof GeometryCollection;
boolean intersects = matrix.isIntersects();
return isGeometryCollection && intersects;
}
/**
* Does a hole in featureToClip
*
* @param fidToClip
* @param posCurrentGeom
* @param diffGeomOnMap
*
* @throws SpatialOperationException
*/
private void transactionHole(final String fidToClip, final int posCurrentGeom, final Geometry diffGeomOnMap)
throws SpatialOperationException {
transactionDifference(fidToClip, posCurrentGeom, diffGeomOnMap);
}
/**
* Creates feature for each geometry fragment. The method take into account
* the following two case:
* <p>
* If the original feature (to clip feature) has simple geometry, creates
* new features for each new fragment. If it has Geometry collection, adds
* the fragments in the collection replacing the old geometry.
* </p>
*
* @param fidToClip
* @param posGeometry
* @param geometryFragmentsOnMap
*
* @throws SpatialOperationException
*/
private void transactionSplit( final String fidToClip,
final int posGeometry,
final GeometryCollection geometryFragmentsOnMap) throws SpatialOperationException {
// retrieve the feature partially modified and update its geometry
SimpleFeature featureToClip = findFeature(this.targetStore, fidToClip);
Geometry originalGeom = (Geometry) featureToClip.getDefaultGeometry();
if (originalGeom instanceof GeometryCollection) {
// updates the geometry presents in the result
GeometryCollection newGeometry = replaceSplitByFragments(originalGeom, posGeometry, geometryFragmentsOnMap);
Geometry geomProjected = projectOnTargetLayer(newGeometry);
modifyFeatureGeometryInStore(fidToClip, geomProjected, this.targetStore);
} else { // update the feature with Geometry collection with the new
// fragments
// if toClip layer and target are equals, inserts the fragments
// and deletes the original feature else only inserts the new
// fragments
final boolean modifyLayerToClip = this.sourceName.equals(this.targetLayerName);
// saves the attributes values in fragments before deletes the
// original feature
final boolean copyData = modifyLayerToClip || isCreatingNewLayer;
// has simple geometry
Set<SimpleFeature> featureList = createFeatureFragments(featureToClip, geometryFragmentsOnMap, copyData);
assert !featureList.isEmpty();
Set<String> newFidFragments = insertFeaturesInStore(featureList, this.targetStore);
this.featuresInProcessing.addAll(newFidFragments);
// if the values were saved the source feature must be deleted
String processedFID = featureToClip.getID();
deleteFeature(processedFID);
this.featuresInProcessing.remove(processedFID);
}
}
/**
* Replaces the split geometry in the original geometry by its fragments.
*
* @param originalGeometry
* @param posSplitGeometry
* @param geometryFragments
*
* @return a geometry collection with the new geometry fragments
*/
private GeometryCollection replaceSplitByFragments( final Geometry originalGeometry,
final int posSplitGeometry,
final GeometryCollection geometryFragments) {
final int numGeoms = originalGeometry.getNumGeometries();
// adds geometries contained in the original geometry without the split
// geometry
ArrayList<Geometry> newGeomArray = new ArrayList<Geometry>(numGeoms - 1);
for (int i = 0; i < numGeoms; i++) {
if (i != posSplitGeometry) {
Geometry currentGeom = originalGeometry.getGeometryN(i);
newGeomArray.add(currentGeom);
}
}
// adds the fragments to the final result
final int numFragments = geometryFragments.getNumGeometries();
for (int i = 0; i < numFragments; i++) {
Geometry g = geometryFragments.getGeometryN(i);
newGeomArray.add(g);
}
Class<GeometryCollection> expectedClass = (Class<GeometryCollection>) getClipLayerGeometry();
GeometryCollection result = GeometryUtil.adaptToGeomCollection(newGeomArray, expectedClass);
return result;
}
/**
* Creates the features for each geometry fragment.
*
* @param featurePrototype
* @param geomFragmentCollectionOnMap
* @param requireSaveData
* @return return the fragments projected on target layer
* @throws SpatialOperationException
*/
private final Set<SimpleFeature> createFeatureFragments(final SimpleFeature featurePrototype,
final GeometryCollection geomFragmentCollectionOnMap,
final boolean requireSaveData)
throws SpatialOperationException {
try {
Set<SimpleFeature> newFeatures = new HashSet<SimpleFeature>();
// creates a new feature for each geometry fragment
for (int i = 0; i < geomFragmentCollectionOnMap.getNumGeometries(); i++) {
Geometry geom = geomFragmentCollectionOnMap.getGeometryN(i);
Geometry geomOnTarget = projectOnTargetLayer(geom);
SimpleFeature feature = createFeature(featurePrototype, geomOnTarget, requireSaveData);
newFeatures.add(feature);
}
return newFeatures;
} catch (Exception e) {
String msg = e.getMessage();
LOGGER.severe(msg);
throw makeException(e, msg);
}
}
/**
* Creates a new feature using a prototype. The new feature will have the
* new geometry and all the attributes present in the prototype.
*
* @param featurePrototype
* @param newGeometry
* @param requiresCopyData
* @return a new feature
* @throws SpatialOperationException
*/
private final SimpleFeature createFeature( final SimpleFeature featurePrototype,
final Geometry newGeometry,
final boolean requiresCopyData) throws SpatialOperationException {
try {
// create the new feature and set the geometry fragment
SimpleFeature newFeature = DataUtilities.template(featurePrototype.getFeatureType());
// copies the data in the new feature
if (requiresCopyData) {
newFeature = FeatureUtil.copyAttributes(featurePrototype, newFeature);
}
newFeature.setDefaultGeometry(newGeometry);
return newFeature;
} catch (Exception e) {
final String msg = e.getMessage();
LOGGER.severe(msg);
throw makeException(e, msg);
}
}
/**
* Deletes the geometry's feature. The feature to clip could be deleted if
* the feature result has not geometry. In other case the method will delete
* only the clipped geometry and modifies the processed feature.
*
* @param fidToClip
* @param geomPosition
*
* @return the feature without the geometry
* @throws SpatialOperationException
*/
private void transactionDelete(final String fidToClip, final int geomPosition) throws SpatialOperationException {
try {
// retrieve the feature partially modified and update its geometry
SimpleFeature featureToClip = findFeature(this.targetStore, fidToClip);
Geometry originalGeom = (Geometry) featureToClip.getDefaultGeometry();
Geometry deleteGeom = computeGeometryDelete(geomPosition, originalGeom);
featureToClip.setDefaultGeometry(deleteGeom);
// updates the store with the result feature or deletes it, if the
// result feature has not any geometries
Geometry resultPorjected = projectOnTargetLayer(deleteGeom);
if (resultPorjected.getNumGeometries() == 0) {
deleteFeature(fidToClip);
this.featuresInProcessing.remove(fidToClip);
} else {
modifyFeatureGeometryInStore(fidToClip, resultPorjected, this.targetStore);
}
} catch (IllegalAttributeException e) {
final String msg = e.getMessage();
LOGGER.severe(msg);
throw makeException(e, msg);
}
}
/**
* Removes the geometry from the feature's geometries.
*
* @param geomToDelete
* a simple geometry
* @param originalGeometry
* feature's geometry
* @return a geometry collection without the geometry to clip
*/
private Geometry computeGeometryDelete(final int positionOfGeomToDelete, final Geometry originalGeometry) {
final int numOriginalGeom = originalGeometry.getNumGeometries();
// create a new geometry array without the geometry to delete
final int size = numOriginalGeom - 1;
ArrayList<Geometry> newGeomArray = new ArrayList<Geometry>(size);
for (int i = 0; i < numOriginalGeom; i++) {
Geometry currentGeom = originalGeometry.getGeometryN(i);
if (i != positionOfGeomToDelete) {
newGeomArray.add(currentGeom);
}
}
GeometryFactory geomFactory = originalGeometry.getFactory();
Geometry result = makeCompatibleGeometry(newGeomArray, geomFactory, originalGeometry.getClass());
return result;
}
/**
* Get the geometry of the clip layer.
*
* @param layer
* @return
*/
private Class<? extends Geometry> getClipLayerGeometry() {
GeometryDescriptor attr = sourceFeatures.getSchema().getGeometryDescriptor();
Class<? extends Geometry> result = (Class<? extends Geometry>) attr.getType().getBinding();
return result;
}
/**
* Deletes the feature if the target layer is equal to the layer to clip
*
* @param fid
*/
private void deleteFeature(final String fid) {
FeatureStore<SimpleFeatureType, SimpleFeature> store = getTargetStore();
Transaction transaction = targetStore.getTransaction();
try {
// Deletes only if the target is equal to the layer to clip
Id filter = getFilterId(fid);
store.removeFeatures(filter);
transaction.commit();
} catch (IOException e) {
try {
transaction.rollback();
} catch (IOException e1) {
e1.printStackTrace();
}
final String msg = Messages.ClipProcess_failed_deleting;
LOGGER.severe(msg);
throw (RuntimeException) new RuntimeException(msg).initCause(e);
} finally {
try {
transaction.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @return the FeatureStore of target layer
*/
private FeatureStore<SimpleFeatureType, SimpleFeature> getTargetStore() {
assert this.targetStore != null;
return this.targetStore;
}
}