/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI * for visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.workbench.ui.renderer.java2D; import java.awt.Shape; import java.awt.geom.GeneralPath; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Line2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.Point2D.Double; import java.awt.geom.PathIterator; import java.util.ArrayList; 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.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; /** * Converts JTS Geometry objects into Java 2D Shape objects */ // Optimizations from larry becker's SkyJUMP code to OpenJUMP [mmichaud] // 1 - Added point decimation to toViewCoordinates. // Results in much improved render speeds when displaying full extent of large datasets. // 2 - Optimized the toShape conversion for linestrings. // Reduced darw times by 60%. // 3 - Made toViewCoordinates(Coordinate[]) public to make use // of its decimation optimization in AbstractSelectionRenderer. public class Java2DConverter { private static double POINT_MARKER_SIZE = 3.0; private PointConverter pointConverter; // Add the resolution of the decimator as an option to be able to choose more easily // between speed and quality (ex. speed is preferred for light-gray display while // dragging the zoombar or while using the mousewheel) [mmichaud 2007-05-27] // Default resolution for the decimator is half a pixel as discussed on the list private double decimatorResolution = 0.5; public Java2DConverter(PointConverter pointConverter) { this.pointConverter = pointConverter; } // Add a constructor to choose another decimatorResolution public Java2DConverter(PointConverter pointConverter, double resolution) { this.pointConverter = pointConverter; this.decimatorResolution = resolution; } private Shape toShape(Polygon p) throws NoninvertibleTransformException { ArrayList holeVertexCollection = new ArrayList(); for (int j = 0; j < p.getNumInteriorRing(); j++) { holeVertexCollection.add( toViewCoordinates(p.getInteriorRingN(j).getCoordinates())); } return new PolygonShape( toViewCoordinates(p.getExteriorRing().getCoordinates()), holeVertexCollection); } public Coordinate[] toViewCoordinates(Coordinate[] modelCoordinates) throws NoninvertibleTransformException { Coordinate[] viewCoordinates = new Coordinate[modelCoordinates.length]; double ps = decimatorResolution / pointConverter.getScale(); // convert in model units Coordinate p0 = modelCoordinates[0]; int npts = 0; int mpts = modelCoordinates.length; for (int i = 0; i < mpts; i++) { Coordinate pi = modelCoordinates[i]; //inline Decimator double xd = Math.abs(p0.x-pi.x); double yd = Math.abs(p0.y-pi.y); if ((xd>=ps) || (yd>=ps) || (npts<4) || (i == mpts-1)) { //LDB: have replaced the following with inline code but // it was no faster. AffineTransform must be highly optimized! Point2D point2D = pointConverter.toViewPoint(pi); viewCoordinates[npts++] = new Coordinate(point2D.getX(), point2D.getY()); p0 = pi; } } if (npts != mpts) { Coordinate[] viewCoordinates2 = new Coordinate[npts]; for (int i = 0; i < npts; i++) { viewCoordinates2[i] = viewCoordinates[i]; } return viewCoordinates2; // LDB: benchmarkes verify that the following line is slower than // the above loop probably because of copying vs. referencing //return Arrays.copyOfRange(viewCoordinates,0,npts); } else return viewCoordinates; } private Shape toShape(GeometryCollection gc) throws NoninvertibleTransformException { GeometryCollectionShape shape = new GeometryCollectionShape(); for (int i = 0; i < gc.getNumGeometries(); i++) { Geometry g = (Geometry) gc.getGeometryN(i); shape.add(toShape(g)); } return shape; } private GeneralPath toShape(MultiLineString mls) throws NoninvertibleTransformException { GeneralPath path = new GeneralPath(); for (int i = 0; i < mls.getNumGeometries(); i++) { LineString lineString = (LineString) mls.getGeometryN(i); path.append(toShape(lineString), false); } //BasicFeatureRenderer expects LineStrings and MultiLineStrings to be //converted to GeneralPaths. [Jon Aquino] return path; } class LineStringPath implements PathIterator { private int iterate; private int numPoints; private Coordinate[] points; private boolean closed; public LineStringPath(LineString linestring, Java2DConverter j2D){ try { points = j2D.toViewCoordinates(linestring.getCoordinates()); } catch (NoninvertibleTransformException ex){ } this.numPoints = points.length; iterate = 0; closed = (numPoints>1) && (points[0].equals2D(points[numPoints-1])); } private int getSegType(){ // tip from Larry Becker for a better rendering 2007-07-13 [mmichaud] if (closed && (iterate == numPoints-1)) return PathIterator.SEG_CLOSE; return (iterate==0) ? PathIterator.SEG_MOVETO : PathIterator.SEG_LINETO; } public int currentSegment(double[] coords) { coords[0] = points[iterate].x; coords[1] = points[iterate].y; return getSegType(); } public int currentSegment(float[] coords) { coords[0] = (float) points[iterate].x; coords[1] = (float) points[iterate].y; return getSegType(); } public int getWindingRule() { return GeneralPath.WIND_NON_ZERO; } public boolean isDone() { return !(iterate < numPoints); } public void next() { iterate++; } } // Method rewritten by mmichaud on 2011-03-05 // to avoid drawing out of the viewport /* private GeneralPath toShape_(LineString lineString) throws NoninvertibleTransformException { int numPoints = lineString.getNumPoints(); GeneralPath shape = new GeneralPath(GeneralPath.WIND_NON_ZERO, numPoints); PathIterator pi = new LineStringPath(lineString, this); shape.append(pi,false); //Point2D viewPoint = toViewPoint(lineString.getCoordinateN(0)); //shape.moveTo((float) viewPoint.getX(), (float) viewPoint.getY()); // //for (int i = 1; i < lineString.getNumPoints(); i++) { // viewPoint = toViewPoint(lineString.getCoordinateN(i)); // shape.lineTo((float) viewPoint.getX(), (float) viewPoint.getY()); //} //BasicFeatureRenderer expects LineStrings and MultiLineStrings to be //converted to GeneralPaths. [Jon Aquino] return shape; } */ // New toShape method for LineString [mmichaud 2011-03-05] // This new method exclude all segments entirely out of the viewPort from // the general path private GeneralPath toShape(LineString lineString) throws NoninvertibleTransformException { com.vividsolutions.jts.geom.GeometryFactory gf = lineString.getFactory(); Coordinate[] cc = lineString.getCoordinates(); GeneralPath shape = new GeneralPath(GeneralPath.WIND_NON_ZERO, cc.length); com.vividsolutions.jts.geom.CoordinateList list = new com.vividsolutions.jts.geom.CoordinateList(); Envelope view = pointConverter.getEnvelopeInModelCoordinates(); Coordinate c0 = cc[0]; boolean start = true; for (int i = 1, max = cc.length ; i < max ; i++) { Coordinate c1 = cc[i]; if (view.intersects(new Envelope(c0, c1))) { if (start) list.add(c0); list.add(c1); start = false; } else if (!list.isEmpty()) { PathIterator pi = new LineStringPath(gf.createLineString(list.toCoordinateArray()), this); shape.append(pi,false); list.clear(); start = true; } else {/**list empty, do nothing*/} if (i == max-1 && !list.isEmpty()) { PathIterator pi = new LineStringPath(gf.createLineString(list.toCoordinateArray()), this); shape.append(pi,false); } c0 = c1; } return shape; } private Shape toShape(Point point) throws NoninvertibleTransformException { Rectangle2D.Double pointMarker = new Rectangle2D.Double( 0.0, 0.0, POINT_MARKER_SIZE, POINT_MARKER_SIZE); Point2D viewPoint = toViewPoint(point.getCoordinate()); pointMarker.x = (double) (viewPoint.getX() - (POINT_MARKER_SIZE / 2)); pointMarker.y = (double) (viewPoint.getY() - (POINT_MARKER_SIZE / 2)); return pointMarker; } private Point2D toViewPoint(Coordinate modelCoordinate) throws NoninvertibleTransformException { //Do the rounding now; don't rely on Java 2D rounding, because it //seems to do it differently for drawing and filling, resulting in the draw //being a pixel off from the fill sometimes. [Jon Aquino] //LDB 04/25/2007: this assumption doesn't seem to be true any longer Point2D viewPoint = pointConverter.toViewPoint(modelCoordinate); //Optimization recommended by Todd Warnes [Jon Aquino 2004-02-06] //viewPoint.setLocation( // Math.round(viewPoint.getX()), // Math.round(viewPoint.getY())); return viewPoint; } public static interface PointConverter { public Point2D toViewPoint(Coordinate modelCoordinate) throws NoninvertibleTransformException; public double getScale() throws NoninvertibleTransformException; public Envelope getEnvelopeInModelCoordinates(); } /** * If you pass in a general GeometryCollection, note that a Shape cannot * preserve information about which elements are 1D and which are 2D. * For example, if you pass in a GeometryCollection containing a ring and a * disk, you cannot render them as such: if you use Graphics.fill, you'll get * two disks, and if you use Graphics.draw, you'll get two rings. Solution: * create Shapes for each element. */ public Shape toShape(Geometry geometry) throws NoninvertibleTransformException { // [NOTE] I tested a short-circuit here to return a 1 pixel representation of // geometries having an envelope of less than 1/2 pixel, but I get only an ugly // render for a small performance improvement [mmichaud - 2007-05-23] if (geometry.isEmpty()) { return new GeneralPath(); } if (geometry instanceof Polygon) { return toShape((Polygon) geometry); } if (geometry instanceof MultiPolygon) { return toShape((MultiPolygon) geometry); } if (geometry instanceof LineString) { return toShape((LineString) geometry); } if (geometry instanceof MultiLineString) { return toShape((MultiLineString) geometry); } if (geometry instanceof Point) { return toShape((Point) geometry); } if (geometry instanceof GeometryCollection) { return toShape((GeometryCollection) geometry); } throw new IllegalArgumentException( "Unrecognized Geometry class: " + geometry.getClass()); } }