/*
 * The JTS Topology Suite is a collection of Java classes that
 * implement the fundamental operations required to validate a given
 * geo-spatial data set to a known topological specification.
 *
 * Copyright (C) 2001 Vivid Solutions
 *
 * 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; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; 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.jts.awt;

import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.ArrayList;

import com.vividsolutions.jts.geom.*;


/**
 * Writes {@link Geometry}s into Java2D {@link Shape} objects
 */
public class ShapeWriter 
{
	/**
	 * The point transformation used by default.
	 */
	public static final PointTransformation DEFAULT_POINT_TRANSFORMATION = new IdentityPointTransformation();
	
	/**
	 * The point shape factory used by default.
	 */
	public static final PointShapeFactory DEFAULT_POINT_FACTORY = new PointShapeFactory.Square(3.0);
	
	private PointTransformation pointTransformer = DEFAULT_POINT_TRANSFORMATION;
	private PointShapeFactory pointFactory = DEFAULT_POINT_FACTORY;

	/**
	 * Cache a Point2D object to use to transfer coordinates into shape
	 */
	private Point2D transPoint = new Point2D.Double();

	/**
	 * Creates a new ShapeWriter with a specified point transformation
	 * and point shape factory.
	 * 
	 * @param pointTransformer a transformation from model to view space to use 
	 * @param pointFactory the PointShapeFactory to use
	 */
	public ShapeWriter(PointTransformation pointTransformer, PointShapeFactory pointFactory) 
	{
		if (pointTransformer != null)
			this.pointTransformer = pointTransformer;
		if (pointFactory != null)
			this.pointFactory = pointFactory;
	}

	/**
	 * Creates a new ShapeWriter with a specified point transformation
	 * and the default point shape factory.
	 * 
	 * @param pointTransformer a transformation from model to view space to use 
	 */
	public ShapeWriter(PointTransformation pointTransformer) 
	{
		this(pointTransformer, null);
	}

	/**
	 * Creates a new ShapeWriter with the default (identity) point transformation.
	 *
	 */
	public ShapeWriter() {
	}

	/**
	 * Creates a {@link Shape} representing a {@link Geometry}, 
	 * according to the specified PointTransformation
	 * and PointShapeFactory (if relevant).
	 * <p>
	 * Note that Shapes do not
	 * preserve information about which elements in heterogeneous collections
	 * are 1D and which are 2D.
	 * For example, a GeometryCollection containing a ring and a
	 * disk will render as two disks if Graphics.fill is used, 
	 * or as two rings if Graphics.draw is used.
	 * To avoid this issue use separate shapes for the components.
	 * 
	 * @param geometry the geometry to convert
	 * @return a Shape representing the geometry
	 */
	public Shape toShape(Geometry geometry)
	{
		if (geometry.isEmpty()) return new GeneralPath();
		if (geometry instanceof Polygon) return toShape((Polygon) 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());
	}

	private Shape toShape(Polygon p) 
	{
		PolygonShape poly = new PolygonShape();
		
		append(poly, p.getExteriorRing().getCoordinates());
		for (int j = 0; j < p.getNumInteriorRing(); j++) {
			append(poly, p.getInteriorRingN(j).getCoordinates());
		}

		return poly;
	}

	private void append(PolygonShape poly, Coordinate[] coords) 
	{
		for (int i = 0; i < coords.length; i++) {
			transformPoint(coords[i], transPoint);
			poly.addToRing(transPoint);
		}
		poly.endRing();
	}
	
	/*
	 // Obsolete (slower code)
	private Shape OLDtoShape(Polygon p) 
	{
		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);
	}

	
	private Coordinate[] toViewCoordinates(Coordinate[] modelCoordinates)
	{
		Coordinate[] viewCoordinates = new Coordinate[modelCoordinates.length];

		for (int i = 0; i < modelCoordinates.length; i++) {
			Point2D point2D = toPoint(modelCoordinates[i]);
			viewCoordinates[i] = new Coordinate(point2D.getX(), point2D.getY());
		}

		return viewCoordinates;
	}
*/
	
	private Shape toShape(GeometryCollection gc)
	{
		GeometryCollectionShape shape = new GeometryCollectionShape();
		// add components to GC shape
		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)
	{
		GeneralPath path = new GeneralPath();

		for (int i = 0; i < mls.getNumGeometries(); i++) {
			LineString lineString = (LineString) mls.getGeometryN(i);
			path.append(toShape(lineString), false);
		}
		return path;
	}

	private GeneralPath toShape(LineString lineString)
	{
		GeneralPath shape = new GeneralPath();
		
		transformPoint(lineString.getCoordinateN(0), transPoint);
		shape.moveTo((float) transPoint.getX(), (float) transPoint.getY());

		for (int i = 1; i < lineString.getNumPoints(); i++) {
			transformPoint(lineString.getCoordinateN(i), transPoint);
			shape.lineTo((float) transPoint.getX(), (float) transPoint.getY());
		}
		return shape;
	}

	private Shape toShape(Point point)
  {
		Point2D viewPoint = transformPoint(point.getCoordinate());
		return pointFactory.createPoint(viewPoint);
	}

  private Point2D transformPoint(Coordinate model) {
		return transformPoint(model, new Point2D.Double());
	}
  
  private Point2D transformPoint(Coordinate model, Point2D view) {
		pointTransformer.transform(model, view);
		/**
		 * Do the rounding now instead of relying on Java 2D rounding. Java2D seems
		 * to do rounding differently for drawing and filling, resulting in the draw
		 * being a pixel off from the fill sometimes.
		 */
		view.setLocation(Math.round(view.getX()), Math.round(view.getY()));
		return view;
	}
}
