/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2014, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; 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 org.geotools.process.spatialstatistics.transformation;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.feature.collection.SubFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.process.spatialstatistics.core.FeatureTypes;
import org.geotools.process.spatialstatistics.core.StatisticsVisitor.DoubleStrategy;
import org.geotools.process.spatialstatistics.core.StatisticsVisitor.StatisticsStrategy;
import org.geotools.process.spatialstatistics.core.StatisticsVisitorResult;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.Filter;
import org.opengis.filter.expression.Expression;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.Polygon;
/**
* Creates a flow map features using an origin-destination line features.
*
* @author Minpa Lee, MangoSystem
*
* @source $URL$
*/
public class FlowMapFeatureCollection extends GXTSimpleFeatureCollection {
protected static final Logger LOGGER = Logging.getLogger(FlowMapFeatureCollection.class);
private Expression odValue;
private Expression doValue;
private Double maxSize = null;
private double[] minMax = new double[4];
private SimpleFeatureType schema;
public FlowMapFeatureCollection(SimpleFeatureCollection delegate, Expression odValue,
Expression doValue, Double maxSize) {
super(delegate);
this.odValue = odValue;
this.doValue = doValue;
this.maxSize = maxSize;
this.calculateMinMax();
if (this.maxSize == null || this.maxSize == 0) {
ReferencedEnvelope bbox = delegate.getBounds();
this.maxSize = Math.min(bbox.getWidth(), bbox.getHeight()) / 20;
LOGGER.log(Level.WARNING, "The default maxSize is " + this.maxSize);
}
String typeName = delegate.getSchema().getTypeName();
this.schema = FeatureTypes.build(delegate.getSchema(), typeName, Polygon.class);
}
private void calculateMinMax() {
StatisticsStrategy odVisitor = new DoubleStrategy();
StatisticsStrategy doVisitor = new DoubleStrategy();
SimpleFeatureIterator featureIter = delegate.features();
try {
while (featureIter.hasNext()) {
SimpleFeature feature = featureIter.next();
Double value = odValue.evaluate(feature, Double.class);
if (value != null) {
odVisitor.add(value);
}
if (doValue != null) {
value = doValue.evaluate(feature, Double.class);
if (value != null) {
doVisitor.add(value);
}
}
}
} finally {
featureIter.close();
}
StatisticsVisitorResult ret = odVisitor.getResult();
this.minMax[0] = ret.getMinimum();
this.minMax[1] = ret.getMaximum();
ret = doVisitor.getResult();
this.minMax[2] = ret.getMinimum();
this.minMax[3] = ret.getMaximum();
}
@Override
public SimpleFeatureIterator features() {
return new BufferExpressionFeatureIterator(delegate.features(), getSchema(), odValue,
doValue, minMax, maxSize);
}
@Override
public SimpleFeatureType getSchema() {
return schema;
}
@Override
public SimpleFeatureCollection subCollection(Filter filter) {
if (filter == Filter.INCLUDE) {
return this;
}
return new SubFeatureCollection(this, filter);
}
@Override
public int size() {
if (doValue == null) {
return delegate.size();
} else {
return delegate.size() * 2;
}
}
static class BufferExpressionFeatureIterator implements SimpleFeatureIterator {
private SimpleFeatureIterator delegate;
private int index = 0;
private Expression odValue;
private Expression doValue;
private boolean bothSide = false;
private double[] minMax = new double[4];
private Double maxSize = null;
private int count = 0;
private SimpleFeatureBuilder builder;
private SimpleFeature next = null;
private SimpleFeature source = null;
private String typeName;
private ArrowBuilder arrowBuilder = new ArrowBuilder();
public BufferExpressionFeatureIterator(SimpleFeatureIterator delegate,
SimpleFeatureType schema, Expression odValue, Expression doValue, double[] minMax,
Double maxSize) {
this.delegate = delegate;
this.odValue = odValue;
this.doValue = doValue;
this.bothSide = doValue == null;
this.minMax = minMax;
this.maxSize = maxSize;
this.builder = new SimpleFeatureBuilder(schema);
this.typeName = schema.getTypeName();
}
public void close() {
delegate.close();
}
public boolean hasNext() {
while ((next == null && delegate.hasNext())
|| (next == null && !delegate.hasNext() && index > 0)) {
if (index == 0) {
source = delegate.next();
}
next = builder.buildFeature(buildID(typeName, ++count));
next.setAttributes(source.getAttributes());
Geometry line = (Geometry) source.getDefaultGeometry();
double transValue = 0;
if (index == 0) {
Double value = odValue.evaluate(source, Double.class);
if (value == null) {
value = minMax[0]; // minimum
}
transValue = (value - minMax[0]) / (minMax[1] - minMax[0]);
index = bothSide ? 0 : 1;
} else {
// flip line
line = line.reverse();
Double value = doValue.evaluate(source, Double.class);
if (value == null) {
value = minMax[2]; // minimum
}
transValue = (value - minMax[2]) / (minMax[3] - minMax[2]);
index = 0;
}
// create flow arrow
Geometry arrow = arrowBuilder.createArraw(line, transValue, maxSize, bothSide);
next.setDefaultGeometry(arrow);
builder.reset();
}
return next != null;
}
public SimpleFeature next() throws NoSuchElementException {
if (!hasNext()) {
throw new NoSuchElementException("hasNext() returned false!");
}
SimpleFeature result = next;
next = null;
return result;
}
}
/**
* Builds flow arrows from LineString geometry
*/
static class ArrowBuilder {
static final double OFFSET = 10;
public Geometry createArraw(Geometry geometry, double transValue, double maxRadius,
boolean bothSide) {
GeometryFactory gf = geometry.getFactory();
LineString lineString = (LineString) geometry.getGeometryN(0);
Coordinate from = lineString.getStartPoint().getCoordinate();
Coordinate to = lineString.getEndPoint().getCoordinate();
double angle = toDeg(Math.atan2(to.y - from.y, to.x - from.x)) - 180;
double radius = transValue * maxRadius;
List<Coordinate> coords = new ArrayList<Coordinate>();
coords.add(from);
if (bothSide) {
// right side
coords.add(createCoord(to, toRad(angle + OFFSET), radius));
coords.add(createCoord(to, toRad(angle + (OFFSET * 2)), radius));
}
// center
coords.add(to);
// left side
coords.add(createCoord(to, toRad(angle - (OFFSET * 2)), radius));
coords.add(createCoord(to, toRad(angle - OFFSET), radius));
// close rings
coords.add(coords.get(0));
// create polygon
Coordinate[] ring = coords.toArray(new Coordinate[coords.size()]);
return gf.createPolygon(gf.createLinearRing(ring), null);
}
private double toDeg(double radians) {
return radians * (180.0 / Math.PI);
}
private double toRad(double degree) {
return Math.PI / 180.0 * degree;
}
private Coordinate createCoord(Coordinate source, double radian, double radius) {
double dx = Math.cos(radian) * radius;
double dy = Math.sin(radian) * radius;
return new Coordinate(source.x + dx, source.y + dy);
}
}
}