/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 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.process.raster; import java.awt.Rectangle; import java.awt.geom.Point2D; import java.awt.image.RenderedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.logging.Level; import java.util.logging.Logger; import javax.media.jai.iterator.RandomIter; import javax.media.jai.iterator.RandomIterFactory; import org.geotools.coverage.grid.GridCoverage2D; import org.geotools.coverage.grid.GridGeometry2D; import org.geotools.coverage.grid.InvalidGridGeometryException; import org.geotools.data.simple.SimpleFeatureCollection; import org.geotools.feature.FeatureCollection; import org.geotools.feature.FeatureCollections; import org.geotools.feature.simple.SimpleFeatureBuilder; import org.geotools.geometry.Envelope2D; import org.geotools.geometry.jts.ReferencedEnvelope; import org.geotools.process.ProcessException; import org.geotools.process.ProcessFactory; import org.geotools.process.impl.AbstractProcess; import org.geotools.referencing.CRS; import org.geotools.util.NullProgressListener; import org.geotools.util.SimpleInternationalString; import org.geotools.util.SubProgressListener; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.geometry.Envelope; import org.opengis.metadata.spatial.PixelOrientation; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.MathTransform2D; import org.opengis.referencing.operation.TransformException; import org.opengis.util.ProgressListener; import com.vividsolutions.jts.algorithm.InteriorPointArea; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineSegment; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.operation.polygonize.Polygonizer; /** * Vectorizes discrete regions of uniform value in the specified band of a GridCoverage2D * object. Data are treated as double values regardless of the data type of the input grid * coverage. * <p> * Example of use via the GeoTools process API * * <pre><code> * GridCoverage2D cov = ... * * final Map<String, Object> params = new HashMap<String, Object>(); * params.put(RasterToVectorFactory.RASTER.key, cov); * params.put(RasterToVectorFactory.BAND.key, Integer.valueOf(0)); * params.put(RasterToVectorFactory.BOUNDS.key, env); * params.put(RasterToVectorFactory.OUTSIDE.key, Collections.singleton(0.0d)); * params.put(RasterToVectorFactory.INSIDE_EDGES.key, Boolean.TRUE); * * final Process r2v = Processors.createProcess(new NameImpl(ProcessFactory.GT_NAMESPACE, "RasterToVectorProcess")); * * // run the process on a thread other than the event-dispatch thread * // (here we use SwingWorker to accomplish this) * SwingWorker worker = new SwingWorker<Map<String, Object>, Void>() { * protected Map<String, Object> doInBackground() throws Exception { * ProgressWindow pw = new ProgressWindow(null); * pw.setTitle("Vectorizing coverage"); * return r2v.execute(params, pw); * } * * protected void done() { * Map<String, Object> vectorizingResults = null; * try { * vectorizingResults = get(); * } catch (Exception ignore) {} * * if (vectorizingResults != null) { * FeatureCollection<SimpleFeatureType, SimpleFeature> fc = * (FeatureCollection<SimpleFeatureType, SimpleFeature>) vectorizingResults.get( * RasterToVectorFactory.RESULT_FEATURES.key); * * // do something with features... * } * } * }; * * worker.execute(); * </code></pre> * * Example of "manual" use with the static helper method: * <pre><code> * GridCoverage2D cov = ... * int band = ... * Envelope bounds = ... * List<Double> outsideValues = ... * boolean insideEdges = ... * * FeatureCollection<SimpleFeatureType,SimpleFeature> features = * RasterToVectorProcess.process(cov, band, bounds, outsideValues, insideEdges, null); * </code></pre> * * @author Jody Garnett * @author Michael Bedward * @since 2.6 * * * @source $URL$ * @version $Id$ */ public class RasterToVectorProcess extends AbstractProcess { /* the JTS object that does all the topological work for us */ private Polygonizer polygonizer; /* the coverage presently being processed */ private GridCoverage2D coverage; /** * The transform to use to convert pixel lower right corner positions to real world coordinates */ private MathTransform2D transformLR; /* Image bounds for vectorizing */ private Rectangle imageBounds; /* Input coverage cell width in the X and Y directions */ private double cellWidthX; private double cellWidthY; // positions in curData matrix just to avoid confusion private static final int TL = 0; private static final int TR = 1; private static final int BL = 2; private static final int BR = 3; // these are used to identify the orientation of corner touches // between possibly separate polygons with the same value private static final int TL_BR = 4; private static final int TR_BL = 5; private static final int CROSS = 6; // Precision of comparison in the function different(a, b) private static final double EPSILON = 1.0e-8d; // Set of values that indicate 'outside' or 'no data' areas in the raster private SortedSet<Double> outside; // Proxy value used when inside edges are not being vectorized private Double inside = null; // Flag value used to code polygons when inside edges are not // being vectorized private static final int INSIDE_FLAG_VALUE = 1; /* * array of Coor objects that store end-points of vertical lines under construction */ private Map<Integer, LineSegment> vertLines; /* * end-points of horizontal line under construction */ private LineSegment horizLine; /* * collection of line strings on the boundary of raster regions */ private List<LineString> lines; /* * Factory for construction of JTS Geometry objects */ private GeometryFactory geomFactory; /* * list of corner touches between possibly separate polygons of * the same value. Each Coordinate has x:y = col:row and z set * to either TL_BR or TR_BL to indicate the orientation of the * corner touch. */ List<Coordinate> cornerTouches; /* * input image */ private RenderedImage image; /** * Package-access constructor. Client code should use the public * {@linkplain RasterToVectorFactory#create } method * @param factory */ RasterToVectorProcess(ProcessFactory factory) { super(factory); } /** * Run the raster to vector process. * The returned {@code Map} will contain a single object: the FeatureCollection * of vector polygons which can be retrieved as follows * <pre><code> * FeatureCollection<SimpleFeatureType, SimpleFeature> features = * (FeatureCollection<SimpleFeatureType, SimpleFeature>) resultsMap.get( * RasterToVectorFactory.RESULT_FEATURES.key); * </code></pre> * * @param input a map of input parameters (see {@linkplain RasterToVectorFactory} for details) * * @param monitor an optional {@code ProgressListener} (may be {@code null}) * * @return a {@code Map} containing the vectorized features */ public Map<String, Object> execute(Map<String, Object> input, ProgressListener monitor) throws ProcessException { Object o; GridCoverage2D cov = (GridCoverage2D) input.get(RasterToVectorFactory.RASTER.key); int band = 0; o = input.get(RasterToVectorFactory.BAND.key); if (o != null) { band = (Integer) o; } Envelope bounds = (Envelope) input.get(RasterToVectorFactory.BOUNDS.key); Collection<Double> outsideValues = (Collection<Double>) input.get( RasterToVectorFactory.OUTSIDE.key); if(outsideValues == null) { outsideValues = Collections.emptyList(); } boolean insideEdges = true; o = input.get(RasterToVectorFactory.INSIDE_EDGES.key); if (o != null) { insideEdges = (Boolean) o; } FeatureCollection features = convert(cov, band, bounds, outsideValues, insideEdges, monitor); Map<String, Object> results = new HashMap<String, Object>(); results.put(RasterToVectorFactory.RESULT_FEATURES.key, features); return results; } /** * A static helper method that can be called directy to run the process. * <p> * The process interface is useful for advertising functionality to * dynamic applications, but for 'hands on' coding this method is much more * convenient than working via {@linkplain org.geotools.process.Process#execute }. * * @param cov the input coverage * @param band the index of the band to be vectorized * @param bounds bounds of the area (in world coordinates) to vectorize; if {@code null} * the whole coverage * @param outside a collection of one or more values which represent 'outside' or no data * (may be {@code null} or empty) * @param insideEdges whether to vectorize inside edges (those separating grid regions * with non-outside values) * @param monitor an optional ProgressListener (may be {@code null}) * * @return a FeatureCollection containing simple polygon features */ public static SimpleFeatureCollection process( GridCoverage2D cov, int band, Envelope bounds, Collection<Double> outsideValues, boolean insideEdges, ProgressListener monitor) throws ProcessException { RasterToVectorFactory factory = new RasterToVectorFactory(); RasterToVectorProcess process = factory.create(); return process.convert(cov, band, bounds, outsideValues, insideEdges, monitor); } /** * Convert the input raster coverage to vector polygons. * * @param cov the input coverage * @param band the index of the band to be vectorized * @param bounds of the area to vectorize in world coords (null means whole coverage) * @param outside a collection of one or more values which represent 'outside' or no data * (may be {@code null} or empty) * @param insideEdges whether to vectorize inside edges (those separating grid regions * with non-outside values) * @param monitor a progress listener (may be {@code null}) * * @return a FeatureCollection containing simple polygon features */ private SimpleFeatureCollection convert( GridCoverage2D cov, int band, Envelope bounds, Collection<Double> outsideValues, boolean insideEdges, ProgressListener monitor) throws ProcessException { if (monitor == null) { monitor = new NullProgressListener(); } else { monitor.started(); } ReferencedEnvelope workingBounds = null; if (bounds == null) { workingBounds = new ReferencedEnvelope(cov.getEnvelope()); } else { CoordinateReferenceSystem sourceCRS = bounds.getCoordinateReferenceSystem(); CoordinateReferenceSystem targetCRS = cov.getCoordinateReferenceSystem(); if (sourceCRS != null) { if (!CRS.equalsIgnoreMetadata(sourceCRS, targetCRS)) { throw new ProcessException("CRS of bounds must match that of the coverage"); } } ReferencedEnvelope inputBounds = new ReferencedEnvelope(bounds); ReferencedEnvelope covBounds = new ReferencedEnvelope(cov.getEnvelope()); workingBounds = new ReferencedEnvelope(covBounds.intersection(inputBounds), targetCRS); if (workingBounds == null || workingBounds.isEmpty()) { throw new ProcessException("Specified bounds lie wholly outside of coverage"); } } try { monitor.setTask(new SimpleInternationalString("Initializing")); initialize(cov, workingBounds, new SubProgressListener(monitor, 10)); monitor.setTask(new SimpleInternationalString("Vectorizing")); vectorizeAndCollectBoundaries(band, outsideValues, insideEdges, new SubProgressListener(monitor, 70)); /*********************************************************** * Assemble the LineStringss into Polygons, and create the * collection of features to return ***********************************************************/ SimpleFeatureType schema = RasterToVectorFactory.getSchema(cov .getCoordinateReferenceSystem()); monitor.setTask(new SimpleInternationalString("Creating polygon features")); SimpleFeatureCollection features = assembleFeatures(cov, band, insideEdges, schema, new SubProgressListener(monitor, 20)); return features; } catch (Exception ex) { throw new ProcessException(ex); } finally { monitor.complete(); } } /** * Assemble a feature collection by polygonizing the boundary segments that * have been collected by the vectorizing algorithm. * * @param grid the input grid coverage * @param band the band containing the data to vectorize * @param insideEdges whether to vectorize inside edges (those separating grid regions * with non-outside values) * @param type feature type * @param monitor a progress listener (may be {@code null}) * @return a new FeatureCollection containing the boundary polygons */ private SimpleFeatureCollection assembleFeatures(GridCoverage2D grid, int band, boolean insideEdges, SimpleFeatureType type, ProgressListener monitor) { if (monitor == null) { monitor = new NullProgressListener(); } SimpleFeatureCollection features = FeatureCollections.newCollection(); SimpleFeatureBuilder builder = new SimpleFeatureBuilder(type); Point2D p = new Point2D.Double(); double[] bandData = new double[grid.getNumSampleDimensions()]; polygonizer.add(lines); Collection polygons = polygonizer.getPolygons(); final int size = polygons.size(); try { float progressScale = 100.0f / size; monitor.started(); int index = 0; for (Iterator i = polygons.iterator(); i.hasNext(); index++) { if (monitor.isCanceled()) { throw new CancellationException(); } monitor.progress(progressScale * index); Polygon poly = (Polygon) i.next(); InteriorPointArea ipa = new InteriorPointArea(poly); Coordinate c = ipa.getInteriorPoint(); Point insidePt = geomFactory.createPoint(c); if (!poly.contains(insidePt)) { // try another method to generate an interior point boolean found = false; for (Coordinate ringC : poly.getExteriorRing().getCoordinates()) { c.x = ringC.x + cellWidthX / 2; c.y = ringC.y; insidePt = geomFactory.createPoint(c); if (poly.contains(insidePt)) { found = true; break; } } if (!found) { throw new IllegalStateException("Can't locate interior point for polygon"); } } p.setLocation(c.x, c.y); bandData = grid.evaluate(p, bandData); if (!isOutside(bandData[band])) { builder.add(poly); if (insideEdges) { builder.add(bandData[band]); } else { builder.add(INSIDE_FLAG_VALUE); } features.add(builder.buildFeature(null)); } } return features; } finally { monitor.complete(); } } /** * Set various data fields used to control the vectorizing process. * * @param coverage the input grid coverage * @param bounds bounds (world coords) of the area to be vectorized * @param monitor a progress listener (may be {@code null}) */ private void initialize(GridCoverage2D coverage, Envelope bounds, ProgressListener monitor) throws TransformException, InvalidGridGeometryException { if (monitor == null) monitor = new NullProgressListener(); try { monitor.started(); this.coverage = coverage; GridGeometry2D gridGeom = coverage.getGridGeometry(); // image used to sample the grid coverage image = coverage.getRenderedImage(); this.transformLR = coverage.getGridGeometry().getGridToCRS2D( PixelOrientation.LOWER_RIGHT); monitor.progress(30); imageBounds = coverage.getGridGeometry().worldToGrid(new Envelope2D(bounds)); cellWidthX = gridGeom.getEnvelope2D().getSpan(gridGeom.axisDimensionX) / gridGeom.getGridRange2D().getSpan(gridGeom.gridDimensionX); cellWidthY = gridGeom.getEnvelope2D().getSpan(gridGeom.axisDimensionY) / gridGeom.getGridRange2D().getSpan(gridGeom.gridDimensionY); lines = new ArrayList<LineString>(); geomFactory = new GeometryFactory(); polygonizer = new Polygonizer(); monitor.progress(80); vertLines = new HashMap<Integer, LineSegment>(); cornerTouches = new ArrayList<Coordinate>(); } finally { monitor.complete(); } } /** * Vectorize the boundaries of regions of uniform value in the input grid coverage * and collect the boundaries as LineStrings * * @param band index of the band which contains the data to be vectorized * @param outsideValues a collection of one or more values which represent 'outside' * or no data (may be {@code null} or empty) * @param insideEdges whether to vectorize inside edges (those separating grid regions * with non-outside values) * @param monitor a progress listener (may be {@code null}) */ private void vectorizeAndCollectBoundaries(int band, Collection<Double> outsideValues, boolean insideEdges, ProgressListener monitor) { if (monitor == null) { monitor = new NullProgressListener(); } try { // a 2x2 matrix of double values used as a moving window double[] curData = new double[4]; RandomIter imageIter = RandomIterFactory.create(image, null); outside = new TreeSet<Double>(); if (outsideValues == null || outsideValues.isEmpty()) { outside.add(Double.NaN); } else { outside.addAll(outsideValues); } if (!insideEdges) { setInsideValue(); } // we add a virtual border, one cell wide, coded as 'outside' // around the raster float progressScale = 100.0f / (imageBounds.y + imageBounds.height - 1); for (int row = imageBounds.y - 1; row < imageBounds.y + imageBounds.height; row++) { if (monitor.isCanceled()) { throw new CancellationException(); } monitor.progress(progressScale * row); curData[TR] = curData[BR] = outside.first(); for (int col = imageBounds.x - 1; col < imageBounds.x + imageBounds.width; col++) { boolean[] ok = inDataWindow(row, col); curData[TL] = curData[TR]; curData[BL] = curData[BR]; curData[TR] = (ok[TR] ? imageIter.getSampleDouble(col + 1, row, band) : outside.first()); if (isOutside(curData[TR])) { curData[TR] = outside.first(); } else if (!insideEdges) { curData[TR] = inside; } curData[BR] = (ok[BR] ? imageIter.getSampleDouble(col + 1, row + 1, band) : outside.first()); if (isOutside(curData[BR])) { curData[BR] = outside.first(); } else if (!insideEdges) { curData[BR] = inside; } updateCoordList(row, col, curData); } } } finally { monitor.complete(); } } /** * Sets the proxy value used for "inside" cells when inside edges * are not being vectorized. */ private void setInsideValue() { Double maxFinite = null; for (Double d : outside) { if (!(d.isInfinite() || d.isNaN())) { maxFinite = d; } } if (maxFinite != null) { inside = maxFinite + 1; } else { inside = (double) INSIDE_FLAG_VALUE; } } /** * Check the position of the data window with regard to the grid coverage * boundaries. We do this because a virtual, single-cell-width border is * placed around the input data. * * @param row index of the image row in the top left cell of the data window * @param col index of the image col in the top left cell of the data window * @return an array of four boolean values to be indexed with the TL, TR, BL * and BR constants. */ private boolean[] inDataWindow(int row, int col) { boolean[] ok = new boolean[4]; int rowflag = (row < imageBounds.y ? -1 : (row >= imageBounds.y + imageBounds.height - 1 ? 1 : 0)); int colflag = (col < imageBounds.x ? -1 : (col >= imageBounds.x + imageBounds.width - 1 ? 1 : 0)); ok[TL] = rowflag >= 0 && colflag >= 0; ok[TR] = rowflag >= 0 && colflag < 1; ok[BL] = rowflag < 1 && colflag >= 0; ok[BR] = rowflag < 1 && colflag < 1; return ok; } /** * This method controls the construction of line segments that border regions of uniform data * in the raster. See the {@linkplain #nbrConfig} method for more details. * * @param row index of the image row in the top left cell of the 2x2 data window * @param col index of the image col in the top left cell of the 2x2 data window * @param curData values in the current data window */ private void updateCoordList(int row, int col, double[] curData) { LineSegment seg; switch (nbrConfig(curData)) { case 0: // vertical line continuing // nothing to do break; case 1: // bottom right corner // new horizontal and vertical lines horizLine = new LineSegment(); horizLine.p0.x = col; seg = new LineSegment(); seg.p0.y = row; vertLines.put(col, seg); break; case 2: // horizontal line continuing // nothing to do break; case 3: // bottom left corner // end of horizontal line; start of new vertical line horizLine.p1.x = col; addHorizLine(row); horizLine = null; seg = new LineSegment(); seg.p0.y = row; vertLines.put(col, seg); break; case 4: // top left corner // end of horizontal line; end of vertical line horizLine.p1.x = col; addHorizLine(row); horizLine = null; seg = vertLines.get(col); seg.p1.y = row; addVertLine(col); vertLines.remove(col); break; case 5: // top right corner // start horiztonal line; end vertical line horizLine = new LineSegment(); horizLine.p0.x = col; seg = vertLines.get(col); seg.p1.y = row; addVertLine(col); vertLines.remove(col); break; case 6: // inverted T in upper half // end horiztonal line; start new horizontal line; end vertical line horizLine.p1.x = col; addHorizLine(row); horizLine.p0.x = col; seg = vertLines.get(col); seg.p1.y = row; addVertLine(col); vertLines.remove(col); break; case 7: // T in lower half // end horizontal line; start new horizontal line; start new vertical line horizLine.p1.x = col; addHorizLine(row); horizLine.p0.x = col; seg = new LineSegment(); seg.p0.y = row; vertLines.put(col, seg); break; case 8: // T pointing left // end horizontal line; end vertical line; start new vertical line horizLine.p1.x = col; addHorizLine(row); horizLine = null; seg = vertLines.get(col); seg.p1.y = row; addVertLine(col); seg = new LineSegment(); seg.p0.y = row; vertLines.put(col, seg); break; case 9: // T pointing right // start new horizontal line; end vertical line; start new vertical line horizLine = new LineSegment(); horizLine.p0.x = col; seg = vertLines.get(col); seg.p1.y = row; addVertLine(col); seg = new LineSegment(); seg.p0.y = row; vertLines.put(col, seg); break; case 10: // cross // end horizontal line; start new horizontal line // end vertical line; start new vertical line horizLine.p1.x = col; addHorizLine(row); horizLine.p0.x = col; seg = vertLines.get(col); seg.p1.y = row; addVertLine(col); seg = new LineSegment(); seg.p0.y = row; vertLines.put(col, seg); int z = -1; if (isDifferent(curData[TL], curData[BR])) { if (!isDifferent(curData[TR], curData[BL])) { z = CROSS; } } else { if (isDifferent(curData[TR], curData[BL])) { z = TL_BR; } else { z = TR_BL; } } if (z != -1) { cornerTouches.add(new Coordinate(col, row, z)); } break; case 11: // uniform // nothing to do break; } } /** * Examine the values in the 2x2 kernel and match to one of * the cases in the table below: * <pre> * 0) AB 1) AA 2) AA 3) AA * AB AB BB BA * * 4) AB 5) AB 6) AB 7) AA * BB AA CC BC * * 8) AB 9) AB 10) AB 11) AA * CB AC CD AA * </pre> * These patterns are those used in the GRASS raster to vector routine. * @param curData array of current data window values * @return integer id of the matching configuration */ private int nbrConfig(double[] curData) { if (isDifferent(curData[TL], curData[TR])) { // 0, 4, 5, 6, 8, 9, 10 if (isDifferent(curData[TL], curData[BL])) { // 4, 6, 8, 10 if (isDifferent(curData[BL], curData[BR])) { // 8, 10 if (isDifferent(curData[TR], curData[BR])) { return 10; } else { return 8; } } else { // 4, 6 if (isDifferent(curData[TR], curData[BR])) { return 6; } else { return 4; } } } else { // 0, 5, 9 if (isDifferent(curData[BL], curData[BR])) { // 0, 9 if (isDifferent(curData[TR], curData[BR])) { return 9; } else { return 0; } } else { return 5; } } } else { // 1, 2, 3, 7, 11 if (isDifferent(curData[TL], curData[BL])) { // 2, 3, 7 if (isDifferent(curData[BL], curData[BR])) { // 3, 7 if (isDifferent(curData[TR], curData[BR])) { return 7; } else { return 3; } } else { return 2; } } else { // 1, 11 if (isDifferent(curData[TR], curData[BR])) { return 1; } else { return 11; } } } } /** * Create a LineString for a newly constructed horizontal border segment * @param row index of the image row in the top left cell of the current data window */ private void addHorizLine(int row) { Point2D pixelStart = new Point2D.Double(horizLine.p0.x, row); Point2D pixelEnd = new Point2D.Double(horizLine.p1.x, row); Point2D rwStart = new Point2D.Double(); Point2D rwEnd = new Point2D.Double(); try { transformLR.transform(pixelStart, rwStart); transformLR.transform(pixelEnd, rwEnd); } catch (TransformException ex) { Logger.getLogger(RasterToVectorProcess.class.getName()).log(Level.SEVERE, null, ex); } Coordinate[] coords = new Coordinate[] { new Coordinate(rwStart.getX(), rwStart.getY()), new Coordinate(rwEnd.getX(), rwEnd.getY()) }; lines.add(geomFactory.createLineString(coords)); } /** * Create a LineString for a newly constructed vertical border segment * @param col index of the image column in the top-left cell of the current data window */ private void addVertLine(int col) { Point2D pixelStart = new Point2D.Double(col, vertLines.get(col).p0.y); Point2D pixelEnd = new Point2D.Double(col, vertLines.get(col).p1.y); Point2D rwStart = new Point2D.Double(); Point2D rwEnd = new Point2D.Double(); try { transformLR.transform(pixelStart, rwStart); transformLR.transform(pixelEnd, rwEnd); } catch (TransformException ex) { Logger.getLogger(RasterToVectorProcess.class.getName()).log(Level.SEVERE, null, ex); } Coordinate[] coords = new Coordinate[] { new Coordinate(rwStart.getX(), rwStart.getY()), new Coordinate(rwEnd.getX(), rwEnd.getY()) }; GeometryFactory gf = new GeometryFactory(); lines.add(gf.createLineString(coords)); } private boolean isOutside(double value) { for (Double d : outside) { if (!isDifferent(d, value)) { return true; } } return false; } /** * Test if two double values are different. Uses an absolute tolerance and * checks for NaN values. * * @param a first value * @param b second value * @return true if the values are different; false otherwise */ private boolean isDifferent(double a, double b) { if (Double.isNaN(a) ^ Double.isNaN(b)) { return true; } else if (Double.isNaN(a) && Double.isNaN(b)) { return false; } if (Math.abs(a - b) > EPSILON) { return true; } else { return false; } } }