Class ShapeWriter

  1 /*
  2  * Copyright (c) 2016 Vivid Solutions.
  3  *
  4  * All rights reserved. This program and the accompanying materials
  5  * are made available under the terms of the Eclipse Public License 2.0
  6  * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
  7  * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
  8  * and the Eclipse Distribution License is available at
  9  *
 10  * http://www.eclipse.org/org/documents/edl-v10.php.
 11  */
 12 package org.locationtech.jts.awt;
 13  
 14 import java.awt.Shape;
 15 import java.awt.geom.GeneralPath;
 16 import java.awt.geom.Point2D;
 17  
 18 import org.locationtech.jts.geom.Coordinate;
 19 import org.locationtech.jts.geom.Geometry;
 20 import org.locationtech.jts.geom.GeometryCollection;
 21 import org.locationtech.jts.geom.LineString;
 22 import org.locationtech.jts.geom.MultiLineString;
 23 import org.locationtech.jts.geom.Point;
 24 import org.locationtech.jts.geom.Polygon;
 25  
 26  
 27  
 28 /**
 29  * Writes {@link Geometry}s into Java2D {@link Shape} objects
 30  * of the appropriate type.
 31  * This supports rendering geometries using Java2D.
 32  * The ShapeWriter allows supplying a {@link PointTransformation}
 33  * class, to transform coordinates from model space into view space.
 34  * This is useful if a client is providing its own transformation
 35  * logic, rather than relying on Java2D <tt>AffineTransform</tt>s.
 36  * <p>
 37  * The writer supports removing duplicate consecutive points
 38  * (via the {@link #setRemoveDuplicatePoints(boolean)} method) 
 39  * as well as true <b>decimation</b>
 40  * (via the {@link #setDecimation(double)} method. 
 41  * Enabling one of these strategies can substantially improve 
 42  * rendering speed for large geometries.
 43  * It is only necessary to enable one strategy.
 44  * Using decimation is preferred, but this requires 
 45  * determining a distance below which input geometry vertices
 46  * can be considered unique (which may not always be feasible).
 47  * If neither strategy is enabled, all vertices
 48  * of the input <tt>Geometry</tt>
 49  * will be represented in the output <tt>Shape</tt>.
 50  * <p>
 51  * 
 52  */
 53 public class ShapeWriter 
 54 {
 55     /**
 56      * The point transformation used by default.
 57      */
 58     public static final PointTransformation DEFAULT_POINT_TRANSFORMATION = new IdentityPointTransformation();
 59     
 60     /**
 61      * The point shape factory used by default.
 62      */
 63     public static final PointShapeFactory DEFAULT_POINT_FACTORY = new PointShapeFactory.Square(3.0);
 64     
 65     private PointTransformation pointTransformer = DEFAULT_POINT_TRANSFORMATION;
 66     private PointShapeFactory pointFactory = DEFAULT_POINT_FACTORY;
 67  
 68     /**
 69      * Cache a Point2D object to use to transfer coordinates into shape
 70      */
 71     private Point2D transPoint = new Point2D.Double();
 72  
 73     /**
 74      * If true, decimation will be used to reduce the number of vertices
 75      * by removing consecutive duplicates.
 76      * 
 77      */
 78     private boolean doRemoveDuplicatePoints = false;
 79     
 80     private double decimationDistance = 0;
 81     
 82     /**
 83      * Creates a new ShapeWriter with a specified point transformation
 84      * and point shape factory.
 85      * 
 86      * @param pointTransformer a transformation from model to view space to use 
 87      * @param pointFactory the PointShapeFactory to use
 88      */
 89     public ShapeWriter(PointTransformation pointTransformer, PointShapeFactory pointFactory) 
 90     {
 91         if (pointTransformer != null)
 92             this.pointTransformer = pointTransformer;
 93         if (pointFactory != null)
 94             this.pointFactory = pointFactory;
 95     }
 96  
 97     /**
 98      * Creates a new ShapeWriter with a specified point transformation
 99      * and the default point shape factory.
100      * 
101      * @param pointTransformer a transformation from model to view space to use 
102      */
103     public ShapeWriter(PointTransformation pointTransformer) 
104     {
105         this(pointTransformer, null);
106     }
107  
108     /**
109      * Creates a new ShapeWriter with the default (identity) point transformation.
110      *
111      */
112     public ShapeWriter() {
113     }
114  
115     /**
116      * Sets whether duplicate consecutive points should be eliminated.
117      * This can reduce the size of the generated Shapes
118      * and improve rendering speed, especially in situations
119      * where a transform reduces the extent of the geometry.
120      * <p>
121      * The default is <tt>false</tt>.
122      * 
123      * @param doDecimation whether decimation is to be used
124      */
125   public void setRemoveDuplicatePoints(boolean doRemoveDuplicatePoints)
126   {
127     this.doRemoveDuplicatePoints = doRemoveDuplicatePoints;
128   }
129   
130   /**
131    * Sets the decimation distance used to determine
132    * whether vertices of the input geometry are 
133    * considered to be duplicate and thus removed.
134    * The distance is axis distance, not Euclidean distance.
135    * The distance is specified in the input geometry coordinate system
136    * (NOT the transformed output coordinate system).
137    * <p>
138    * When rendering to a screen image, a suitably small distance should be used
139    * to avoid obvious rendering defects.  
140    * A distance equivalent to the equivalent of 1.5 pixels or less is recommended
141    * (and perhaps even smaller to avoid any chance of visible artifacts).
142    * <p>
143    * The default distance is 0.0, which disables decimation.
144    * 
145    * @param decimationDistance the distance below which vertices are considered to be duplicates
146    */
147   public void setDecimation(double decimationDistance)
148   {
149     this.decimationDistance = decimationDistance;
150   }
151   
152     /**
153      * Creates a {@link Shape} representing a {@link Geometry}, 
154      * according to the specified PointTransformation
155      * and PointShapeFactory (if relevant).
156      * <p>
157      * Note that Shapes do not
158      * preserve information about which elements in heterogeneous collections
159      * are 1D and which are 2D.
160      * For example, a GeometryCollection containing a ring and a
161      * disk will render as two disks if Graphics.fill is used, 
162      * or as two rings if Graphics.draw is used.
163      * To avoid this issue use separate shapes for the components.
164      * 
165      * @param geometry the geometry to convert
166      * @return a Shape representing the geometry
167      */
168     public Shape toShape(Geometry geometry)
169     {
170         if (geometry.isEmpty()) return new GeneralPath();
171         if (geometry instanceof Polygon) return toShape((Polygon) geometry);
172         if (geometry instanceof LineString)             return toShape((LineString) geometry);
173         if (geometry instanceof MultiLineString)     return toShape((MultiLineString) geometry);
174         if (geometry instanceof Point)             return toShape((Point) geometry);
175         if (geometry instanceof GeometryCollection) return toShape((GeometryCollection) geometry);
176  
177         throw new IllegalArgumentException(
178             "Unrecognized Geometry class: " + geometry.getClass());
179     }
180  
181     private Shape toShape(Polygon p) 
182     {
183         PolygonShape poly = new PolygonShape();
184         
185         appendRing(poly, p.getExteriorRing().getCoordinates());
186         for (int j = 0; j < p.getNumInteriorRing(); j++) {
187           appendRing(poly, p.getInteriorRingN(j).getCoordinates());
188         }
189  
190         return poly;
191     }
192  
193     private void appendRing(PolygonShape poly, Coordinate[] coords) 
194     {
195       if (coords.length == 0return;
196       
197     double prevx = Double.NaN;
198     double prevy = Double.NaN;
199     Coordinate prev = null;
200     
201     int n = coords.length - 1;
202     /**
203      * Don't include closing point.
204      * Ring path will be closed explicitly, which provides a 
205      * more accurate path representation.
206      */
207         for (int i = 0; i < n; i++) {
208           
209           if (decimationDistance > 0.0) {
210             boolean isDecimated = prev != null 
211               && Math.abs(coords[i].x - prev.x) < decimationDistance
212               && Math.abs(coords[i].y - prev.y) < decimationDistance;
213             if (i < n && isDecimated) 
214               continue;
215             prev = coords[i];
216           }
217           
218             transformPoint(coords[i], transPoint);
219             
220             if (doRemoveDuplicatePoints) {
221         // skip duplicate points (except the last point)
222               boolean isDup = transPoint.getX() == prevx && transPoint.getY() == prevy;
223         if (i < n && isDup)
224           continue;
225         prevx = transPoint.getX();
226         prevy = transPoint.getY();
227             }
228             poly.addToRing(transPoint);
229         }
230         // handle closing point
231         poly.endRing();
232     }
233     
234     private Shape toShape(GeometryCollection gc)
235     {
236         GeometryCollectionShape shape = new GeometryCollectionShape();
237         // add components to GC shape
238         for (int i = 0; i < gc.getNumGeometries(); i++) {
239             Geometry g = (Geometry) gc.getGeometryN(i);
240             shape.add(toShape(g));
241         }
242         return shape;
243     }
244  
245     private GeneralPath toShape(MultiLineString mls)
246     {
247         GeneralPath path = new GeneralPath();
248  
249         for (int i = 0; i < mls.getNumGeometries(); i++) {
250             LineString lineString = (LineString) mls.getGeometryN(i);
251             path.append(toShape(lineString), false);
252         }
253         return path;
254     }
255  
256     private GeneralPath toShape(LineString lineString)
257     {
258         GeneralPath shape = new GeneralPath();
259         
260     Coordinate prev = lineString.getCoordinateN(0);
261     transformPoint(prev, transPoint);
262         shape.moveTo((floattransPoint.getX(), (floattransPoint.getY());
263  
264     double prevx = transPoint.getX();
265     double prevy = transPoint.getY();
266     
267     int n = lineString.getNumPoints() - 1;
268     //int count = 0;
269     for (int i = 1; i <= n; i++) {
270       Coordinate currentCoord = lineString.getCoordinateN(i);
271       if (decimationDistance > 0.0) {
272         boolean isDecimated = prev != null
273             && Math.abs(currentCoord.x - prev.x) < decimationDistance
274             && Math.abs(currentCoord.y - prev.y) < decimationDistance;
275         if (i < n && isDecimated) {
276           continue;
277         }
278         prev = currentCoord;
279       }
280  
281       transformPoint(currentCoord, transPoint);
282  
283             if (doRemoveDuplicatePoints) {
284               // skip duplicate points (except the last point)
285               boolean isDup = transPoint.getX() == prevx && transPoint.getY() == prevy;
286               if (i < n && isDup)
287                 continue;
288               prevx = transPoint.getX();
289               prevy = transPoint.getY();
290               //count++;
291             }
292             shape.lineTo((floattransPoint.getX(), (floattransPoint.getY());
293         }
294         //System.out.println(count);
295         return shape;
296     }
297  
298     private Shape toShape(Point point)
299   {
300         Point2D viewPoint = transformPoint(point.getCoordinate());
301         return pointFactory.createPoint(viewPoint);
302     }
303  
304   private Point2D transformPoint(Coordinate model) {
305         return transformPoint(model, new Point2D.Double());
306     }
307   
308   private Point2D transformPoint(Coordinate model, Point2D view) {
309         pointTransformer.transform(model, view);
310         return view;
311     }
312 }
313