/* 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.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import org.geotools.data.FeatureStore;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.FeatureCollection;
import org.geotools.feature.FeatureIterator;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Id;
import org.opengis.filter.identity.FeatureId;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Geometry;
import es.axios.geotools.util.FeatureUtil;
import es.axios.geotools.util.GeoToolsUtils;
/**
* Spatial Join Process
* <p>
* Generates a new layer with the features of layer A and B which fulfill with
* the spatial relation.
* </p>
* <p>
* <b> select A.*, B.* from A join B on A.Geometry <Relation> B.Geometry </b>
* <p>
* <p>
*
* <pre>
* Relation is one of: <b>intersect</b>,
* <b>overlaps</b>,
* <b>contains</b>,
* <b>covers</b>,
* <b>is-cover-by</b>,
* <b>crosses</b>,
* <b>disjoint</b>,
* <b>equals</b>,
* <b>overlap</b>,
* <b>within</b>,
* <b>is-within-distance</b>.
* <b>touches</b>,
*
* </pre>
*
* The A source is only used as reference to the spatial relation, the B
* sources' features that accomplish the relation will be copied in the result
* without changes in its geometries.
* </p>
* <p>
* </p>
* Note: the alfanumeric data manipulation is not solved in this algorithm it
* require a mapping between the source and target. This mapping should be a new
* parameter. </p>
*
* @author Mauricio Pazos (www.axios.es)
* @author Aritz Davila (www.axios.es)
* @since 1.1.0
*/
final class SpatialJoinTask extends AbstractSpatialOperationTask<Object> implements ISpatialJoinTask<Object> {
private static final Logger LOGGER = Logger.getLogger(SpatialJoinTask.class.getName());
/**
* two mode to execute the join task: copy into target layer or select into
* source layer
*/
private enum Mode {
copy, selection
};
private final FeatureCollection<SimpleFeatureType, SimpleFeature> firstSource;
private final FeatureCollection<SimpleFeatureType, SimpleFeature> referenceFeatures;
private final CoordinateReferenceSystem firstSourceCrs;
private final CoordinateReferenceSystem secondSourceCrs;
private final CoordinateReferenceSystem mapCrs;
private CoordinateReferenceSystem targetCrs;
private SpatialRelation spatialRelation = null;
private Double distance = null;
private Mode processMode = null;
private Set<FeatureId> featureIds = new HashSet<FeatureId>();
/**
* new instance of SpatialJoinGeometryProcess
*
* @param firstSource
* @param secondSource
* @param crs
* coordinate reference used to do the spatial relation
* @param target
* @throws SpatialDataProcessException
*/
protected SpatialJoinTask( final FeatureCollection<SimpleFeatureType, SimpleFeature> firstSource,
final FeatureCollection<SimpleFeatureType, SimpleFeature> secondSource,
final CoordinateReferenceSystem crs,
final SimpleFeatureStore target,
final CoordinateReferenceSystem sourceCrs,
final CoordinateReferenceSystem secondCrs,
final CoordinateReferenceSystem targetCrs) throws SpatialOperationException {
assert firstSource != null : "firstSource cannot be null"; //$NON-NLS-1$
assert secondSource != null : "secondSource cannot be null"; //$NON-NLS-1$
assert target != null : "target cannot be null"; //$NON-NLS-1$
assert crs != null : "crs cannot be null"; //$NON-NLS-1$
this.firstSource = firstSource;
this.referenceFeatures = secondSource;
this.targetStore = target;
this.mapCrs = crs;
// initialize the coordinate reference of sources
this.firstSourceCrs = sourceCrs;
this.secondSourceCrs = secondCrs;
this.targetCrs = targetCrs;
assert this.firstSourceCrs != null;
assert this.secondSourceCrs != null;
assert this.targetCrs != null;
}
/**
* new instance of SpatialJoinGeometryProcess
*
* @param firstSource
* @param secondSource
* @param mapCrs
* @param firstCRS
* @param secondCRS
* @throws SpatialDataProcessException
*/
protected SpatialJoinTask( final FeatureCollection<SimpleFeatureType, SimpleFeature> firstSource,
final FeatureCollection<SimpleFeatureType, SimpleFeature> secondSource,
final CoordinateReferenceSystem mapCrs,
final CoordinateReferenceSystem firstCRS,
final CoordinateReferenceSystem secondCRS) throws SpatialOperationException {
assert firstSource != null : "firstSource cannot be null"; //$NON-NLS-1$
assert secondSource != null : "secondSource cannot be null"; //$NON-NLS-1$
assert mapCrs != null : "mapCrs cannot be null"; //$NON-NLS-1$
assert firstCRS != null : "firstCRS cannot be null";//$NON-NLS-1$
assert secondCRS != null : "secondCRS cannot be null";//$NON-NLS-1$
this.firstSource = firstSource;
this.referenceFeatures = secondSource;
this.mapCrs = mapCrs;
// initialize the coordinate reference of sources
this.firstSourceCrs = firstCRS;
this.secondSourceCrs = secondCRS;
this.targetStore = null;
assert this.firstSourceCrs != null;
assert this.secondSourceCrs != null;
}
/**
* Creates an instance of SpatialJoinGeometryProcess
*
* @param firstSource
* @param secondSource
* @param spatialRelation
* @param mapCrs
* coordinate reference used to do the spatial relation
* @param target
* @return SpatialJoinGeometryProcess
* @throws SpatialDataProcessException
*/
public static SpatialJoinTask createProcess(final FeatureCollection<SimpleFeatureType, SimpleFeature> firstSource,
final FeatureCollection<SimpleFeatureType, SimpleFeature> secondSource,
final SpatialRelation spatialRelation,
final CoordinateReferenceSystem mapCrs,
final SimpleFeatureStore target,
final CoordinateReferenceSystem sourceCrs,
final CoordinateReferenceSystem secondCrs,
final CoordinateReferenceSystem targetCrs)
throws SpatialOperationException {
assert spatialRelation != null : "spatialRelation cannot be null"; //$NON-NLS-1$
SpatialJoinTask process = new SpatialJoinTask(firstSource, secondSource, mapCrs, target, sourceCrs, secondCrs,
targetCrs);
process.spatialRelation = spatialRelation;
process.processMode = Mode.copy;
return process;
}
/**
*
* @param firstSource
* @param secondSource
* @param spatialRelation
* @param mapCrs
* @param target
* @return
* @throws SpatialDataProcessException
*/
public static SpatialJoinTask createSelectionProcess( final FeatureCollection<SimpleFeatureType, SimpleFeature> firstSource,
final FeatureCollection<SimpleFeatureType, SimpleFeature> secondSource,
final SpatialRelation spatialRelation,
final CoordinateReferenceSystem mapCrs,
final CoordinateReferenceSystem firstCRS,
final CoordinateReferenceSystem secondCRS)
throws SpatialOperationException {
assert spatialRelation != null : "spatialRelation cannot be null"; //$NON-NLS-1$
SpatialJoinTask process = new SpatialJoinTask(firstSource, secondSource, mapCrs, firstCRS, secondCRS);
process.spatialRelation = spatialRelation;
process.processMode = Mode.selection;
return process;
}
/**
* Creates a process that add in target those features which are in the
* specified distance.
*
* @param firstSource
* @param secondSource
* @param distance
* @param crs
* coordinate reference used to do the spatial relation
* @param target
* @return SpatialJoinGeometryProcess
* @throws SpatialDataProcessException
*/
public static SpatialJoinTask createDistanceRelationProcess(final FeatureCollection<SimpleFeatureType, SimpleFeature> firstSource,
final FeatureCollection<SimpleFeatureType, SimpleFeature> secondSource,
final Double distance,
final CoordinateReferenceSystem crs,
final SimpleFeatureStore target,
final CoordinateReferenceSystem sourceCrs,
final CoordinateReferenceSystem secondCrs,
final CoordinateReferenceSystem targetCrs)
throws SpatialOperationException {
assert distance != null : "distance cannot be null"; //$NON-NLS-1$
SpatialJoinTask process = new SpatialJoinTask(firstSource, secondSource, crs, target, sourceCrs, secondCrs, targetCrs);
process.distance = distance;
return process;
}
@Override
public Object call() throws Exception {
perform();
Object result = null;
if (Mode.copy.equals(this.processMode)) {
result = getResult();
} else if (Mode.selection.equals(this.processMode)) {
result = getSelectionFilter();
} else {
assert false : "must set the process Mode"; //$NON-NLS-1$
}
return result;
}
/**
* Evaluates the spatial relation for each features present in sources and
* adds in the target those features which accomplish it.
*/
@Override
protected void perform() throws SpatialOperationException {
FeatureIterator<SimpleFeature> iterFirstSource = null;
try {
iterFirstSource = this.firstSource.features();
while (iterFirstSource.hasNext()) {
SimpleFeature featureInFirst = iterFirstSource.next();
Geometry firstGeom = (Geometry) featureInFirst.getDefaultGeometry();
boolean existRelation = false;
if (SpatialRelation.Disjoint.equals(this.spatialRelation)) {
existRelation = existDisjoint(firstGeom);
} else {
existRelation = existeRelation(this.spatialRelation, firstGeom);
}
if (existRelation) {
if (Mode.selection.equals(this.processMode)) {
addToFeatureList(featureInFirst);
} else if (Mode.copy.equals(this.processMode)) {
insertIntoStore(this.targetStore, featureInFirst);
}
}
}
} catch (Exception e) {
throw new SpatialOperationException(e);
} finally {
if (iterFirstSource != null) {
iterFirstSource.close();
}
}
}
/**
* Stores the feature Id
*
* @param featureInFirst
* The feature of the source layer.
*/
private void addToFeatureList(SimpleFeature featureInFirst) {
FeatureId fid = FILTER_FACTORY.featureId(featureInFirst.getID());
featureIds.add(fid);
}
/**
* Feature from source must fulfill geometry disjoint with all features of
* reference features.
*
* @param firstGeom
* @return true if the spatial relation is true, false in other case
* @throws SpatialOperationException
*/
private boolean existDisjoint(final Geometry firstGeom) throws SpatialOperationException {
FeatureIterator<SimpleFeature> iterSecondSource = null;
try {
iterSecondSource = this.referenceFeatures.features();
while (iterSecondSource.hasNext()) {
SimpleFeature featureInSecond = iterSecondSource.next();
Geometry secondGeom = (Geometry) featureInSecond.getDefaultGeometry();
if (!existGeometryRelation(SpatialRelation.Disjoint, firstGeom, this.firstSourceCrs, secondGeom,
this.secondSourceCrs, this.mapCrs)) {
return false;
}
}
return true;
} finally {
if (iterSecondSource != null) {
iterSecondSource.close();
}
}
}
/**
* Feature from source must fulfill geometry relation at least with one
* feature from reference features. This method do not considerate the
* disjoint spatial relation. To disjoint this task use the method
* {@link #existDisjoint(Geometry)}.
*
*
* @param relation
* @param firstGeom
*
* @return True if exist relation, false in other case
* @throws SpatialDataProcessException
*/
private boolean existeRelation(final SpatialRelation relation, final Geometry firstGeom)
throws SpatialOperationException {
assert !relation.equals(SpatialRelation.Disjoint) : "illegal argument: disjoint is not handed by this method"; //$NON-NLS-1$
FeatureIterator<SimpleFeature> iterSecondSource = null;
try {
iterSecondSource = this.referenceFeatures.features();
while (iterSecondSource.hasNext()) {
SimpleFeature featureInSecond = iterSecondSource.next();
Geometry referenceGeom = (Geometry) featureInSecond.getDefaultGeometry();
if (existGeometryRelation(relation, firstGeom, this.firstSourceCrs, referenceGeom,
this.secondSourceCrs, this.mapCrs)) {
return true;
}
}
return false;
} catch (Exception e) {
throw makeException(e);
} finally {
if (iterSecondSource != null) {
iterSecondSource.close();
}
}
}
/**
* Inserts a new feature using the geometry presents in the second feature.
* This method do not copies alphanumeric data to the target layer. TODO a
* mapping to specify what alphanumeric data, this process, must copy.
*
* @param target
* @param featureInFirst
*
* @throws SpatialDataProcessException
* if adds features fail
* @throws SpatialOperationException
*/
private void insertIntoStore( final FeatureStore<SimpleFeatureType, SimpleFeature> target,
final SimpleFeature featureInFirst) throws SpatialOperationException {
// creates the new features
SimpleFeature newSecond = createFeatureUsing(featureInFirst, this.firstSourceCrs, this.targetCrs);
insert(target, newSecond);
}
/**
* Creates the new feature projecting the featue's geometry on target.
*
* @param feature
* @param sourceCrs
* @param targetCrs
* @param geomOnTargetCrs
* @return Feature
* @throws SpatialDataProcessException
*/
private SimpleFeature createFeatureUsing( final SimpleFeature feature,
final CoordinateReferenceSystem sourceCrs,
final CoordinateReferenceSystem targetCrs)
throws SpatialOperationException {
try {
// projects the geometry on target
Geometry firstGeom = (Geometry) feature.getDefaultGeometry();
Geometry geomOnTargetCrs = GeoToolsUtils.reproject(firstGeom, sourceCrs, targetCrs);
// create the new feature using the projected geometry
final SimpleFeatureType targetType = this.targetStore.getSchema();
SimpleFeature newFeature = FeatureUtil.createFeatureUsing(feature, targetType, geomOnTargetCrs);
return newFeature;
} catch (Exception e) {
throw createException(e);
}
}
/**
* Evaluates the geometry relation between the first and second geometry,
* using the CRS specified.
*
* @param relation
* @param firstGeom
* @param firstCrs
* @param secondGeom
* @param secondCrs
* @param crs
* @return
* @throws SpatialOperationException
* @throws SpatialDataProcessException
*/
private boolean existGeometryRelation( final SpatialRelation relation,
final Geometry firstGeom,
final CoordinateReferenceSystem firstCrs,
final Geometry secondGeom,
final CoordinateReferenceSystem secondCrs,
final CoordinateReferenceSystem crs) throws SpatialOperationException {
try {
Geometry firstGeomOnCrs = GeoToolsUtils.reproject(firstGeom, firstCrs, crs);
Geometry secondGeomOnCrs = GeoToolsUtils.reproject(secondGeom, secondCrs, crs);
boolean exist = false;
switch (relation) {
case Intersects:
exist = firstGeomOnCrs.intersects(secondGeomOnCrs);
break;
case Disjoint: // inverse intersects
exist = firstGeomOnCrs.disjoint(secondGeomOnCrs);
break;
case Contains:
exist = firstGeomOnCrs.contains(secondGeomOnCrs);
break;
case Within: // inverse contains
exist = firstGeomOnCrs.within(secondGeomOnCrs);
break;
case IsCoverBy:
exist = firstGeomOnCrs.coveredBy(secondGeomOnCrs);
break;
case Covers: // inverse coveredBy
exist = firstGeomOnCrs.covers(secondGeomOnCrs);
break;
case Crosses:
// do overlaps when geometries dimension is the same and also is
// 2.
if ((firstGeomOnCrs.getDimension() == secondGeomOnCrs.getDimension())
&& firstGeomOnCrs.getDimension() == 2) {
exist = firstGeomOnCrs.overlaps(secondGeomOnCrs);
} else {
exist = firstGeomOnCrs.crosses(secondGeomOnCrs);
}
break;
case Equals:
exist = firstGeomOnCrs.equals(secondGeomOnCrs);
break;
case Overlaps:
exist = firstGeomOnCrs.overlaps(secondGeomOnCrs);
break;
case Touches:
exist = firstGeomOnCrs.touches(secondGeomOnCrs);
break;
case IsWithinDistance:
assert this.distance != null: "must set the distance for within distance relation"; //$NON-NLS-1$
exist = firstGeomOnCrs.isWithinDistance(secondGeomOnCrs, this.distance);
break;
default:
assert false : "unsupported spatial relation"; //$NON-NLS-1$
break;
}
return exist;
} catch (Exception e) {
throw makeException(e);
}
}
/**
* Returns the target with the join result.
*
* @return {@link FeatureStore} or {@link Id}
*/
@Override
protected Object getResult() {
Object result = null;
if (Mode.copy.equals(this.processMode)) {
result = this.targetStore;
} else if (Mode.selection.equals(this.processMode)) {
result = getSelectionFilter();
}
assert result != null : "spatial join without result !!"; //$NON-NLS-1$
return result;
}
/**
* @return the selected features
*/
protected Id getSelectionFilter() {
Id filter = null;
filter = FILTER_FACTORY.id(featureIds);
assert filter != null;
return filter;
}
}