/**
* OrbisGIS is a java GIS application dedicated to research in GIScience.
* OrbisGIS is developed by the GIS group of the DECIDE team of the
* Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
*
* The GIS group of the DECIDE team is located at :
*
* Laboratoire Lab-STICC – CNRS UMR 6285
* Equipe DECIDE
* UNIVERSITÉ DE BRETAGNE-SUD
* Institut Universitaire de Technologie de Vannes
* 8, Rue Montaigne - BP 561 56017 Vannes Cedex
*
* OrbisGIS is distributed under GPL 3 license.
*
* Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
* Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285)
*
* This file is part of OrbisGIS.
*
* OrbisGIS is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* OrbisGIS 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
*
* For more information, please consult: <http://www.orbisgis.org/>
* or contact directly:
* info_at_ orbisgis.org
*/
package org.orbisgis.coremap.renderer.se.common;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import java.awt.Shape;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.*;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
/**
* Provides utility methods to handle Shape instances.
* @author Maxence Laurent, Alexis Guéganno
*/
public final class ShapeHelper {
public static final double ONE_DEG_IN_RAD = Math.PI / 180.0;
private static final boolean ENABLE_QUAD = true;
private static final double FLATNESS = 1e-5;
private static final Logger LOGGER = LoggerFactory.getLogger(ShapeHelper.class);
private static final I18n I18N = I18nFactory.getI18n(ShapeHelper.class);
private ShapeHelper(){
}
/**
* Compute the perimeter of the shape
* @todo test and move
* @param area
* @return
*/
public static double getAreaPerimeterLength(Shape area) {
PathIterator it = area.getPathIterator(null, FLATNESS);
double coords[] = new double[6];
double p = 0.0;
Double x1 = null;
Double y1 = null;
Double xFirst = null;
Double yFirst = null;
while (!it.isDone()) {
int type = it.currentSegment(coords);
double x2;
double y2;
if (type == PathIterator.SEG_CLOSE) {
x2 = xFirst;
y2 = yFirst;
} else {
x2 = coords[0];
y2 = coords[1];
}
if (x1 != null && y1 != null) {
double xx, yy;
xx = x2 - x1;
yy = y2 - y1;
p += Math.sqrt(xx * xx + yy * yy);
} else {
xFirst = x2;
yFirst = y2;
}
x1 = x2;
y1 = y2;
it.next();
}
return p;
}
/**
* @see #getAreaPerimeterLength
*/
public static double getLineLength(Shape line) {
return getAreaPerimeterLength(line);
}
/**
* Uses the current logger to print the coordinate of each vertex of the
* given {@code Shape}.
* @param shp
*/
public static void printvertices(Shape shp) {
PathIterator it = shp.getPathIterator(null, FLATNESS);
double coords[] = new double[6];
while (!it.isDone()) {
int type = it.currentSegment(coords);
switch (type) {
case PathIterator.SEG_CLOSE:
LOGGER.warn(I18N.tr("CLOSE"));
break;
case PathIterator.SEG_CUBICTO:
LOGGER.warn(I18N.tr("CUBIC TO {0}:{1}",coords[0],coords[1]));
break;
case PathIterator.SEG_LINETO:
LOGGER.warn(I18N.tr("LINE TO {0}:{1}",coords[0],coords[1]));
break;
case PathIterator.SEG_MOVETO:
LOGGER.warn(I18N.tr("MOVE TO {0}:{1}",coords[0],coords[1]));
break;
case PathIterator.SEG_QUADTO:
LOGGER.warn(I18N.tr("QUAD TO {0}:{1}",coords[0],coords[1]));
break;
default:
LOGGER.warn(I18N.tr("!DEFAULT!"));
break;
}
it.next();
}
}
/**
* Split a line into two lines. The first line length equals firstLineLenght parameter
* The sum of the two line length equals the length of the initial line.
*
* If firstLineLenght > initial line length, only one line (initial line copy) is returned
*
* @param line the line to split
* @param firstLineLength expected length of the first returned line
*
* @return Generated lines.
*/
public static List<Shape> splitLine(Shape line, double firstLineLength) {
ArrayList<Shape> shapes = new ArrayList<Shape>();
if (ShapeHelper.getLineLength(line) < firstLineLength) {
shapes.add(line);
return shapes;
}
PathIterator it = line.getPathIterator(null, FLATNESS);
double coords[] = new double[6];
Path2D.Double segment = new Path2D.Double();
double p = 0.0;
double p1;
it.currentSegment(coords);
double x1 = coords[0];
double y1 = coords[1];
segment.moveTo(x1, y1);
double xFirst = x1;
double yFirst = y1;
it.next();
double x2;
double y2;
boolean first = true;
while (!it.isDone()) {
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_CLOSE) {
x2 = xFirst;
y2 = yFirst;
} else {
x2 = coords[0];
y2 = coords[1];
}
double xx, yy;
xx = x2 - x1;
yy = y2 - y1;
p1 = Math.sqrt(xx * xx + yy * yy);
p += p1;
if (first && p > firstLineLength) {
first = false;
// Le point courant dépasse la limite de longueur
double delta = (p - firstLineLength);
// Obtenir le point qui est exactement à la limite
Point2D.Double pt = getPointAt(x1, y1, x2, y2, p1 - delta);
x1 = pt.x;
y1 = pt.y;
// On termine le segment, l'ajoute à la liste de shapes
segment.lineTo(x1, y1);
p = 0;
shapes.add(segment);
// Et commence le nouveau segment à
segment = new Path2D.Double();
segment.moveTo(x1, y1);
} else {
// Le point courant dépasse la limite de longueur
segment.lineTo(x2, y2);
x1 = x2;
y1 = y2;
it.next();
}
}
//last segment end with last point
shapes.add(segment);
return shapes;
}
/**
* Split a linear feature in segments of specified length. Last one may be shorter
*
* @param line the line to split
* @param segLength the length of the parts to create
* @return list of equal-length segment
*/
public static List<Shape> splitLineInSeg(Shape line, double segLength) {
List<Shape> shapes = new ArrayList<Shape>();
double totalLength = ShapeHelper.getLineLength(line);
if (segLength <= 0.0 || segLength >= totalLength) {
shapes.add(line);
return shapes;
}
PathIterator it = line.getPathIterator(null, FLATNESS);
double coords[] = new double[6];
Path2D.Double segment = new Path2D.Double();
double p = 0.0;
double p1;
it.currentSegment(coords);
double x1 = coords[0];
double y1 = coords[1];
double xFirst = x1;
double yFirst = y1;
segment.moveTo(x1, y1);
it.next();
double x2;
double y2;
while (!it.isDone()) {
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_CLOSE) {
x2 = xFirst;
y2 = yFirst;
} else {
x2 = coords[0];
y2 = coords[1];
}
double xx, yy;
xx = x2 - x1;
yy = y2 - y1;
p1 = Math.sqrt(xx * xx + yy * yy);
p += p1;
if (p > segLength) {
// Le point courant dépasse la limite de longueur
double delta = (p - segLength);
// Obtenir le point qui est exactement à la limite
Point2D.Double pt = getPointAt(x1, y1, x2, y2, p1 - delta);
x1 = pt.x;
y1 = pt.y;
// On termine le segment, l'ajoute à la liste de shapes
segment.lineTo(x1, y1);
p = 0;
shapes.add(segment);
// Et commence le nouveau segment à
segment = new Path2D.Double();
segment.moveTo(x1, y1);
} else {
// Le point courant ne dépasse pas la limite de longueur
segment.lineTo(x2, y2);
x1 = x2;
y1 = y2;
it.next();
}
}
shapes.add(segment);
return shapes;
}
/**
* Split a linear feature in the specified number of part, which have all the same length
*
* @param line the line to split
* @param nbPart the number of part to create
* @return list of equal-length segment
*/
public static List<Shape> splitLine(Shape line, int nbPart) {
ArrayList<Shape> shapes = new ArrayList<Shape>();
double perimeter = getLineLength(line);
double segLength = perimeter / nbPart;
PathIterator it = line.getPathIterator(null, FLATNESS);
double coords[] = new double[6];
Path2D.Double segment = new Path2D.Double();
double p = 0.0;
double p1;
it.currentSegment(coords);
double x1 = coords[0];
double y1 = coords[1];
double xFirst = x1;
double yFirst = y1;
segment.moveTo(x1, y1);
it.next();
double x2;
double y2;
while (!it.isDone()) {
int type = it.currentSegment(coords);
if (type == PathIterator.SEG_CLOSE) {
x2 = xFirst;
y2 = yFirst;
} else {
x2 = coords[0];
y2 = coords[1];
}
double xx, yy;
xx = x2 - x1;
yy = y2 - y1;
p1 = Math.sqrt(xx * xx + yy * yy);
p += p1;
if (p > segLength) {
// Le point courant dépasse la limite de longueur
double delta = (p - segLength);
// Obtenir le point qui est exactement à la limite
Point2D.Double pt = getPointAt(x1, y1, x2, y2, p1 - delta);
x1 = pt.x;
y1 = pt.y;
// On termine le segment, l'ajoute à la liste de shapes
segment.lineTo(x1, y1);
p = 0;
shapes.add(segment);
// Et commence le nouveau segment à
segment = new Path2D.Double();
segment.moveTo(x1, y1);
} else {
// Le point courant dépasse la limite de longueur
segment.lineTo(x2, y2);
x1 = x2;
y1 = y2;
it.next();
}
}
//last segment end with last point
//segment.lineTo(x1, y1);
shapes.add(segment);
return shapes;
}
/**
* return coordinates which are:
* 1) at the specified distance from point (x1,y1)
* 2) on the line going through points (x1,y1) and (x2,y2)
* @param x1 first point x coordinate
* @param y1 first point y coordinate
* @param x2 second point x coordinate
* @param y2 second point y coordinate
* @param distance the distance between first point (x1,y1) and the returned one
* @return the coordinate, nested in a point
*/
private static Point2D.Double getPointAt(double x1, double y1, double x2, double y2, double distance) {
double length = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
return new Point2D.Double(x1 + distance * (x2 - x1) / length, y1 + distance * (y2 - y1) / length);
}
/**
* Go along a line shape and return the point at the specified distance from the beginning of the line
* @param shp the line
* @param distance
* @return point representing the point at the linear length distance
*/
public static Point2D.Double getPointAt(Shape shp, double distance) {
PathIterator it = shp.getPathIterator(null, FLATNESS);
double coords[] = new double[6];
double p = 0.0;
Double x1 = null;
Double y1 = null;
double x2 = 0.0;
double y2 = 0.0;
double segLength = 0.0;
double xF = 0.0;
double yF = 0.0;
while (!it.isDone()) {
int currentSegment = it.currentSegment(coords);
x2 = coords[0];
y2 = coords[1];
if (currentSegment == PathIterator.SEG_CLOSE) {
x2 = xF;
y2 = yF;
}
// Since two point are known, we can start to look for our point
if (x1 != null && y1 != null) {
double xx, yy;
xx = x2 - x1;
yy = y2 - y1;
segLength = Math.sqrt(xx * xx + yy * yy);
p += segLength;
if (p > distance) {
// The point is on this segment !
break;
}
} else {
xF = x2;
yF = y2;
}
it.next();
if (!it.isDone()) {
x1 = x2;
y1 = y2;
}
}
if (distance < 0.0) {
return new Point2D.Double(xF, yF);
} else {
return getPointAt(x1, y1, x2, y2, segLength - p + distance);
}
}
//private static Polygon perpendicularOffsetForArea() {
// return null;
//}
private static class Vertex {
private double x;
private double y;
private Double quadX1;
private Double quadY1;
private Double quadX2;
private Double quadY2;
private Double quadX3;
private Double quadY3;
public Vertex(double x, double y) {
this.x = x;
this.y = y;
quadX1 = null;
quadY1 = null;
quadX2 = null;
quadY2 = null;
quadX3 = null;
quadY3 = null;
}
public void setQuadTo(double x1, double y1, double x2, double y2, double x3, double y3) {
quadX1 = x1;
quadY1 = y1;
quadX2 = x2;
quadY2 = y2;
quadX3 = x3;
quadY3 = y3;
}
@Override
public boolean equals(Object o) {
if (o instanceof Vertex) {
Vertex v = (Vertex) o;
return Math.abs(v.x - this.x) < 0.0001 && Math.abs(v.y - this.y) < 0.0001;
}
return false;
}
@Override
public int hashCode() {
assert false : "hashCode not designed";
return 42; // any arbitraty constant !
}
@Override
public String toString() {
return "" + x + ";" + y;
}
}
private static class Edge {
private int mPos;
private int mDir;
private boolean processed;
public Edge() {
this.processed = false;
mPos = 1;
mDir = 0;
}
public boolean hasBeedProcessed() {
return processed;
}
public boolean is11() {
return mPos == 1 && mDir == 1;
}
public boolean is10() {
return mPos == 0 && mDir == 1;
}
public boolean isUnfeasible() {
return mPos == 0;
}
@Override
public String toString() {
return "" + mDir + mPos;
}
}
/**
* Convert Shape into a list of coordinates.
* Will also convert curves to set of segment
* @param shp the shape to convert
* @return array list of coordinate, same order
*/
private static List<ArrayList<Vertex>> getVertexes(Shape shp) {
ArrayList<ArrayList<Vertex>> shapes = new ArrayList<ArrayList<Vertex>>();
PathIterator it = shp.getPathIterator(null, FLATNESS);
ArrayList<Vertex> vertexes = new ArrayList<Vertex>();
double coords[] = new double[6];
Vertex v;
// Want a direct access to coordinates !!!
while (!it.isDone()) {
int type = it.currentSegment(coords);
switch (type) {
case PathIterator.SEG_CLOSE:
shapes.add(vertexes);
vertexes = new ArrayList<Vertex>();
break;
case PathIterator.SEG_QUADTO:
case PathIterator.SEG_CUBICTO:
break;
case PathIterator.SEG_LINETO:
case PathIterator.SEG_MOVETO:
v = new Vertex(coords[0], coords[1]);
if (vertexes.size() > 0) {
if (!v.equals(vertexes.get(vertexes.size() - 1))) {
vertexes.add(v);
}
} else {
vertexes.add(v);
}
break;
default:
break;
}
it.next();
}
if (vertexes.size() > 1) {
shapes.add(vertexes);
}
return shapes;
}
/**
* Remove point which stands in the middle of a straight line
* @param vertexes
*/
private static void removeUselessVertex(List<Vertex> vertexes) {
if (isClosed(vertexes)) {
vertexes.remove(vertexes.size() - 1);
}
}
/**
* Compute offset vertexes
* @param vertexes
* @param offset
* @return list of corresponding offseted vertex
*/
private static List<Vertex> createOffsetVertexes(List<Vertex> vertexes, double offset, boolean closed) {
int i;
ArrayList<Vertex> offseted = new ArrayList<Vertex>();
double absOffset = Math.abs(offset);
double gamma;
double theta = Math.PI / 2;
for (i = 0; i < vertexes.size(); i++) {
if (i == 0 && !closed) {
// First point (unclosed path case)
Vertex v = vertexes.get(i);
Vertex vP1 = vertexes.get(i + 1);
gamma = Math.atan2(vP1.y - v.y, vP1.x - v.x) + theta;
Vertex ov = new Vertex(v.x - Math.cos(gamma) * offset, v.y - Math.sin(gamma) * offset);
offseted.add(ov);
} else if (i == vertexes.size() - 1 && !closed) {
// Last point (unclosed path case)
Vertex v = vertexes.get(i);
Vertex vM1 = vertexes.get(i - 1);
gamma = Math.atan2(v.y - vM1.y, v.x - vM1.x) + theta;
offseted.add(new Vertex(v.x - Math.cos(gamma) * offset, v.y - Math.sin(gamma) * offset));
} else {
Vertex v = vertexes.get(i);
Vertex vM1 = vertexes.get((i - 1 + vertexes.size()) % vertexes.size()); // TODO handle Closed path Case ! (with modulo...)
Vertex vP1 = vertexes.get((i + 1) % vertexes.size());
double eP1X = vP1.x - v.x;
double eP1Y = vP1.y - v.y;
double eP1Norm = Math.sqrt(eP1X * eP1X + eP1Y * eP1Y);
eP1X /= eP1Norm;
eP1Y /= eP1Norm;
double eX = v.x - vM1.x;
double eY = v.y - vM1.y;
double eNorm = Math.sqrt(eX * eX + eY * eY);
eX /= eNorm;
eY /= eNorm;
double dxTmp;
double dyTmp;
// Determine gamma angle : law of cosines
//a
dxTmp = vP1.x - v.x;
dyTmp = vP1.y - v.y;
double aLength = Math.sqrt(dxTmp * dxTmp + dyTmp * dyTmp);
//b
dxTmp = v.x - vM1.x;
dyTmp = v.y - vM1.y;
double bLength = Math.sqrt(dxTmp * dxTmp + dyTmp * dyTmp);
// c
dxTmp = vP1.x - vM1.x;
dyTmp = vP1.y - vM1.y;
double cLength = Math.sqrt(dxTmp * dxTmp + dyTmp * dyTmp);
gamma = Math.acos((cLength * cLength - aLength * aLength - bLength * bLength) / (-2 * aLength * bLength));
// Skip straight segment
if (Double.isNaN(gamma) || Math.abs(gamma - Math.PI) < 2 * ONE_DEG_IN_RAD || Math.abs(gamma) < 2 * ONE_DEG_IN_RAD) {
vertexes.remove(i);
i--;
continue;
}
double angleStatus = crossProduct(vM1.x, vM1.y, v.x, v.y, vP1.x, vP1.y) * offset;
if (angleStatus < 0) {
// Interior
double dx = eP1X - eX;
double dy = eP1Y - eY;
double dNorm = Math.sqrt(dx * dx + dy * dy);
dx /= dNorm;
dy /= dNorm;
dx *= absOffset / Math.sin(gamma / 2);
dy *= absOffset / Math.sin(gamma / 2);
offseted.add(new Vertex(v.x + dx, v.y + dy));
} else {
// Exterior
double dx = eX - eP1X;
double dy = eY - eP1Y;
double dNorm = Math.sqrt(dx * dx + dy * dy);
dx /= dNorm;
dy /= dNorm;
dx *= absOffset / Math.cos((Math.PI - gamma) / 2);
dy *= absOffset / Math.cos((Math.PI - gamma) / 2);
gamma = Math.atan2(v.y - vM1.y, v.x - vM1.x) + theta;
double quadx3 = v.x - Math.cos(gamma) * offset;
double quady3 = v.y - Math.sin(gamma) * offset;
gamma = Math.atan2(vP1.y - v.y, vP1.x - v.x) + theta;
double quadx1 = v.x - Math.cos(gamma) * offset;
double quady1 = v.y - Math.sin(gamma) * offset;
double deltaX = (quadx1 + quadx3) / 2 - v.x;
double deltaY = (quady1 + quady3) / 2 - v.y;
double h = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
double quadx2;
double quady2;
if (Math.abs(h) < 0.001) {
deltaX = v.x - quadx1;
deltaY = v.y - quady1;
quadx2 = v.x - deltaY;
quady2 = v.y + deltaX;
} else {
quadx2 = v.x + deltaX * absOffset / h;
quady2 = v.y + deltaY * absOffset / h;
}
Vertex nv = new Vertex(v.x + dx, v.y + dy);
nv.setQuadTo(quadx3, quady3, quadx2, quady2, quadx1, quady1);
offseted.add(nv);
}
}
}
return offseted;
}
/**
* Compute the distance between point (x3;y3) and the segment defined by (x1;y2) and (x2;y3).
*
*
* @param x1 x coordinate of segment point 1
* @param y1 y coordinate of segment point 1
* @param x2 x coordinate of segment point 2
* @param y2 y coordinate of segment point 2
* @param x3 x coordinate of the point
* @param y3 y coordinate of the point
* @return
*/
private static double getDistanceFromSegment(double x1, double y1, double x2, double y2, double x3, double y3) {
double p4x = x3 + (y2 - y1);
double p4y = y3 - (x2 - x1);
// Segment length
double h = Math.sqrt((x1 - x2) * (x1 - x2) + (y2 - y1) * (y2 - y1));
// Case 1 : the minimum length is perpendicular to the segment
if (crossProduct(x3, y3, p4x, p4y, x1, y1) * crossProduct(x3, y3, p4x, p4y, x2, y2) < 0) {
return Math.abs(crossProduct(x1, y1, x2, y2, x3, y3) / h);
} else {
double d1 = (x1 - x3) * (x1 - x3) + (y1 - y3) * (y1 - y3);
double d2 = (x2 - x3) * (x2 - x3) + (y2 - y3) * (y2 - y3);
double d3 = Math.min(d1, d2);
return Math.sqrt(d3);
}
}
/**
* According to offseted and original vertexes. compoute edge status
* @param vertexes
* @param offsetVertexes
* @param offset
* @param closed
* @return
*/
private static List<Edge> computeEdges(List<Vertex> vertexes, List<Vertex> offsetVertexes, double offset, boolean closed) {
ArrayList<Edge> offstedEdges = new ArrayList<Edge>();
int i;
double absOffset = Math.abs(offset);
for (i = 0; i < vertexes.size(); i++) {
if (i < vertexes.size() - 1 || closed) {
int j = (i + 1) % vertexes.size();
Vertex v1 = vertexes.get(i);
Vertex v2 = vertexes.get(j);
Vertex ov1 = offsetVertexes.get(i);
Vertex ov2 = offsetVertexes.get(j);
Edge e = new Edge();
e.mDir = (isSegIntersect(v1.x, v1.y, ov1.x, ov1.y, v2.x, v2.y, ov2.x, ov2.y) ? 0 : 1);
offstedEdges.add(e);
}
}
for (i = 0; i < offstedEdges.size() - (closed ? 0 : 1); i++) {
Edge e = offstedEdges.get(i);
if (e.mDir == 0) {
e.mPos = 0;
} else if (closed) {
Vertex p31 = offsetVertexes.get(i);
Vertex p32 = offsetVertexes.get((i + 1) % vertexes.size());
double d1 = absOffset;
double d2 = absOffset;
int j;
for (j = 0; j < offstedEdges.size() - (closed ? 0 : 1); j++) {
Vertex p1 = vertexes.get(j);
Vertex p2 = vertexes.get((j + 1) % vertexes.size());
double d = getDistanceFromSegment(p1.x, p1.y, p2.x, p2.y, p31.x, p31.y);
if (d < d1) {
d1 = d;
}
d = getDistanceFromSegment(p1.x, p1.y, p2.x, p2.y, p32.x, p32.y);
if (d < d2) {
d2 = d;
}
if (d1 < absOffset - 0.1 && d2 < absOffset - 0.1) {
e.mPos = 0;
break;
}
}
}
}
return offstedEdges;
}
/**
* Transform list of vertex into Shape
*
* @param vertexes list of vertex
* @param closed is the vertexes represent a ring ?
* @return shape corresponding to vertexes
*/
private static Shape createShapeFromVertexes(List<Vertex> vertexes, boolean closed) {
if (vertexes.size() < 2) {
return null;
}
Path2D.Double shp = new Path2D.Double();
Vertex v1 = vertexes.get(0);
if (v1.quadX1 != null) {
double dx = v1.quadX2 - v1.x;
double dy = v1.quadY2 - v1.y;
if (ENABLE_QUAD && dx * dx + dy * dy > 9) {
//if (dx * dx + dy * dy < -9) { // i.e. never (a² + b² > 0) !
shp.moveTo(v1.quadX1, v1.quadY1);
shp.quadTo(v1.quadX2, v1.quadY2, v1.quadX3, v1.quadY3);
} else {
shp.moveTo(v1.x, v1.y);
}
} else {
shp.moveTo(v1.x, v1.y);
}
int i;
for (i = 1; i < vertexes.size(); i++) {
Vertex v = vertexes.get(i);
if (v.quadX1 != null) {
double dx = v.quadX2 - v.x;
double dy = v.quadY2 - v.y;
if (ENABLE_QUAD && dx * dx + dy * dy > 9) {
//if (dx * dx + dy * dy < -9) { // i.e. never (a² + b² > 0) !
shp.lineTo(v.quadX1, v.quadY1);
shp.quadTo(v.quadX2, v.quadY2, v.quadX3, v.quadY3);
} else {
shp.lineTo(v.x, v.y);
}
} else {
shp.lineTo(v.x, v.y);
}
}
if (closed) {
shp.closePath();
}
return shp;
}
/**
* According to edges status and offseted vertexes, determine which vertexes will be part of the offset contour
* @param edges
* @param vertexes
* @return
*/
private static List<Vertex> computeRawLink(List<Edge> edges, List<Vertex> vertexes, boolean closed) {
ArrayList<Vertex> rawLink = new ArrayList<Vertex>();
ArrayList<Integer> offsetLinkList = new ArrayList<Integer>();
ArrayList<Integer> bufferLinkList = new ArrayList<Integer>();
int i;
for (i = 0; i < edges.size(); i++) {
//Edge e = edges.get(i);
offsetLinkList.add(i);
}
int backward = 0;
int forward = 0;
int inDir = 0;
int inPos = 0;
if (!closed) {
rawLink.add(vertexes.get(0));
}
if (edges.size() == 1) {
rawLink.add(vertexes.get(0));
rawLink.add(vertexes.get(1));
}
for (i = 0; i < offsetLinkList.size(); i++) {
int id = offsetLinkList.get(i);
Edge e = edges.get(id);
if (e.is11()) {
backward = i;
break;
} else {
offsetLinkList.remove(i);
offsetLinkList.add(id);
}
}
while (backward < offsetLinkList.size()) {
for (i = (backward + 1) % offsetLinkList.size(); i != backward; i = (i + 1) % offsetLinkList.size()) {
int id = offsetLinkList.get(i);
Edge e = edges.get(id);
if (e.hasBeedProcessed()) {
break;
}
if (e.is11()) {
forward = i;
e.processed = true;
break;
} else {
if (e.mDir == 0) {
inDir += 1;
}
if (e.mPos == 0) {
inPos += 1;
}
if (e.is10()) {
bufferLinkList.add(id);
bufferLinkList.add((id + 1) % vertexes.size());
}
}
}
if (backward == forward) {
break;
}
int bn = (offsetLinkList.get(backward) + 1) % vertexes.size();
int fn = (offsetLinkList.get(forward) + 1) % vertexes.size();
if (inDir == 0 && inPos == 0) {
// Add backward edge 2nd point
rawLink.add(vertexes.get(bn));
} else if (inDir == 0 && inPos > 0) {
for (Integer j : bufferLinkList) {
rawLink.add(vertexes.get(j));
}
} else if (inDir >= 1) {
Vertex v1 = vertexes.get(offsetLinkList.get(backward));
Vertex v2 = vertexes.get(bn);
Vertex v3 = vertexes.get(offsetLinkList.get(forward));
Vertex v4 = vertexes.get(fn);
Point2D.Double inter = getLineIntersection(v1.x, v1.y, v2.x, v2.y, v3.x, v3.y, v4.x, v4.y);
if (inter != null
&& isPointOnSegement(v1.x, v1.y, v2.x, v2.y, inter.x, inter.y)
&& isPointOnSegement(v3.x, v3.y, v4.x, v4.y, inter.x, inter.y)) {
// 3.1
Vertex nv = new Vertex(inter.x, inter.y);
rawLink.add(nv);
} else {
// 3.2
for (int j = backward + 1; j < forward; j++) {
Vertex vn = vertexes.get(offsetLinkList.get(j));
Vertex vm = vertexes.get((offsetLinkList.get(j) + 1) % vertexes.size());
Point2D.Double i1 = getLineIntersection(v1.x, v1.y, v2.x, v2.y, vn.x, vn.y, vm.x, vm.y);
Point2D.Double i2 = getLineIntersection(vn.x, vn.y, vm.x, vm.y, v3.x, v3.y, v4.x, v4.y);
if (i1 != null && i2 != null
&& isPointOnSegement(v1.x, v1.y, v2.x, v2.y, i1.x, i1.y)
&& isPointOnSegement(v3.x, v3.y, v4.x, v3.y, i2.x, i2.y)) {
//rawLink.add(new Vertex(i1.x, i1.y));
//rawLink.add(new Vertex(i2.x, i2.y));
Vertex nv = new Vertex(i1.x, i1.y);
rawLink.add(nv);
nv = new Vertex(i2.x, i2.y);
rawLink.add(nv);
}
}
}
}
backward = forward;
inDir = 0;
inPos = 0;
bufferLinkList.clear();
}
return rawLink;
}
private static boolean isPointOnSegement(double x1, double y1, double x2, double y2, double x3, double y3) {
double dist12 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1);
double dist13 = (x3 - x1) * (x3 - x1) + (y3 - y1) * (y3 - y1);
double dist23 = (x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2);
return Math.abs(dist12 - dist13 - dist23) < 0.01;
/*if (Math.abs(x1 - x2) < 0.0001) {
// segment is vertical, check against y value
double ymin = Math.min(y1, y2);
double ymax = Math.max(y1, y2);
return y3 > ymin && y3 < ymax;
} else {
// check against X values
double xmin = Math.min(x1, x2);
double xmax = Math.max(x1, x2);
return x3 > xmin && x3 < xmax;
}*/
}
private static boolean isClosed(List<Vertex> vertexes) {
return vertexes.get(0).equals(vertexes.get(vertexes.size() - 1));
}
private static List<Shape> contourParallelShape(Shape shp, double offset) {
List<ArrayList<Vertex>> shapes = getVertexes(shp);
ArrayList<Shape> rawShapes = new ArrayList<Shape>();
for (List<Vertex> vertexes : shapes) {
boolean closed = isClosed(vertexes);
removeUselessVertex(vertexes);
//if (closed) {
// normalize(vertexes);
//}
List<Vertex> offsetVertexes = createOffsetVertexes(vertexes, offset, closed);
if (offsetVertexes.size() < 2) {
LOGGER.error(I18N.tr("Unable to compute perpendicular offset"));
return rawShapes;
}
List<Edge> edges = computeEdges(vertexes, offsetVertexes, offset, closed);
List<Vertex> rawLink = computeRawLink(edges, offsetVertexes, closed);
Shape finalShape = createShapeFromVertexes(rawLink, closed);
if (finalShape != null) {
rawShapes.add(finalShape);
}
}
return rawShapes;
//return createShapeFromVertexes(rawLink, closed);
//return null;
}
/**
* Compute cross product :
*
* o(x2,y2)
* /
* cp > 0 /
* / cp < 0
* /
* /
* o (x1, y1)
*
* @param x1 seg first point x coord
* @param y1 seg first point y coord
* @param x2 seg second point x coord
* @param y2 seg second point y coord
* @param x3 the point to check x coord
* @param y3 the point to check y coord
*
*/
static double crossProduct(double x1, double y1, double x2, double y2, double x3, double y3) {
return (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1);
}
/**
* Is (x1y1)(x2y2) (strictly) intersects (x3y3)(x4y4) ?
* @param x1
* @param y1
* @param x2
* @param y2
* @param x3
* @param y3
* @param x4
* @param y4
* @return
*/
private static boolean isSegIntersect(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) {
double cp1, cp2, cp3, cp4;
cp1 = crossProduct(x1, y1, x2, y2, x3, y3);
cp2 = crossProduct(x1, y1, x2, y2, x4, y4);
cp3 = crossProduct(x3, y3, x4, y4, x1, y1);
cp4 = crossProduct(x3, y3, x4, y4, x2, y2);
return (cp1 * cp2 < 0 && cp3 * cp4 < 0);
}
private static Point2D.Double computeSegmentIntersection(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) {
/*
double cp1, cp2, cp3, cp4;
cp1 = crossProduct(x1, y1, x2, y2, x3, y3);
cp2 = crossProduct(x1, y1, x2, y2, x4, y4);
cp3 = crossProduct(x3, y3, x4, y4, x1, y1);
cp4 = crossProduct(x3, y3, x4, y4, x2, y2);
*/
if (isSegIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) {
// 1 intersection point !
return getLineIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
} else {
// none or many intersection point !
return null;
}
}
/**
* Compute intersection between two line.
* The first line is passing by points (x1,y1) & (x2, y2). The second by (x3,y3) and (x4,y4)
*
* @param x1
* @param y1
* @param x2
* @param y2
* @param x3
* @param y3
* @param x4
* @param y4
* @return null if lines are parallel, the intersection point otherwise
*/
private static Point2D.Double getLineIntersection(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) {
double denom1 = x2 - x1;
double denom2 = x4 - x3;
if (Math.abs(denom1) < 0.0001) {
denom1 = 0;
}
if (Math.abs(denom2) < 0.0001) {
denom2 = 0;
}
double a1 = (y2 - y1) / denom1;
double a2 = (y4 - y3) / denom2;
double b1 = y2 - a1 * x2;
double b2 = y4 - a2 * x4;
double x;
double y;
if (Double.isInfinite(a1) && Double.isInfinite(a2)) {
return null;
} else if (Double.isInfinite(a1)) {
x = x1;
y = a2 * x + b2;
} else if (Double.isInfinite(a2)) {
x = x3;
y = a1 * x + b1;
} else {
x = (b2 - b1) / (a1 - a2);
y = a1 * x + b1;
if (Double.isNaN(x) || Double.isInfinite(x)) {
return null;
}
}
return new Point2D.Double(x, y);
}
/**
* REF : http://www.springerlink.com/content/nx71u48201887310/fulltext.pdf
* @param shp
* @param offset
* @return
*/
public static List<Shape> perpendicularOffset(Shape shp, double offset) {
return contourParallelShape(shp, offset);
//return perpendicularOffsetForLine(shp, offset);
}
public static Line2D.Double intersection(Line2D.Double line, Rectangle2D.Double bounds) {
//line.x1, line.y1, line.x2, line.y2
Point2D.Double bottom = computeSegmentIntersection(line.x1, line.y1, line.x2, line.y2,
bounds.getMinX(), bounds.getMaxY(), bounds.getMaxX(), bounds.getMaxY());
Point2D.Double right = computeSegmentIntersection(line.x1, line.y1, line.x2, line.y2,
bounds.getMaxX(), bounds.getMaxY(), bounds.getMaxX(), bounds.getMinY());
Point2D.Double top = computeSegmentIntersection(line.x1, line.y1, line.x2, line.y2,
bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMinY());
Point2D.Double left = computeSegmentIntersection(line.x1, line.y1, line.x2, line.y2,
bounds.getMinX(), bounds.getMinY(), bounds.getMinX(), bounds.getMaxY());
ArrayList<Point2D.Double> pts = new ArrayList<Point2D.Double>();
if (bottom != null) {
pts.add(bottom);
}
if (right != null) {
pts.add(right);
}
if (top != null) {
pts.add(top);
}
if (left != null) {
pts.add(left);
}
if (pts.size() != 2) {
return null;
} else {
return new Line2D.Double(pts.get(0), pts.get(1));
}
}
public static Geometry clipToExtent(Geometry theGeom, Envelope extent) {
GeometryFactory geometryFactory = new GeometryFactory();
Envelope incExtent = new Envelope(extent);
incExtent.expandBy(extent.getWidth() / 10, extent.getHeight() / 10);
Geometry geometry = theGeom.intersection(geometryFactory.toGeometry(extent));
if (geometry.isEmpty()) {
return null;
} else {
return geometry;
}
}
}