/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2014, Geomatys
*
* 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.geotoolkit.referencing;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.operation.distance.DistanceOp;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.TreeMap;
import javax.vecmath.Vector2d;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Static;
import org.geotoolkit.display2d.GO2Utilities;
import org.geotoolkit.display2d.primitive.jts.JTSLineIterator;
import org.geotoolkit.display2d.style.j2d.PathWalker;
import org.geotoolkit.gml.xml.MultiGeometry;
import org.geotoolkit.math.XMath;
/**
* Linear referencing utilities.
*
* @author Johann Sorel (Geomatys)
*/
public class LinearReferencing extends Static{
public static final class SegmentInfo{
public int startCoordIndex;
public double startDistance;
public double endDistance;
public double length;
public Coordinate[] segmentCoords;
public LineString geometry;
public Vector2d forward;
public Vector2d right;
public Coordinate getPoint(double distanceAlongLinear, double distancePerpendicular){
final Vector2d tempForward = new Vector2d();
final Vector2d tempPerpendicular = new Vector2d();
tempForward.scale(distanceAlongLinear, forward);
tempPerpendicular.scale(distancePerpendicular, right);
return new Coordinate(
segmentCoords[0].x+tempForward.x+tempPerpendicular.x,
segmentCoords[0].y+tempForward.y+tempPerpendicular.y);
}
}
/**
* Information about a point after its reprojection on a linear object.
*/
public static final class ProjectedPoint {
/** Source point, before projection. */
public Point reference;
/**
* Index of the segment in source segment array used for projection. Be
* careful, this information remains valid only while you keep source
* segment array unchanged.
*/
public int segmentIndex = -1;
/** Segment on which the point has been projected. */
public SegmentInfo segment;
/** Coordinate of the point after projection. */
public Coordinate projected;
/**
* Distance from the start of the origin linear object (not just current segment)
* and the point after projection on this linear.
*/
public double distanceAlongLinear;
}
/**
* Compute geographic / projected position from linear information.
*
* @param geom Reference linear. If it's not a {@link LineString}, it will be
* converted as specified by {@link #asLineString(com.vividsolutions.jts.geom.Geometry) } method.
* @param reference A point on reference linear.
* @param distanceAlongLinear A distance (can be negative to rewind on linear)
* from reference point along input linear.
* @param distancePerpendicular distance from the linear (perpendicularly to it)
* for output point.
* @return position Found point, whose projection is thee same as input linear.
*/
public static Point computeCoordinate(Geometry geom, Point reference,
double distanceAlongLinear, double distancePerpendicular){
ArgumentChecks.ensureNonNull("linear", geom);
ArgumentChecks.ensureNonNull("reference", reference);
return computeCoordinate(buildSegments(asLineString(geom)), reference, distanceAlongLinear, distancePerpendicular);
}
/**
* Compute geographic / projected position from linear information.
*
* @param segments Reference linear, cut as a succession of segments.
* @param reference A point on reference linear.
* @param distanceAlongLinear A distance (can be negative to rewind on linear)
* from reference point along input linear.
* @param distancePerpendicular distance from the linear (perpendicularly to it)
* for output point.
* @return A geographic or projected (depends on input linear CRS) point defined
* by input parameters.
*/
public static Point computeCoordinate(final SegmentInfo[] segments, Point reference,
double distanceAlongLinear, double distancePerpendicular) {
ArgumentChecks.ensureNonNull("linear", segments);
ArgumentChecks.ensureNonNull("reference", reference);
//project reference
final ProjectedPoint projection = projectReference(segments, reference);
//find segment at given distance
final double distanceFinal = projection.distanceAlongLinear + distanceAlongLinear;
final SegmentInfo segment = getSegment(segments, distanceFinal);
final Point pt = GO2Utilities.JTS_FACTORY.createPoint(segment.getPoint(
distanceFinal-segment.startDistance, distancePerpendicular));
pt.setSRID(segment.geometry.getSRID());
pt.setUserData(segment.geometry.getUserData());
return pt;
}
/**
*
* @param geom linear geometry
* @param references positions
* @param position
* @return Entry : Key = index of the closest reference point
* Value = distance along the linear
*/
public static Entry<Integer, Double> computeRelative(Geometry geom, Point[] references, Point position) {
ArgumentChecks.ensureNonNull("linear", geom);
final LineString linear = asLineString(geom);
final SegmentInfo[] segments = buildSegments(linear);
return computeRelative(segments, references, position);
}
/**
*
* @param segments The list of segments which compose source linear.
*
* @param references positions
* @param position
* @return Entry : Key = index of the closest reference point
* Value = distance along the linear
*/
public static Entry<Integer, Double> computeRelative(final SegmentInfo[] segments, Point[] references, Point position){
ArgumentChecks.ensureNonNull("linear", segments);
ArgumentChecks.ensureNonNull("position", position);
ArgumentChecks.ensureNonNull("references", references);
ArgumentChecks.ensurePositive("references", references.length);
//project target
final ProjectedPoint positionProj = projectReference(segments, position);
//project references and find nearest
double distanceAlongLinear = Double.MAX_VALUE;
int index = 0;
for(int i=0;i<references.length;i++){
final ProjectedPoint projection = projectReference(segments, references[i]);
final double candidateDistance = positionProj.distanceAlongLinear-projection.distanceAlongLinear;
if(Math.abs(candidateDistance) < Math.abs(distanceAlongLinear)){
index = i;
distanceAlongLinear = candidateDistance;
}
}
return new AbstractMap.SimpleImmutableEntry<>(index, distanceAlongLinear);
}
/**
* Project a point on the linear and obtain all related information.
*
* @param segments linear segments
* @param reference position
* @return ProjectedReference Never null.
*/
public static ProjectedPoint projectReference(SegmentInfo[] segments, Point reference){
final ProjectedPoint projection = new ProjectedPoint();
projection.reference = reference;
//find the nearest segment
double minDistance = Double.MAX_VALUE;
SegmentInfo segment;
for (int i = 0 ; i < segments.length ; i++) {
segment = segments[i];
final Coordinate[] candidateNearests = DistanceOp.nearestPoints(segment.geometry, reference);
final double candidateDistance = candidateNearests[0].distance(candidateNearests[1]);
if(candidateDistance<minDistance){
minDistance = candidateDistance;
projection.projected = candidateNearests[0];
projection.segment = segment;
projection.segmentIndex = i;
projection.distanceAlongLinear = segment.startDistance +
segment.segmentCoords[0].distance(projection.projected);
}
}
return projection;
}
/**
* Project the geometry on the segments.
* The result can be a Point/MultiPoint/LineString/MultiLinestring over the segments.
*
* @param segments
* @param geometry
* @return LineString
*/
public static Geometry project(SegmentInfo[] segments, Geometry geometry){
ArgumentChecks.ensureNonNull("geometry", geometry);
Geometry geom;
if(geometry instanceof MultiGeometry){
final int nbGeom = geometry.getNumGeometries();
final List<Geometry> geometries = new ArrayList<>();
for(int i=0;i<nbGeom;i++){
final Geometry geometryN = geometry.getGeometryN(i);
geometries.add(project(segments, geometryN));
}
geom = GO2Utilities.JTS_FACTORY.buildGeometry(geometries);
}else if(geometry instanceof Point){
final ProjectedPoint pf = projectReference(segments, (Point) geometry);
geom = GO2Utilities.JTS_FACTORY.createPoint(pf.projected);
}else if(geometry instanceof LineString || geometry instanceof Polygon){
final Coordinate[] coordinates = geometry.getCoordinates();
final Point pt = GO2Utilities.JTS_FACTORY.createPoint(coordinates[0]);
final TreeMap<Double,Coordinate> map = new TreeMap<>();
for(Coordinate crd : coordinates){
pt.getCoordinate().setCoordinate(crd);
final ProjectedPoint pf = projectReference(segments, pt);
map.put(pf.distanceAlongLinear, pf.projected);
}
geom = GO2Utilities.JTS_FACTORY.createLineString(map.values().toArray(new Coordinate[0]));
}else{
throw new IllegalArgumentException("Unsupported geometry type : "+geometry);
}
geom.setSRID(geometry.getSRID());
geom.setUserData(geometry.getUserData());
return geom;
}
/**
* Split the geometry in a serie of line strings.
*
* @param linear geometry
* @return SegmentInfo[] linear segments
*/
public static SegmentInfo[] buildSegments(final LineString linear){
final Coordinate[] coords = linear.getCoordinates();
final SegmentInfo[] segments = new SegmentInfo[coords.length-1];
final int srid = linear.getSRID();
final Object userData = linear.getUserData();
double cumulativeDistance = 0;
for (int i = 0; i < coords.length - 1; i++) {
final SegmentInfo segment = new SegmentInfo();
segment.startCoordIndex = i;
segment.segmentCoords = new Coordinate[]{coords[i],coords[i+1]};
segment.geometry = GO2Utilities.JTS_FACTORY.createLineString(segment.segmentCoords);
segment.geometry.setSRID(srid);
segment.geometry.setUserData(userData);
segment.length = segment.segmentCoords[0].distance(segment.segmentCoords[1]);
segment.startDistance = cumulativeDistance;
cumulativeDistance += segment.length;
segment.endDistance = cumulativeDistance;
//calculate direction vectors
segment.forward = new Vector2d(
segment.segmentCoords[1].x-segment.segmentCoords[0].x,
segment.segmentCoords[1].y-segment.segmentCoords[0].y);
segment.forward.normalize();
segment.right = new Vector2d(segment.forward.y,-segment.forward.x);
segments[i] = segment;
}
return segments;
}
/**
* Find nearest segment to given distance.
*
* @param segments
* @param distance
* @return SegmentInfo
*/
public static SegmentInfo getSegment(SegmentInfo[] segments, double distance){
SegmentInfo segment = segments[0];
for(int i=1;i<segments.length;i++){
if(segments[i].startDistance < distance){
segment = segments[i];
}else{
break;
}
}
return segment;
}
/**
* Cast or convert a given geometry into a {@link LineString}. If input geometry
* is :
* - A linear, it's returned as is.
* - A multipoint, a line composed of all points, in their order in the collection,
* is returned.
* - A geometry collections (other than multipoint), we will convert its first geometry.
* - A polygon, its exterior ring is used as line string.
* - A point, we return a line string which is a segment whose start and end points
* are one and the same.
*
* Otherwise a null value is returned.
*
* @param candidate The geometry to convert. If null, a null value is returned.
* @return The resulting linear, or null.
*/
public static LineString asLineString(Geometry candidate) {
LineString linear = null;
if (candidate instanceof LineString) {
linear = (LineString) candidate;
} else if (candidate instanceof Polygon) {
linear = ((Polygon)candidate).getExteriorRing();
} else if (candidate instanceof Point) {
Coordinate coordinate = candidate.getCoordinate();
return GO2Utilities.JTS_FACTORY.createLineString(new Coordinate[]{coordinate, coordinate});
} else if (candidate instanceof MultiPoint) {
return GO2Utilities.JTS_FACTORY.createLineString(((candidate).getCoordinates()));
} else if (candidate instanceof GeometryCollection) {
final GeometryCollection gc = (GeometryCollection) candidate;
final int nb = gc.getNumGeometries();
if(nb>0){
linear = asLineString(gc.getGeometryN(0));
}
}
return linear;
}
public static LineString cut(LineString linear, double distanceDebut, double distanceFin){
//ensure we don't go out of the linear
final double linearLength = linear.getLength();
distanceDebut = XMath.clamp(distanceDebut, 0, linearLength);
distanceFin = XMath.clamp(distanceFin, 0, linearLength);
//cut linear at given distances
final PathIterator ite = new JTSLineIterator(linear, null);
final PathWalker walker = new PathWalker(ite);
walker.walk((float)distanceDebut);
float remain = (float) (distanceFin-distanceDebut);
final List<Coordinate> structureCoords = new ArrayList<>();
Point2D point = walker.getPosition(null);
structureCoords.add(new Coordinate(point.getX(), point.getY()));
while(!walker.isFinished() && remain>0){
final float advance = Math.min(walker.getSegmentLengthRemaining(), remain);
remain -= advance;
walker.walk(advance);
point = walker.getPosition(point);
structureCoords.add(new Coordinate(point.getX(), point.getY()));
}
if(structureCoords.size()==1){
//we need at least 2 points
structureCoords.add(new Coordinate(structureCoords.get(0)));
}
final LineString geom = GO2Utilities.JTS_FACTORY.createLineString(structureCoords.toArray(new Coordinate[structureCoords.size()]));
geom.setSRID(linear.getSRID());
geom.setUserData(linear.getUserData());
return geom;
}
}