Package org.geotools.geometry.jts

Source Code of org.geotools.geometry.jts.Decimator

/*
*    GeoTools - The Open Source Java GIS Toolkit
*    http://geotools.org
*
*    (C) 2004-2014, 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 java.awt.Rectangle;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;

import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;

/**
* Accepts geometries and collapses all the vertices that will be rendered to
* the same pixel. This class works only if the Geometries are based on
* {@link LiteCoordinateSequence} instances.
*
* @author jeichar
* @since 2.1.x
*
*
* @source $URL$
*/
public final class Decimator {
   
    private static final Logger LOGGER = org.geotools.util.logging.Logging.getLogger(Decimator.class);
   
    static final double DP_THRESHOLD;
   
    static {
        int threshold = -1;
        String sthreshold = System.getProperty("org.geotools.decimate.dpThreshold");
        if(sthreshold != null) {
            try {
                threshold = Integer.parseInt(sthreshold);
            } catch(Throwable t) {
                LOGGER.log(Level.WARNING, "Invalid value for org.geotools.decimate.dpThreshold, " +
                    "should be a positive integer but is: " + sthreshold);
            }
        }
        DP_THRESHOLD = threshold;
    }
   
    private static final double EPS = 1e-9;

  private double spanx = -1;

  private double spany = -1;
 
  /**
     * Builds a decimator that will generalize geometries so that two subsequent points
     * will be at least pixelDistance away from each other when painted on the screen.
     * Set pixelDistance to 0 if you don't want any generalization (but just a transformation)
     *
     * @param screenToWorld
     * @param paintArea
     * @param pixelDistance
     */
    public Decimator(MathTransform screenToWorld, Rectangle paintArea, double pixelDistance) {
        if (screenToWorld != null && pixelDistance > 0) {
            try {
                double[] spans = computeGeneralizationDistances(screenToWorld, paintArea, pixelDistance);
                this.spanx = spans[0];
                this.spany = spans[1];
            } catch(TransformException e) {
                throw new RuntimeException("Could not perform the generalization spans computation", e);
            }
        } else {
            this.spanx = 1;
            this.spany = 1;
        }
    }

    /**
     * The generalization step in the x direction
     */
    public double getSpanX() {
        return spanx;
    }

    /**
     * The generalization step in the y direction
     */
    public double getSpanY() {
        return spany;
    }

  /**
   * djb - noticed that the old way of finding out the decimation is based on
   * the (0,0) location of the image. This is often wildly unrepresentitive of
   * the scale of the entire map.
   *
   * A better thing to do is to decimate this on a per-shape basis (and use
   * the shape's center). Another option would be to sample the image at
   * different locations (say 9) and choose the smallest spanx/spany you find.
   *
   * Also, if the xform is an affine Xform, you can be a bit more aggressive
   * in the decimation. If its not an affine xform (ie. its actually doing a
   * CRS xform), you may find this is a bit too aggressive due to any number
   * of mathematical issues.
   *
   * This is just a simple method that uses the centre of the given rectangle
   * instead of (0,0).
   *
   * NOTE: this could need more work based on CRS, but the rectangle is in
   * pixels so it should be fairly immune to all but crazy projections.
   *
   *
   * @param screenToWorld
   * @param paintArea
   */
  public Decimator(MathTransform screenToWorld, Rectangle paintArea) {
      // 0.8 is just so you don't decimate "too much". magic number.
    this(screenToWorld, paintArea, 0.8);
  }

  /**
   * Given a full transformation from screen to world and the paint area computes a best
   * guess of the maxium generalization distance that won't make the transformations induced
   * by the generalization visible on screen. <p>In other words, it computes how long a pixel
   * is in the native spatial reference system of the data</p>
   * @param screenToWorld
   * @param paintArea
   * @return
   * @throws TransformException
   */
    public static double[] computeGeneralizationDistances(MathTransform screenToWorld, Rectangle paintArea, double pixelDistance)
            throws TransformException {
        try {
            // init the spans with the upper left corner
            double[] spans = getGeneralizationSpans(paintArea.x, paintArea.y, screenToWorld);
            // search over a simple 3x3 grid for higher spans so that we perform a basic sampling of the whole area
            // and we pick the shortest generalization distances
            for(int i = 0; i < 2; i++) {
                for(int j = 0; j < 2; j++) {
                    double[] ns = getGeneralizationSpans(paintArea.x + paintArea.width * i / 2.0,
                            paintArea.y + paintArea.height / 2.0, screenToWorld);
                    if(ns[0] < spans[0])
                        spans[0] = ns[0];
                    if(ns[1] < spans[1])
                        spans[1] = ns[1];
                }
            }
            spans[0] *= pixelDistance;
            spans[1] *= pixelDistance ;
            return spans;
        } catch(TransformException e) {
            // if we can't transform we went way out of the area of definition for the transform -> don't generalize
            return new double[] {0, 0};
        }
    }
   
    /**
     * Computes the real world distance of a one pixel segment centered in the specified point
     * @param x
     * @param y
     * @param transform
     * @return
     * @throws TransformException
     */
    static double[] getGeneralizationSpans(double x, double y, MathTransform transform) throws TransformException {
        double[] original = new double[] {
                x - 0.5, y - 0.5,
                x + 0.5, y + 0.5};
        double[] transformed = new double[4];
        transform.transform(original, 0, transformed, 0, 2);
        double[] spans = new double[2];
        spans[0] = Math.abs(transformed[0] - transformed[2]);
        spans[1] = Math.abs(transformed[1] - transformed[3]);
        return spans;
    }
   

  /**
   * @throws TransformException
   * @deprecated use the other constructor (with rectange) see javadox. This
   *             works fine, but it the results are often poor if you're also
   *             doing CRS xforms.
   */
  public Decimator(MathTransform screenToWorld) {
    this(screenToWorld, new Rectangle()); // do at (0,0)
  }
 
  public Decimator(double spanx, double spany) {
      this.spanx = spanx;
      this.spany = spany;
  }

    public final Geometry decimateTransformGeneralize(Geometry geometry,
      MathTransform transform) throws TransformException {
    if (geometry instanceof GeometryCollection) {
      GeometryCollection collection = (GeometryCollection) geometry;
      final int length = collection.getNumGeometries();
      boolean cloned = false;
      Class elementType = null;
            Geometry[] elements = null;
      for (int i = 0; i < length; i++) {
        Geometry source = collection.getGeometryN(i);
                Geometry generalized = decimateTransformGeneralize(source,
            transform);
               
                // lazily handle the case where we need to deep clone
                if(generalized != source) {
                    cloned = true;
                    if(elements == null) {
                        elements = new Geometry[collection.getNumGeometries()];
                        for (int j = 0; j < i; j++) {
                            Geometry element = source.getGeometryN(j);
                            elements[j] = element;
                            accumulateGeometryType(elementType, element);
                        }
                    }
                }
                if(cloned) {
                    elements[i] = generalized;
                    elementType = accumulateGeometryType(elementType, generalized);
                }
      }
      if(cloned) {
          if(elementType == Point.class) {
              Point[] points = new Point[elements.length];
              System.arraycopy(elements, 0, points, 0, elements.length);
                    return collection.getFactory().createMultiPoint(points);
                } else if (elementType == LineString.class) {
                    LineString[] lines = new LineString[elements.length];
                    System.arraycopy(elements, 0, lines, 0, elements.length);
                    return collection.getFactory().createMultiLineString(lines);
                } else if (elementType == Polygon.class) {
                    Polygon[] polys = new Polygon[elements.length];
                    System.arraycopy(elements, 0, polys, 0, elements.length);
                    return collection.getFactory().createMultiPolygon(polys);
                } else {
                    return collection.getFactory().createGeometryCollection(elements);
          }
      } else {
          return collection;
      }
    } else if (geometry instanceof Point) {
      LiteCoordinateSequence seq = (LiteCoordinateSequence) ((Point) geometry)
          .getCoordinateSequence();
            decimateTransformGeneralize(seq, transform, false, spanx, spany);
            return geometry;
    } else if (geometry instanceof Polygon) {
      Polygon polygon = (Polygon) geometry;
            LinearRing shell = (LinearRing) decimateTransformGeneralize(polygon.getExteriorRing(),
                    transform);
            boolean cloned = shell != polygon.getExteriorRing();
            final int length = polygon.getNumInteriorRing();
            LinearRing[] holes = cloned ? new LinearRing[length] : null;
      for (int i = 0; i < length; i++) {
                LineString hole = polygon.getInteriorRingN(i);
                LinearRing generalized = (LinearRing) decimateTransformGeneralize(hole,
            transform);
                cloned |= generalized != hole;
                if (cloned) {
                    if (holes == null) {
                        holes = new LinearRing[length];
                        for (int j = 0; j < i; j++) {
                            holes[j] = (LinearRing) polygon.getInteriorRingN(j);
                        }
                    }
                    holes[i] = generalized;
                }
            }

            if (cloned) {
                return polygon.getFactory().createPolygon(shell, holes);
            } else {
                return polygon;
      }
    } else if (geometry instanceof LineString) {
            double spanx = this.spanx;
            double spany = this.spany;
      LineString ls = (LineString) geometry;
            if (ls instanceof CurvedGeometry<?>) {
                CurvedGeometry<LineString> curved = (CurvedGeometry<LineString>) ls;
                ls = curved.linearize(Math.min(Math.abs(spanx), Math.abs(spany)));
                // do not generalize further, we already got a good representation
                spanx = -1;
                spany = -1;
            }
            LiteCoordinateSequence seq = (LiteCoordinateSequence) ls.getCoordinateSequence();
            boolean loop = ls instanceof LinearRing;
            if (!loop && seq.size() > 1) {
                double x0 = seq.getOrdinate(0, 0);
                double y0 = seq.getOrdinate(0, 1);
                double x1 = seq.getOrdinate(seq.size() - 1, 0);
                double y1 = seq.getOrdinate(seq.size() - 1, 1);
                loop = Math.abs(x0 - x1) < EPS && Math.abs(y0 - y1) < EPS;
            }
            decimateTransformGeneralize(seq, transform, loop, spanx, spany);
            return ls;
        } else {
            return geometry;
    }
  }

    private Class accumulateGeometryType(Class elementType, Geometry generalized) {
        Class<? extends Geometry> geometryType = generalized.getClass();
        if(elementType == null) {
            elementType = geometryType;
        } else if(elementType != geometryType && elementType != Geometry.class) {
            if(elementType.isAssignableFrom(geometryType)) {
                // nothing to do
            } else if(geometryType.isAssignableFrom(elementType)) {
                elementType = geometryType;
            } else {
                elementType = Geometry.class;
            }
        }
        return elementType;
    }

  /**
   * decimates JTS geometries.
   */
  public final void decimate(Geometry geom) {
    if (spanx == -1)
      return;
    if (geom instanceof MultiPoint) {
      // TODO check geometry and if its bbox is too small turn it into a 1
      // point geom
      return;
    }
    if (geom instanceof GeometryCollection) {
      // TODO check geometry and if its bbox is too small turn it into a
      // 1-2 point geom
      // takes a bit of work because the geometry will need to be
      // recreated.
      GeometryCollection collection = (GeometryCollection) geom;
      final int numGeometries = collection.getNumGeometries();
      for (int i = 0; i < numGeometries; i++) {
        decimate(collection.getGeometryN(i));
      }
    } else if (geom instanceof LineString) {
      LineString line = (LineString) geom;
      LiteCoordinateSequence seq = (LiteCoordinateSequence) line
          .getCoordinateSequence();
      if (decimateOnEnvelope(line, seq)) {
        return;
      }
      decimate(line, seq);
    } else if (geom instanceof Polygon) {
      Polygon line = (Polygon) geom;
      decimate(line.getExteriorRing());
      final int numRings = line.getNumInteriorRing();
      for (int i = 0; i < numRings; i++) {
        decimate(line.getInteriorRingN(i));
      }
    }
  }

  /**
   * @param geom
   * @param seq
   */
  private boolean decimateOnEnvelope(Geometry geom, LiteCoordinateSequence seq) {
    Envelope env = geom.getEnvelopeInternal();
    if (env.getWidth() <= spanx && env.getHeight() <= spany) {
        if(geom instanceof LinearRing) {
            decimateRingFully(seq);
            return true;
        } else {
          double[] coords = seq.getArray();
          int dim = seq.getDimension();
          double[] newcoords = new double[dim * 2];
          for (int i = 0; i < dim; i++) {
            newcoords[i] = coords[i];
            newcoords[dim + i] = coords[coords.length - dim + i];
          }
          seq.setArray(newcoords);
          return true;
        }
    }
    return false;
  }
 
  /**
   * Makes sure the ring is turned into a minimal 3 non equal points one
   * @param ring
   */
  private void decimateRingFully(LiteCoordinateSequence seq) {
      double[] coords = seq.getArray();
        int dim = seq.getDimension();
       
        // degenerate one, it's not even a triangle, or just a triangle
        if(seq.size() <= 4)
            return;
       
        double[] newcoords = new double[dim * 4];
        // assuming the ring makes sense in the first place (i.e., it's at least a triangle),
        // we copy the first two and the last two points
        for (int i = 0; i < dim; i++) {
            newcoords[i] = coords[i];
            newcoords[dim + i] = coords[dim + i];
            newcoords[dim * 2 + i] = coords[coords.length - dim * 2 + i];
            newcoords[dim * 3 + i] = coords[coords.length - dim + i];
        }
        seq.setArray(newcoords);
  }

  /**
   * 1. remove any points that are within the spanx,spany. We ALWAYS keep 1st
   * and last point 2. transform to screen coordinates 3. remove any points
   * that are close (span <1)
   *
   * @param seq
   * @param tranform
   */
  private final void decimateTransformGeneralize(LiteCoordinateSequence seq,
            MathTransform transform, boolean ring, double spanx, double spany)
            throws TransformException {
    // decimates before XFORM
    int ncoords = seq.size();
    double coords[] = null;
    if(transform != null) {
        coords = seq.getOrdinateArray(transform.getSourceDimensions());
    } else {
        coords = seq.getXYArray();
    }

    if (ncoords < 2) {
      if (ncoords == 1) // 1 coordinate -- just xform it
      {
        // double[] newCoordsXformed2 = new double[2];
          if(transform != null) {
              transform.transform(coords, 0, coords, 0, 1);
              seq.setArray(coords, 2);
          }
        return;
      } else
        return; // ncoords =0
    }
   
    // if spanx/spany is -1, then no generalization should be done and all
        // coordinates can just be transformed directly
        if (spanx == -1 && spany == -1) {
            // do the xform if needed
            if ((transform != null) && (!transform.isIdentity())) {
                transform.transform(coords, 0, coords, 0, ncoords);
                seq.setArray(coords, 2);
            }
            return;
        }

        // generalize, use the heavier algorithm for longer lines
        int actualCoords = spanBasedGeneralize(ncoords, coords, spanx, spany);
        if(DP_THRESHOLD > 0 && actualCoords > DP_THRESHOLD) {
            actualCoords = dpBasedGeneralize(actualCoords, coords, Math.min(spanx, spany) * Math.min(spanx, spany));
        }
   
    // handle rings
    if(ring && actualCoords <= 3) {
        if(coords.length > 6) {
            // normal rings
          coords[2] = coords[2];
          coords[3] = coords[3];
          coords[4] = coords[4];
          coords[5] = coords[5];
          actualCoords = 3;
        } else if(coords.length > 4){
            // invalid rings, they do A-B-A, that is, two overlapping lines
            coords[2] = coords[2];
                coords[3] = coords[3];
                actualCoords = 2;
        }
    }
   
    // always have last one
    coords[actualCoords * 2] = coords[(ncoords - 1) * 2];
    coords[actualCoords * 2 + 1] = coords[(ncoords - 1) * 2 + 1];
    actualCoords++;

    // DO THE XFORM
    if ((transform == null) || (transform.isIdentity())) {
        // no actual xform
    } else {
        transform.transform(coords, 0, coords, 0, actualCoords);
    }

    // stick back into the coordinate sequence
    if(actualCoords * 2 < coords.length) {
        double[] seqDouble = new double[2 * actualCoords];
        System.arraycopy(coords, 0, seqDouble, 0, actualCoords * 2);
        seq.setArray(seqDouble, 2);
    } else {
        seq.setArray(coords, 2);
    }
  }

    private int spanBasedGeneralize(int ncoords, double[] coords, double spanx, double spany) {
        int actualCoords = 1;
    double lastX = coords[0];
    double lastY = coords[1];
    for (int t = 1; t < (ncoords - 1); t++) {
      // see if this one should be added
      double x = coords[t * 2];
      double y = coords[t * 2 + 1];
      if ((Math.abs(x - lastX) > spanx) || (Math.abs(y - lastY)) > spany) {
        coords[actualCoords * 2] = x;
        coords[actualCoords * 2 + 1] = y;
        lastX = x;
        lastY = y;
        actualCoords++;
      }
    }
        return actualCoords;
    }
   
    private int dpBasedGeneralize(int ncoords, double[] coords, double maxDistance) {
        while(coords[0] == coords[(ncoords - 1) * 2] && coords[1] == coords[2 * ncoords - 1] && ncoords > 0) {
            ncoords--;
        }
        if(ncoords == 0) {
            return 0;
        }
       
        dpSimplifySection(0, ncoords - 1, coords, maxDistance);
        int actualCoords = 1;
        for(int i = 1; i < ncoords - 1; i++) {
            final double x = coords[i * 2];
            final double y = coords[i * 2 + 1];
            if(!Double.isNaN(x)) {
                coords[actualCoords * 2] = x;
                coords[actualCoords * 2 + 1] = y;
                actualCoords++;
            }
        }
       
        return actualCoords;
    }

  private void dpSimplifySection(int first, int last, double[] coords, double maxDistanceSquared) {
      if(last - 1 <= first) {
          return;
      }
     
      double x0 = coords[first * 2];
      double y0 = coords[first * 2 + 1];
      double x1 = coords[last * 2];
      double y1 = coords[last * 2 + 1];
      double dx = x1 - x0;
      double dy = y1 - y0;
      double ls = dx * dx + dy * dy;
     
      int idx = -1;
      double dsmax = -1;
      for (int i = first + 1; i < last; i++) {
            double x = coords[i * 2];
            double y = coords[i * 2 + 1];
         
            double ds;
            double r = ((x - x0) * dx + (y - y0) * dy/ ls;
            if (r <= 0.0) {
                ds = (x - x0) * (x - x0) + (y - y0) * (y - y0);
            } else if (r >= 1.0) {
                ds = (x - x1) * (x - x1) + (y - y1) * (y - y1);
            } else {
                double s = ((y0 - y) * dx - (x0 - x) *dy) / ls;
                ds = s * s * ls;
            }
           
            if(idx == -1 || ds > dsmax) {
                idx = i;
                dsmax = ds;
            }
        }
     
      if(dsmax <= maxDistanceSquared) {
          for (int i = first + 1; i < last; i++) {
                coords[i * 2] = Double.NaN;
                coords[i * 2 + 1] = Double.NaN;
            }
      } else {
          dpSimplifySection(first, idx, coords, maxDistanceSquared);
          dpSimplifySection(idx, last, coords, maxDistanceSquared);
      }
    }

    private void decimate(Geometry g, LiteCoordinateSequence seq) {
    double[] coords = seq.getXYArray();
    int dim = seq.getDimension();
    int numDoubles = coords.length;
    int readDoubles = 0;
    double prevx, currx, prevy, curry, diffx, diffy;
    for (int currentDoubles = 0; currentDoubles < numDoubles; currentDoubles += dim) {
      if (currentDoubles >= dim && currentDoubles < numDoubles - dim) {
        prevx = coords[readDoubles - dim];
        currx = coords[currentDoubles];
        diffx = Math.abs(prevx - currx);
        prevy = coords[readDoubles - dim + 1];
        curry = coords[currentDoubles + 1];
        diffy = Math.abs(prevy - curry);
        if (diffx > spanx || diffy > spany) {
          readDoubles = copyCoordinate(coords, dim, readDoubles,
              currentDoubles);
        }
      } else {
        readDoubles = copyCoordinate(coords, dim, readDoubles,
            currentDoubles);
      }
    }
    if(g instanceof LinearRing && readDoubles < dim * 4) {
        decimateRingFully(seq);
    } else {
        if(readDoubles < numDoubles) {
            double[] newCoords = new double[readDoubles];
            System.arraycopy(coords, 0, newCoords, 0, readDoubles);
            seq.setArray(newCoords);
        }
    }
  }

  /**
   * @param coords
   * @param dimension
   * @param readDoubles
   * @param currentDoubles
   */
  private int copyCoordinate(double[] coords, int dimension, int readDoubles,
      int currentDoubles) {
    for (int i = 0; i < dimension; i++) {
      coords[readDoubles + i] = coords[currentDoubles + i];
    }
    readDoubles += dimension;
    return readDoubles;
  }

}
TOP

Related Classes of org.geotools.geometry.jts.Decimator

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.