/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * (c) 2004-2008 Open Source Geospatial Foundation (LGPL) * This code is licensed under the GPL 2.0 license, available at the root * application directory. * * A modified version of Decimator from Geotools renderer.lite. */ package org.vfny.geoserver.wms.responses.map.htmlimagemap; import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; import org.geotools.geometry.jts.LiteCoordinateSequence; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateSequence; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryCollection; import com.vividsolutions.jts.geom.GeometryFactory; 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. * * @author jeichar */ public final class Decimator { private double spanx = -1; private double spany = -1; /** * 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) { if (screenToWorld != null) { double[] original = new double[] { paintArea.x + paintArea.width / 2.0, paintArea.y + paintArea.height / 2.0, paintArea.x + paintArea.width / 2.0 + 1, paintArea.y + paintArea.height / 2.0 + 1, }; double[] coords = new double[4]; try { screenToWorld.transform(original, 0, coords, 0, 2); } catch (TransformException e) { return; } this.spanx = Math.abs(coords[0] - coords[2]) * 0.8; // 0.8 is just so you dont decimate "too much". magic // number. this.spany = Math.abs(coords[1] - coords[3]) * 0.8; } else { this.spanx = 1; this.spany = 1; } } /** * @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 final void decimateTransformGeneralize(Geometry geometry, MathTransform transform) throws TransformException { if (geometry instanceof GeometryCollection) { GeometryCollection collection = (GeometryCollection) geometry; final int length = collection.getNumGeometries(); for (int i = 0; i < length; i++) { decimateTransformGeneralize(collection.getGeometryN(i), transform); } } else if (geometry instanceof Point) { LiteCoordinateSequence seq = (LiteCoordinateSequence) ((Point) geometry) .getCoordinateSequence(); decimateTransformGeneralize(seq, transform); } else if (geometry instanceof Polygon) { Polygon polygon = (Polygon) geometry; decimateTransformGeneralize(polygon.getExteriorRing(), transform); final int length = polygon.getNumInteriorRing(); for (int i = 0; i < length; i++) { decimateTransformGeneralize(polygon.getInteriorRingN(i), transform); } } else if (geometry instanceof LineString) { LiteCoordinateSequence seq = (LiteCoordinateSequence) ((LineString) geometry) .getCoordinateSequence(); decimateTransformGeneralize(seq, transform); } } private void forceClosed(Coordinate[] coords) { if(!coords[0].equals2D(coords[coords.length-1])) coords[coords.length-1]=coords[0]; } /** * decimates JTS geometries. */ public final Geometry decimate(Geometry geom) { GeometryFactory gFac=new GeometryFactory(geom.getPrecisionModel(),geom.getSRID()); if (spanx == -1) return geom; if (geom instanceof MultiPoint) { // TODO check geometry and if its bbox is too small turn it into a 1 // point geom return geom; } 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; Geometry[] result=new Geometry[collection.getDimension()]; final int numGeometries = collection.getNumGeometries(); for (int i = 0; i < numGeometries; i++) { result[i]=decimate(collection.getGeometryN(i)); } return gFac.createGeometryCollection(result); } else if (geom instanceof LineString) { LineString line = (LineString) geom; CoordinateSequence seq = (CoordinateSequence) line .getCoordinateSequence(); LiteCoordinateSequence lseq=new LiteCoordinateSequence(seq.toCoordinateArray()); if (decimateOnEnvelope(line, lseq)) { if(lseq.size()>=2) return gFac.createLineString(lseq); } if(lseq.size()>=2) return gFac.createLineString(decimate(lseq)); return null; } else if (geom instanceof Polygon) { Polygon line = (Polygon) geom; Coordinate[] exterior=decimate(line.getExteriorRing()).getCoordinates(); forceClosed(exterior); if(exterior.length>3) { LinearRing ring=gFac.createLinearRing(exterior); final int numRings = line.getNumInteriorRing(); List<LinearRing> rings=new ArrayList<LinearRing>(); for (int i = 0; i < numRings; i++) { Coordinate[] interior=decimate(line.getInteriorRingN(i)).getCoordinates(); forceClosed(interior); if(interior.length>3) rings.add(gFac.createLinearRing(interior)); } return gFac.createPolygon(ring, rings.toArray(new LinearRing[] {})); } return null; } return geom; } /** * @param geom * @param seq */ private boolean decimateOnEnvelope(Geometry geom, LiteCoordinateSequence seq) { Envelope env = geom.getEnvelopeInternal(); if (env.getWidth() <= spanx && env.getHeight() <= spany) { 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(coords); return true; } return false; } /** * 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) throws TransformException { // decimates before XFORM int ncoords = seq.size(); double originalOrds[] = seq.getXYArray(); // 2*#of points if (ncoords < 2) { if (ncoords == 1) // 1 coordinate -- just xform it { double[] newCoordsXformed2 = new double[2]; transform.transform(originalOrds, 0, newCoordsXformed2, 0, 1); seq.setArray(newCoordsXformed2); return; } else return; // ncoords =0 } // unfortunately, we have to keep things in double precion until after // the transform or we could move things. double[] allCoords = new double[ncoords * 2]; // preallocate -- might // not be full (throw // away Z) allCoords[0] = originalOrds[0]; // allways have 1st one allCoords[1] = originalOrds[1]; int actualCoords = 1; double lastX = allCoords[0]; double lastY = allCoords[1]; for (int t = 1; t < (ncoords - 1); t++) { // see if this one should be added double x = originalOrds[t * 2]; double y = originalOrds[t * 2 + 1]; if ((Math.abs(x - lastX) > spanx) || (Math.abs(y - lastY)) > spany) { allCoords[actualCoords * 2] = x; allCoords[actualCoords * 2 + 1] = y; lastX = x; lastY = y; actualCoords++; } } allCoords[actualCoords * 2] = originalOrds[(ncoords - 1) * 2]; // always have last one allCoords[actualCoords * 2 + 1] = originalOrds[(ncoords - 1) * 2 + 1]; actualCoords++; double[] newCoordsXformed; // DO THE XFORM if ((transform == null) || (transform.isIdentity())) // no actual // xform { newCoordsXformed = allCoords; } else { newCoordsXformed = new double[actualCoords * 2]; transform .transform(allCoords, 0, newCoordsXformed, 0, actualCoords); } // GENERALIZE -- we should be in screen space so spanx=spany=1.0 // unfortunately, we have to keep things in double precion until after // the transform or we could move things. double[] finalCoords = new double[ncoords * 2]; // preallocate -- might // not be full (throw // away Z) finalCoords[0] = newCoordsXformed[0]; // allways have 1st one finalCoords[1] = newCoordsXformed[1]; int actualCoordsGen = 1; lastX = newCoordsXformed[0]; lastY = newCoordsXformed[1]; for (int t = 1; t < (actualCoords - 1); t++) { // see if this one should be added double x = newCoordsXformed[t * 2]; double y = newCoordsXformed[t * 2 + 1]; if ((Math.abs(x - lastX) > 0.75) || (Math.abs(y - lastY)) > 0.75) // 0.75 // instead of 1 just because it tends to look nicer for slightly // more work. magic number. { finalCoords[actualCoordsGen * 2] = x; finalCoords[actualCoordsGen * 2 + 1] = y; lastX = x; lastY = y; actualCoordsGen++; } } finalCoords[actualCoordsGen * 2] = newCoordsXformed[(actualCoords - 1) * 2]; // always have last one finalCoords[actualCoordsGen * 2 + 1] = newCoordsXformed[(actualCoords - 1) * 2 + 1]; actualCoordsGen++; // stick back in double[] seqDouble = new double[2 * actualCoordsGen]; System.arraycopy(finalCoords, 0, seqDouble, 0, actualCoordsGen * 2); seq.setArray(seqDouble); } private CoordinateSequence decimate(LiteCoordinateSequence seq) { double[] coords = seq.getArray(); int numDoubles = coords.length; int dim = seq.getDimension(); int readDoubles = 0; double prevx, currx, prevy, curry, diffx, diffy; for (int currentDoubles = 0; currentDoubles < numDoubles; currentDoubles += dim) { if (currentDoubles >= dim && currentDoubles < numDoubles - 1) { 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); } } double[] newCoords = new double[readDoubles]; System.arraycopy(coords, 0, newCoords, 0, readDoubles); seq.setArray(newCoords); return seq; } /** * @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; } }