/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-2008, 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.renderer.label;
import java.util.Arrays;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.LineString;
/**
* Allows to move a point cursor along the path of a LineString using a
* curvilinear coordinate system and either absolute distances (from the start
* of the line) or offsets relative to the current position, to return the
* absolute position of the cursor as a Point, and to get the orientation of the
* current segment.
*
* @author Andrea Aime
*
*
*
* @source $URL$
*/
public class LineStringCursor {
/**
* Tolerance used for angle comparisons
*/
static final double ONE_DEGREE = Math.PI / 180.0;
/**
* The LineString being walked
*/
LineString lineString;
/**
* The sequence making up the line string
*/
CoordinateSequence coords;
/**
* The current segment
*/
int segment;
/**
* The current positions's offset from the start of the current segment
*/
double offsetDistance;
/**
* All the segments lengths
*/
double[] segmentLenghts;
/**
* The distance from the start of the line string to the first point in the
* segment
*/
double[] segmentStartOrdinate;
/**
* A cache for the orientation of each segment (we use it a lot)
*/
double[] segmentAngles;
/**
* Builds a new cursor
*
* @param ls
*/
public LineStringCursor(LineString ls) {
this.lineString = ls;
coords = ls.getCoordinateSequence();
// reset (it's not really necessary, but still...)
segment = 0;
offsetDistance = 0.0;
// Allocate the length and ordinate caches
final int coordsCount = coords.size();
segmentLenghts = new double[coordsCount - 1];
segmentStartOrdinate = new double[coordsCount - 1];
segmentStartOrdinate[0] = 0;
// prepare the coordinates used for distance computation
Coordinate c1 = new Coordinate();
Coordinate c2 = new Coordinate();
c2.x = coords.getX(0);
c2.y = coords.getY(0);
for (int i = 1; i < coordsCount; i++) {
c1.x = c2.x;
c1.y = c2.y;
c2.x = coords.getX(i);
c2.y = coords.getY(i);
final double distance = c1.distance(c2);
segmentLenghts[i - 1] = distance;
if (i < coords.size() - 1)
segmentStartOrdinate[i] = segmentStartOrdinate[i - 1] + distance;
}
// fill up the segment angles cache with placeholders
segmentAngles = new double[segmentLenghts.length];
Arrays.fill(segmentAngles, Double.NaN);
}
/**
* Copy constructor
*
* @param cursor
*/
public LineStringCursor(LineStringCursor cursor) {
this.lineString = cursor.lineString;
this.coords = cursor.coords;
this.segment = cursor.segment;
this.offsetDistance = cursor.offsetDistance;
this.segmentLenghts = cursor.segmentLenghts;
this.segmentStartOrdinate = cursor.segmentStartOrdinate;
this.segmentAngles = cursor.segmentAngles;
}
/**
* Returns the line string length
*
* @return
*/
public double getLineStringLength() {
return segmentStartOrdinate[coords.size() - 2] + segmentLenghts[coords.size() - 2];
}
/**
* Moves the current position to the
*
* @param ordinate
*/
public void moveTo(double ordinate) {
double position = 0;
if (ordinate < 0) {
// before start
segment = 0;
offsetDistance = 0;
} else if (ordinate > getLineStringLength()) {
// after end
segment = segmentLenghts.length - 1;
offsetDistance = segmentLenghts[segment];
} else {
// find the segment and the offset within the segment
for (int i = 0; i < segmentLenghts.length; i++) {
double length = segmentLenghts[i];
if (ordinate < (length + position)) {
segment = i;
offsetDistance = ordinate - position;
break;
} else {
position += length;
}
}
}
}
/**
* Moves of the specified distance from the current position.
*
* @param offset
* @return true if it was possible to move to the desired offset, false if
* the movement stopped because the start or end of the LineString
* was reached
*/
public boolean moveRelative(double offset) {
if (offset == 0) {
return true;
} else if (offset > 0) {
// move forward until you get to the desired offset, or end up
// into the end of the line
while (offset > 0) {
if ((offsetDistance + offset) <= segmentLenghts[segment]) {
// move within the current segment and we're done
offsetDistance += offset;
return true;
} else if (segment == (segmentLenghts.length - 1)) {
// ops, reached the end of the linestring
offsetDistance = segmentLenghts[segment];
return false;
} else {
// move to the end of the current segment
offset = offset - (segmentLenghts[segment] - offsetDistance);
offsetDistance = 0;
segment++;
}
}
} else {
// move backwards until you get to the desired offset, or end up
// into the end of the line
while (offset < 0.0) {
if ((offsetDistance + offset) >= 0.0) {
// move within the current segment and we're done
offsetDistance += offset;
return true;
} else if (segment == 0) {
// ops, reached the start of the linestring
offsetDistance = 0;
return false;
} else {
// move to the end of the previous segment
offset = offset + offsetDistance;
segment--;
offsetDistance = segmentLenghts[segment];
}
}
}
throw new RuntimeException("You have stumbled into a software bug, "
+ "the code should never get here. Please report with a reproducable test case");
}
/**
* Returns the Point representing the current position along the LineString
*/
public Coordinate getCurrentPosition() {
return getCurrentPosition(new Coordinate());
}
/**
* Returns the Point representing the current position along the LineString
*/
public Coordinate getCurrentPosition(Coordinate c) {
c.setCoordinate(coords.getCoordinate(segment));
if (offsetDistance > 0) {
final double angle = getCurrentAngle();
c.x += offsetDistance * Math.cos(angle);
c.y += offsetDistance * Math.sin(angle);
}
return c;
}
public double getCurrentOrdinate() {
return segmentStartOrdinate[segment] + offsetDistance;
}
/**
* Returns the current segment direction as an angle expressed in radians
*
* @return
*/
public double getCurrentAngle() {
return getSegmentAngle(segment);
}
protected double getSegmentAngle(int segmentIdx) {
if (Double.isNaN(segmentAngles[segmentIdx])) {
double dx = (coords.getX(segmentIdx + 1) - coords.getX(segmentIdx));
double dy = (coords.getY(segmentIdx + 1) - coords.getY(segmentIdx));
segmentAngles[segmentIdx] = Math.atan2(dy, dx);
}
return segmentAngles[segmentIdx];
}
/**
* Returns the current segment direction as an angle expressed in radians
*
* @return
*/
public double getLabelOrientation() {
double dx = (coords.getX(segment + 1) - coords.getX(segment));
double dy = (coords.getY(segment + 1) - coords.getY(segment));
double slope = dy / dx;
double angle = Math.atan(slope);
// make sure we turn PI/2 into -PI/2, we don't want some labels looking straight up
// and some others straight down, when almost vertical they should all be orianted
// on the same side
if(Math.abs(angle - Math.PI / 2) < ONE_DEGREE) {
angle = -Math.PI / 2 + Math.abs(angle - Math.PI / 2);
}
return angle;
}
/**
* Returns the maximum angle change (in radians) between two subsequent
* segments between the specified curvilinear coordinates.
*
* @param startOrdinate
* @param endOrdinate
* @return
*/
public double getMaxAngleChange(double startOrdinate, double endOrdinate) {
if (startOrdinate > endOrdinate)
throw new IllegalArgumentException("Invalid arguments, endOrdinate < starOrdinate");
// compute the begin and end segments
LineStringCursor delegate = new LineStringCursor(this);
delegate.moveTo(startOrdinate);
int startSegment = delegate.segment;
delegate.moveTo(endOrdinate);
int endSegment = delegate.segment;
// everything inside the same segment
if (startSegment == endSegment)
return 0;
double maxDifference = 0;
double prevAngle = getSegmentAngle(startSegment);
for (int i = startSegment + 1; i <= endSegment; i++) {
double currAngle = getSegmentAngle(i);
double difference = Math.abs(currAngle - prevAngle);
if (difference > maxDifference)
maxDifference = difference;
prevAngle = currAngle;
}
return maxDifference;
}
/**
* Returns a line string cursor based on the opposite walking direction.
*
* @return
*/
public LineStringCursor reverse() {
return new LineStringCursor((LineString) lineString.reverse());
}
/**
* The linestrings wrapped by this cursor
*
* @return
*/
public LineString getLineString() {
return lineString;
}
/**
* Returns the linestring that starts and ends at the specified curvilinear
* coordinates.
*
* @param startOrdinate
* @param endOrdinate
* @return
*/
public LineString getSubLineString(double startOrdinate, double endOrdinate) {
LineStringCursor clone = new LineStringCursor(this);
clone.moveTo(startOrdinate);
int startSegment = clone.segment;
Coordinate start = clone.getCurrentPosition();
clone.moveTo(endOrdinate);
int endSegment = clone.segment;
Coordinate end = clone.getCurrentPosition();
Coordinate[] subCoords = new Coordinate[endSegment - startSegment + 2];
subCoords[0] = start;
for (int i = startSegment; i < endSegment; i++) {
subCoords[i - startSegment + 1] = coords.getCoordinate(i + 1);
}
subCoords[subCoords.length - 1] = end;
return lineString.getFactory().createLineString(subCoords);
}
}