/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2002-2008, Open Source Geospatial Foundation (OSGeo)
* (C) 2010, Geomatys
*
* 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.geotoolkit.data.shapefile.shp;
import java.nio.DoubleBuffer;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import com.vividsolutions.jts.algorithm.CGAlgorithms;
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.LinearRing;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import org.geotoolkit.geometry.jts.JTS;
import org.apache.sis.storage.DataStoreException;
import static org.geotoolkit.data.shapefile.ShapefileFeatureStoreFactory.*;
/**
* Wrapper for a Shapefile polygon.
*
* @author aaime
* @author Ian Schneider
* @author Johann Sorel (Geomatys)
* @version $Id$
* @module
*/
public class PolygonHandler extends AbstractShapeHandler {
protected final List<LinearRing> shells = new ArrayList<LinearRing>();
protected final List<LinearRing> holes = new ArrayList<LinearRing>();
public PolygonHandler(final boolean read3D) {
super(ShapeType.POLYGON,read3D);
}
public PolygonHandler(final ShapeType type, final boolean read3D) throws DataStoreException {
super(type,read3D);
if ((type != ShapeType.POLYGON) && (type != ShapeType.POLYGONM)
&& (type != ShapeType.POLYGONZ)) {
throw new DataStoreException(
"PolygonHandler constructor - expected type to be 5, 15, or 25.");
}
}
// returns true if testPoint is a point in the pointList list.
protected boolean pointInList(final Coordinate testPoint, final Coordinate[] pointList) {
Coordinate p;
for (int t = pointList.length - 1; t >= 0; t--) {
p = pointList[t];
if ((testPoint.x == p.x)
&& (testPoint.y == p.y)
&& ((testPoint.z == p.z) || (!(testPoint.z == testPoint.z)))
// nan
// test;
// x!=x
// iff
// x is
// nan
) {
return true;
}
}
return false;
}
@Override
public ShapeType getShapeType() {
return shapeType;
}
@Override
public int getLength(final Object geometry) {
final MultiPolygon multi;
if (geometry instanceof MultiPolygon) {
multi = (MultiPolygon) geometry;
} else {
multi = GEOMETRY_FACTORY
.createMultiPolygon(new Polygon[] { (Polygon) geometry });
}
int nrings = 0;
for (int t = 0; t < multi.getNumGeometries(); t++) {
final Polygon p = (Polygon) multi.getGeometryN(t);
nrings = nrings + 1 + p.getNumInteriorRing();
}
final int npoints = multi.getNumPoints();
final int length;
if (shapeType == ShapeType.POLYGONZ) {
length = 44 + (4 * nrings) + (16 * npoints) + (8 * npoints) + 16
+ (8 * npoints) + 16;
} else if (shapeType == ShapeType.POLYGONM) {
length = 44 + (4 * nrings) + (16 * npoints) + (8 * npoints) + 16;
} else if (shapeType == ShapeType.POLYGON) {
length = 44 + (4 * nrings) + (16 * npoints);
} else {
throw new IllegalStateException(
"Expected ShapeType of Polygon, got " + shapeType);
}
return length;
}
@Override
public Object estimated(final double minX, final double maxX, final double minY, final double maxY) {
final double[] array = new double[]{
minX,minY,
minX,maxY,
maxX,maxY,
maxX,minY,
minX,minY
};
final LinearRing shell = GEOMETRY_FACTORY.createLinearRing(new ShapeCoordinateSequence2D(array, 5));
return GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {GEOMETRY_FACTORY.createPolygon(shell, null)});
}
@Override
public Object read(final ByteBuffer buffer, final ShapeType type) {
if (type == ShapeType.NULL) {
return createNull();
}
//clear from previous read
shells.clear();
holes.clear();
// skip the bounds
buffer.position(buffer.position() + 32);
final int numParts = buffer.getInt();
final int numPoints = buffer.getInt();
final int[] partOffsets = new int[numParts];
for (int i = 0; i < numParts; i++) {
partOffsets[i] = buffer.getInt();
}
final DoubleBuffer dbuffer = buffer.asDoubleBuffer();
final int dimensions = (read3D && shapeType == ShapeType.POLYGONZ)? 3:2;
//read everything in one round : +2 for minZ/maxZ
final double[] coords = new double[numPoints*dimensions + ((dimensions==2)?0:2)];
final int xySize = numPoints*2;
dbuffer.get(coords);
int coordIndex = 0;
for (int part = 0; part < numParts; part++) {
final int finish;
if (part == (numParts - 1)) {
finish = numPoints;
} else {
finish = partOffsets[part + 1];
}
final int length = finish - partOffsets[part];
// REVISIT: polyons with only 1 to 3 points are not polygons -
// geometryFactory will bomb so we skip if we find one.
if(length > 0 && length < 4){
coordIndex += length;
continue;
}
final Coordinate[] points = new Coordinate[length];
for (int i = 0; i < length; i++) {
if(dimensions==2){
points[i] = new Coordinate(coords[coordIndex*2],coords[coordIndex*2+1]);
}else{
points[i] = new Coordinate(coords[coordIndex*2],coords[coordIndex*2+1],coords[xySize+coordIndex+2]);
}
coordIndex++;
}
JTS.ensureClosed(points);
final LinearRing ring = GEOMETRY_FACTORY.createLinearRing(points);
if (CGAlgorithms.isCCW(points)) {
// counter-clockwise
holes.add(ring);
} else {
// clockwise
shells.add(ring);
}
}
// quick optimization: if there's only one shell no need to check
// for holes inclusion
if (shells.size() == 1) {
return createMulti(shells.get(0), holes);
}
// if for some reason, there is only one hole, we just reverse it and
// carry on.
else if (holes.size() == 1 && shells.isEmpty()) {
//LOGGER.warning("only one hole in this polygon record");
return createMulti(JTS.reverseRing(holes.get(0)));
} else {
// build an association between shells and holes
final List<List<LinearRing>> holesForShells = assignHolesToShells(shells, holes);
return buildGeometries(shells, holes, holesForShells);
}
}
// @Override
// public Object read(ByteBuffer buffer, ShapeType type) {
// if (type == ShapeType.NULL) {
// return createNull();
// }
// // skip the bounds
// buffer.position(buffer.position() + 4 * 8);
//
// final int numParts = buffer.getInt();
// final int numPoints = buffer.getInt();
// final int[] partOffsets = new int[numParts];
//
// for (int i = 0; i < numParts; i++) {
// partOffsets[i] = buffer.getInt();
// }
//
// final List<LinearRing> shells = new ArrayList<LinearRing>();
// final List<LinearRing> holes = new ArrayList<LinearRing>();
//
// final Coordinate[] coords = new Coordinate[numPoints];
// for (int t = 0; t < numPoints; t++) {
// coords[t] = new Coordinate(buffer.getDouble(), buffer.getDouble());
// }
//
// if (shapeType == ShapeType.POLYGONZ) {
// // skip zmin and zmax
// buffer.position(buffer.position() + 2 * 8);
//
// for (int t = 0; t < numPoints; t++) {
// coords[t].z = buffer.getDouble();
// }
// }
//
// int offset = 0;
// int start;
// int finish;
// int length;
//
// for (int part = 0; part < numParts; part++) {
// start = partOffsets[part];
//
// if (part == (numParts - 1)) {
// finish = numPoints;
// } else {
// finish = partOffsets[part + 1];
// }
//
// length = finish - start;
//
// // Use the progressive CCW algorithm.
// // basically the area algorithm for polygons
// // which also tells us vertex order based upon the
// // sign of the area.
// Coordinate[] points = new Coordinate[length];
// // double area = 0;
// // int sx = offset;
// for (int i = 0; i < length; i++) {
// points[i] = coords[offset++];
// // int j = sx + (i + 1) % length;
// // area += points[i].x * coords[j].y;
// // area -= points[i].y * coords[j].x;
// }
// // area = -area / 2;
// // REVISIT: polyons with only 1 or 2 points are not polygons -
// // geometryFactory will bomb so we skip if we find one.
// if (points.length == 0 || points.length > 3) {
// LinearRing ring = GEOMETRY_FACTORY.createLinearRing(points);
//
// if (CGAlgorithms.isCCW(points)) {
// // counter-clockwise
// holes.add(ring);
// } else {
// // clockwise
// shells.add(ring);
// }
// }
// }
//
// // quick optimization: if there's only one shell no need to check
// // for holes inclusion
// if (shells.size() == 1) {
// return createMulti(shells.get(0), holes);
// }
// // if for some reason, there is only one hole, we just reverse it and
// // carry on.
// else if (holes.size() == 1 && shells.isEmpty()) {
// //LOGGER.warning("only one hole in this polygon record");
// return createMulti(JTSUtilities.reverseRing(holes.get(0)));
// } else {
//
// // build an association between shells and holes
// final List<List<LinearRing>> holesForShells = assignHolesToShells(shells, holes);
// return buildGeometries(shells, holes, holesForShells);
// }
// }
/**
* @param shells
* @param holes
* @param holesForShells
*/
protected Geometry buildGeometries(final List<LinearRing> shells, final List<LinearRing> holes,
final List<List<LinearRing>> holesForShells) {
final Polygon[] polygons;
// if we have shells, lets use them
final int shellSize = shells.size();
final int holeSize = holes.size();
if (shellSize > 0) {
polygons = new Polygon[shellSize];
} else {
// oh, this is a bad record with only holes
polygons = new Polygon[holeSize];
}
// this will do nothing for the "only holes case"
for (int i=0; i<shellSize; i++) {
final List<LinearRing> lst = holesForShells.get(i);
polygons[i] = GEOMETRY_FACTORY.createPolygon(
shells.get(i),
lst.toArray(new LinearRing[lst.size()]));
}
// this will take care of the "only holes case"
// we just reverse each hole
if (shellSize == 0) {
for (int i=0; i<holeSize; i++) {
final LinearRing hole = holes.get(i);
polygons[i] = GEOMETRY_FACTORY.createPolygon(JTS.reverseRing(hole),
new LinearRing[0]);
}
}
return GEOMETRY_FACTORY.createMultiPolygon(polygons);
}
/**
* <b>Package private for testing</b>
*
* @param shells
* @param holes
*/
protected List<List<LinearRing>> assignHolesToShells(final List<LinearRing> shells, final List<LinearRing> holes) {
final int shellSize = shells.size();
final int holeSize = holes.size();
final List<List<LinearRing>> holesForShells = new ArrayList(shellSize);
for (int i=0; i<shellSize ; i++) {
holesForShells.add(new ArrayList<LinearRing>());
}
// find homes
for (final LinearRing testRing : holes) {
final Envelope testEnv = testRing.getEnvelopeInternal();
final Coordinate testPt = testRing.getCoordinateN(0);
LinearRing minShell = null;
Envelope minEnv = null;
for (final LinearRing tryRing : shells) {
Envelope tryEnv = tryRing.getEnvelopeInternal();
if (minShell != null) {
minEnv = minShell.getEnvelopeInternal();
}
boolean isContained = false;
final Coordinate[] coordList = tryRing.getCoordinates();
if (tryEnv.contains(testEnv)
&& (CGAlgorithms.isPointInRing(testPt, coordList) || (pointInList(
testPt, coordList)))) {
isContained = true;
}
// check if this new containing ring is smaller than the current
// minimum ring
if (isContained) {
if ((minShell == null) || minEnv.contains(tryEnv)) {
minShell = tryRing;
}
}
}
if (minShell == null) {
//LOGGER.warning("polygon found with a hole thats not inside a shell");
// now reverse this bad "hole" and turn it into a shell
shells.add(JTS.reverseRing(testRing));
holesForShells.add(new ArrayList());
} else {
((ArrayList) holesForShells.get(shells.indexOf(minShell)))
.add(testRing);
}
}
return holesForShells;
}
protected MultiPolygon createMulti(final LinearRing single) {
return createMulti(single, java.util.Collections.EMPTY_LIST);
}
protected MultiPolygon createMulti(final LinearRing single, final List<LinearRing> holes) {
return GEOMETRY_FACTORY
.createMultiPolygon(new Polygon[] { GEOMETRY_FACTORY
.createPolygon(single, holes.toArray(new LinearRing[holes.size()])) });
}
protected MultiPolygon createNull() {
return GEOMETRY_FACTORY.createMultiPolygon(null);
}
@Override
public void write(final ByteBuffer buffer, final Object geometry) {
final MultiPolygon multi;
if (geometry instanceof MultiPolygon) {
multi = (MultiPolygon) geometry;
} else {
multi = GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] { (Polygon) geometry });
}
final Envelope box = multi.getEnvelopeInternal();
buffer.putDouble(box.getMinX());
buffer.putDouble(box.getMinY());
buffer.putDouble(box.getMaxX());
buffer.putDouble(box.getMaxY());
//need to find the total number of rings and points
final int nrings;
final CoordinateSequence []coordinates;
final List<CoordinateSequence> allCoords = new ArrayList<CoordinateSequence>();
for (int t=0,n=multi.getNumGeometries(); t<n; t++) {
final Polygon p = (Polygon) multi.getGeometryN(t);
allCoords.add(p.getExteriorRing().getCoordinateSequence());
for(int ringN = 0; ringN < p.getNumInteriorRing(); ringN++){
allCoords.add(p.getInteriorRingN(ringN).getCoordinateSequence());
}
}
coordinates = (CoordinateSequence[])allCoords.toArray(new CoordinateSequence[allCoords.size()]);
nrings = coordinates.length;
final int npoints = multi.getNumPoints();
buffer.putInt(nrings);
buffer.putInt(npoints);
int count = 0;
for (int t = 0; t < nrings; t++) {
buffer.putInt(count);
count = count + coordinates[t].size();
}
final double[] zExtreame = {Double.NaN, Double.NaN};
//write out points here!.. and gather up min and max z values
for (int ringN = 0; ringN < nrings; ringN++) {
final CoordinateSequence coords = coordinates[ringN];
JTS.zMinMax(coords, zExtreame);
final int seqSize = coords.size();
for(int coordN = 0; coordN < seqSize; coordN++){
buffer.putDouble(coords.getOrdinate(coordN, 0));
buffer.putDouble(coords.getOrdinate(coordN, 1));
}
}
if (shapeType == ShapeType.POLYGONZ) {
//z
if (Double.isNaN(zExtreame[0])) {
buffer.putDouble(0.0);
buffer.putDouble(0.0);
} else {
buffer.putDouble(zExtreame[0]);
buffer.putDouble(zExtreame[1]);
}
for (int ringN = 0; ringN < nrings; ringN++) {
final CoordinateSequence coords = coordinates[ringN];
final int seqSize = coords.size();
for (int coordN = 0; coordN < seqSize; coordN++) {
final double z = coords.getOrdinate(coordN, 2);
if (Double.isNaN(z)) {
buffer.putDouble(0.0);
} else {
buffer.putDouble(z);
}
}
}
}
if (shapeType == ShapeType.POLYGONM || shapeType == ShapeType.POLYGONZ) {
//m
buffer.putDouble(-10E40);
buffer.putDouble(-10E40);
for (int t = 0; t < npoints; t++) {
buffer.putDouble(-10E40);
}
}
}
}