/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2015, 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.geometry.jts;
import static java.lang.Math.abs;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.Math.toRadians;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.*;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import javax.media.jai.widget.ScrollingImagePanel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestWatcher;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.CoordinateSequenceFilter;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryComponentFilter;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.PrecisionModel;
import com.vividsolutions.jts.geom.impl.PackedCoordinateSequenceFactory;
import com.vividsolutions.jts.io.ParseException;
import com.vividsolutions.jts.io.WKTReader;
import com.vividsolutions.jts.operation.buffer.BufferParameters;
public class OffsetCurveBuilderTest {
static final double EPS = 2e-1;
static final boolean INTERACTIVE = true; // Boolean.getBoolean("org.geotools.image.test.interactive");
static final boolean INTERACTIVE_ON_SUCCESS = Boolean.getBoolean("org.geotools.image.test.interactive.on.success");
Geometry curve;
Geometry offsetCurve;
@Rule
public TestWatcher interactiveReporter = new TestWatcher() {
protected void succeeded(org.junit.runner.Description description) {
if (curve != null && INTERACTIVE_ON_SUCCESS) {
displayCurves(false);
}
};
@Override
protected void failed(Throwable e, org.junit.runner.Description description) {
if(curve != null) {
System.out.println("Original geometry: " + curve);
System.out.println("Offset geometry: " + offsetCurve);
}
if (curve != null && INTERACTIVE) {
displayCurves(true);
}
}
private void displayCurves(boolean failed) {
BufferedImage image = drawCurves();
ImageDisplay dialog = new ImageDisplay(image, failed ? "Failure" : "Success");
dialog.setModal(true);
dialog.setVisible(true);
}
private BufferedImage drawCurves() {
final int SIZE = 400;
BufferedImage bi = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_3BYTE_BGR);
Envelope envelope = curve.getEnvelopeInternal();
if(offsetCurve != null) {
envelope.expandToInclude(offsetCurve.getEnvelopeInternal());
}
if(envelope.getWidth() == 0) {
envelope.expandBy(envelope.getHeight(), 0);
}
if(envelope.getHeight() == 0) {
envelope.expandBy(0, envelope.getWidth());
}
envelope.expandBy(envelope.getWidth() * 0.1, envelope.getHeight() * 0.1);
double scale = SIZE / Math.max(envelope.getWidth(), envelope.getHeight());
double tx = -envelope.getMinX() * scale;
double ty = envelope.getMinY() * scale + SIZE;
AffineTransform at = new AffineTransform(scale, 0.0d, 0.0d, -scale,
tx, ty);
Graphics2D graphics = bi.createGraphics();
graphics.setColor(Color.WHITE);
graphics.fillRect(0, 0, SIZE, SIZE);
graphics.setColor(Color.BLACK);
graphics.setStroke(new BasicStroke(4));
graphics.draw(new LiteShape(curve, at, false));
graphics.setColor(Color.RED);
graphics.setStroke(new BasicStroke(4, BasicStroke.CAP_SQUARE, BasicStroke.JOIN_ROUND, 1, new float[] {8, 8}, 0));
graphics.draw(new LiteShape(offsetCurve, at, false));
graphics.dispose();
return bi;
};
};
class ImageDisplay extends JDialog {
private static final long serialVersionUID = -8640087805737551918L;
boolean accept = false;
public ImageDisplay(RenderedImage image, String title) {
JPanel content = new JPanel(new BorderLayout());
this.setContentPane(content);
this.setTitle(title);
final JLabel topLabel = new JLabel(
"<html><body>" + "The curve (black) and its offset (red) " + "</html></body>");
topLabel.setBorder(new EmptyBorder(4, 4, 4, 4));
content.add(topLabel, BorderLayout.NORTH);
ScrollingImagePanel imageViewer = new ScrollingImagePanel(image, Math.min(400, image.getWidth()) + 100, Math.min(400,
image.getHeight()) + 100);
content.add(imageViewer);
JButton close = new JButton("Close");
close.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ImageDisplay.this.setVisible(false);
}
});
content.add(close, BorderLayout.SOUTH);
pack();
}
}
Geometry offset(Geometry geometry, double offset) {
this.curve = geometry;
this.offsetCurve = new OffsetCurveBuilder(offset).offset(curve);
return offsetCurve;
}
Geometry geometry(String wkt) throws ParseException {
return new WKTReader().read(wkt);
}
@Test
public void nullSafe() {
Geometry offset = new OffsetCurveBuilder(10).offset(null);
assertNull(offset);
}
@Test
public void testHorizontalSegmentPositiveOffset() throws ParseException {
Geometry offset = simpleOffsetTest("LINESTRING(0 0, 10 0)", 2);
assertTrue(offset.getEnvelopeInternal().getMinY() == 2);
}
@Test
public void testHorizontalSegmentNegativeOffset() throws ParseException {
Geometry offset = simpleOffsetTest("LINESTRING(0 0, 10 0)", -2);
assertTrue(offset.getEnvelopeInternal().getMinY() == -2);
}
@Test
public void testDiagonalSegmentPositiveOffset() throws ParseException {
simpleOffsetTest("LINESTRING(0 0, 10 10)", 2);
}
@Test
public void testAllDiagonalsLeft() throws Exception {
double offset = 2;
testAllDiagonals(offset);
}
@Test
public void testAllDiagonalsRight() throws Exception {
double offset = -2;
testAllDiagonals(offset);
}
private void testAllDiagonals(double offset) {
double[] ordinates = new double[4];
ordinates[0] = 0;
ordinates[1] = 0;
for(int i = 0; i < 360; i++) {
double angle = toRadians(i);
ordinates[2] = 10 * cos(angle);
ordinates[3] = 10 * sin(angle);
curve = new GeometryFactory().createLineString(new PackedCoordinateSequenceFactory().create(ordinates, 2));
simpleOffsetTest(curve, offset);
}
}
@Test
public void testLShapedInternal() throws ParseException {
simpleOffsetTest("LINESTRING(0 10, 0 0, 10 0)", 2);
}
@Test
public void testLShapedExternal() throws ParseException {
simpleOffsetTest("LINESTRING(0 10, 0 0, 10 0)", 2);
}
@Test
public void testUShapedInternal() throws ParseException {
simpleOffsetTest("LINESTRING(0 10, 0 0, 10 0, 10 10)", 2);
}
@Test
public void testUShapedExternal() throws ParseException {
simpleOffsetTest("LINESTRING(0 10, 0 0, 10 0, 10 10)", -2);
}
@Test
public void testClockArmsRight() throws ParseException {
for(int i = 30; i < 360; i++) {
testClockArms(-2, i);
}
}
@Test
public void testClockArmsLeft() throws ParseException {
for(int i = 1; i < 330; i++) {
testClockArms(2, i);
}
}
private void testClockArms(double offset, int a) {
double angle = toRadians(a);
double[] ordinates = new double[6];
ordinates[0] = 10;
ordinates[1] = 0;
ordinates[2] = 0;
ordinates[3] = 0;
ordinates[4] = 10 * cos(angle);
ordinates[5] = 10 * sin(angle);
curve = new GeometryFactory().createLineString(new PackedCoordinateSequenceFactory().create(ordinates, 2));
simpleOffsetTest(curve, offset);
}
@Test
public void testTriangleOuter() throws Exception {
Geometry offset = simpleOffsetTest("LINEARRING(0 10, 0 0, 10 0, 0 10)", -2);
assertThat(offset, instanceOf(LinearRing.class));
}
@Test
public void testTriangleInner() throws Exception {
Geometry offset = simpleOffsetTest("LINEARRING(0 10, 0 0, 10 0, 0 10)", 2);
assertThat(offset, instanceOf(LinearRing.class));
}
@Test
public void testSimpleLoopGenerator() throws Exception {
simpleOffsetTest("LINESTRING(0 0, 5 0, 5 -1, 7 -1, 7 0, 10 0)", 2);
}
@Test
public void testElongatedLoopGenerator() throws Exception {
Geometry geom = geometry("LINESTRING(0 0, 5 0, 5 -10, 7 -10, 7 0, 10 0)");
Geometry offset = offset(geom, 1.5);
assertTrue(offset.isValid());
assertTrue(offset.getLength() > 0);
// this one "fails", but the output cannot be really called wrong anymore, if we are trying to
// offset a road at least
Geometry expected = geometry("LINESTRING (0 1.5, 5 1.5, 5.260472266500395 1.477211629518312, 5.513030214988503 1.4095389311788626, 5.75 1.299038105676658, 5.964181414529809 1.149066664678467, 6.149066664678467 0.9641814145298091, 6.299038105676658 0.7500000000000002, 6.409538931178862 0.5130302149885032, 6.477211629518312 0.2604722665003956, 6.5 0.0000000000000001, 6.5 -8.5, 5.5 -8.5, 5.5 0.0000000000000001, 5.522788370481688 0.2604722665003956, 5.590461068821138 0.5130302149885032, 5.700961894323342 0.7499999999999998, 5.850933335321533 0.9641814145298091, 6.035818585470191 1.149066664678467, 6.25 1.299038105676658, 6.486969785011497 1.4095389311788624, 6.739527733499605 1.477211629518312, 7 1.5, 10 1.5)");
assertTrue(expected.equalsExact(offset, 0.1));
}
@Test
public void testElongatedNonLoopGenerator() throws Exception {
simpleOffsetTest("LINESTRING(0 0, 4 0, 4 -10, 9 -10, 9 0, 10 0)", 2);
}
@Test
public void testSelfIntersectLeft() throws Exception {
Geometry geom = geometry("LINESTRING(0 0, 10 0, 10 -10, 3 -10, 3 3)");
Geometry offset = offset(geom, 2);
assertTrue(offset.isValid());
assertTrue(offset.getLength() > 0);
// the offset line intersects the original one, because it's also self intersecting, so we cannot have this test
// assertEquals(2, offset.distance(geom), EPS);
Geometry expected = geometry("LINESTRING (0 2, 10 2, 10.34729635533386 1.969615506024416, 10.684040286651337 1.8793852415718169, 11 1.7320508075688774, 11.28557521937308 1.532088886237956, 11.532088886237956 1.2855752193730787, 11.732050807568877 1.0000000000000002, 11.879385241571816 0.6840402866513376, 11.969615506024416 0.3472963553338608, 12 0.0000000000000001, 12 -10, 11.969615506024416 -10.34729635533386, 11.879385241571818 -10.684040286651337, 11.732050807568877 -11, 11.532088886237956 -11.28557521937308, 11.28557521937308 -11.532088886237956, 11 -11.732050807568877, 10.684040286651339 -11.879385241571816, 10.34729635533386 -11.969615506024416, 10 -12, 2.9999999999999996 -12, 2.6527036446661394 -11.969615506024416, 2.3159597133486622 -11.879385241571816, 2 -11.732050807568877, 1.714424780626921 -11.532088886237956, 1.467911113762044 -11.28557521937308, 1.2679491924311228 -11, 1.1206147584281831 -10.684040286651337, 1.030384493975584 -10.34729635533386, 1 -10, 1 3)");
assertTrue(expected.equalsExact(offset, 0.1));
}
@Test
public void testSelfIntersectRight() throws Exception {
Geometry geom = geometry("LINESTRING(0 0, 10 0, 10 -10, 3 -10, 3 3)");
Geometry offset = offset(geom, -1);
assertTrue(offset.isValid());
assertTrue(offset.getLength() > 0);
// the offset line intersects the original one, because it's also self intersecting, so we cannot have this test
// assertEquals(2, offset.distance(geom), EPS);
assertEquals(geometry("LINESTRING (0 -1, 9 -1, 9 -9, 4 -9, 4 3)"), offset);
}
@Test
public void testLoopRoad1() throws Exception {
String wkt = "LINESTRING (13 470, 0 270, 24 251, 67 264)";
simpleOffsetTest(wkt, 50);
}
@Test
public void testLoopRoad2() throws Exception {
String wkt = "LINESTRING (13 470, 0 270, 24 251, 67 264, 108 279)";
simpleOffsetTest(wkt, 50);
}
@Test
public void testLoopRoad3() throws Exception {
String wkt = "LINESTRING (67 264, 108 279, 134 279, 143 262, 135 0)";
simpleOffsetTest(wkt, -50);
}
@Test
public void testSpike() throws Exception {
String wkt = "LINESTRING (20 0, 0 67, 9 138, 8 134)";
simpleOffsetTest(wkt, 50);
simpleOffsetTest(wkt, -50);
simpleOffsetTest(wkt, 100);
simpleOffsetTest(wkt, -100);
}
@Test
public void testInwardsSpike() throws Exception {
String wkt = "LINESTRING (594005.44863915 4920198.37095076, 594015.86677618 4920163.01783546, 594006.98015873 4920062.97719912, 593985.83866536 4920003.77506624, 593948.20205744 4919978.69410905, 593949.41233583 4919981.13611074)";
simpleOffsetTest(wkt, 50);
simpleOffsetTest(wkt, -50);
simpleOffsetTest(wkt, 100);
simpleOffsetTest(wkt, -100);
}
private Geometry simpleOffsetTest(String wkt, final double offsetDistance) throws ParseException {
Geometry geom = geometry(wkt);
return simpleOffsetTest(geom, offsetDistance);
}
private Geometry simpleOffsetTest(Geometry geom, final double offsetDistance) {
Geometry offset = offset(geom, offsetDistance);
assertTrue(offset.isValid());
assertTrue(offset.getLength() > 0);
assertEquals(abs(offsetDistance), offset.distance(geom), EPS * abs(offsetDistance));
offset.apply(new GeometryComponentFilter() {
@Override
public void filter(Geometry geom) {
if(geom instanceof LineString) {
LineString ls = (LineString) geom;
CoordinateSequence cs = ls.getCoordinateSequence();
if(cs.size() < 2) {
return;
}
double px = cs.getOrdinate(0, 0);
double py = cs.getOrdinate(0, 1);
for (int i = 1; i < cs.size(); i++) {
double cx = cs.getOrdinate(i, 0);
double cy = cs.getOrdinate(i, 1);
if(cx == px && cy == py) {
fail("Found two subsequent ordinates with the same value: " + cx + ", " + py);
}
px = cx;
py = cy;
}
}
}
});
return offset;
}
public static void main(String[] args) throws ParseException {
// simple utility to take a random WKT and make it into something looking like test data (simplify large coordinates, excess precision)
final double tolerance = 1;
String wkt = "LINESTRING (809 2365, 796 2165, 820 2146, 863 2159, 904 2174, 930 2174, 939 2157, 931 1895)";
Geometry geom = new WKTReader().read(wkt);
Envelope envelope = geom.getEnvelopeInternal();
final double minx = envelope.getMinX();
final double miny = envelope.getMinY();
geom.apply(new CoordinateSequenceFilter() {
@Override
public boolean isGeometryChanged() {
return true;
}
@Override
public boolean isDone() {
// TODO Auto-generated method stub
return false;
}
@Override
public void filter(CoordinateSequence seq, int i) {
double x = seq.getOrdinate(i, 0);
double y = seq.getOrdinate(i, 1);
x -= minx;
y -= miny;
x = Math.round(x / tolerance) * tolerance;
y = Math.round(y / tolerance) * tolerance;
seq.setOrdinate(i, 0, x);
seq.setOrdinate(i, 1, y);
}
});
System.out.println(geom.toText());
}
}