/*
* ShapeGeometryExporter.java
*
* Created on March 28, 2007, 10:23 PM
*
*/
package edu.oregonstate.cartography.geometryexport;
import edu.oregonstate.cartography.simplefeatures.Geometry;
import edu.oregonstate.cartography.simplefeatures.GeometryCollection;
import edu.oregonstate.cartography.simplefeatures.LineString;
import edu.oregonstate.cartography.simplefeatures.Point;
import java.awt.geom.Rectangle2D;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
/**
* Exports a GeoSet to .shp and .shx files. Does not create a .dbf file.
*
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich
*/
public class ShapeGeometryExporter {
/**
* Write points.
*/
public static final int POINT_SHAPE_TYPE = 1;
/**
* Write open polylines.
*/
public static final int POLYLINE_SHAPE_TYPE = 3;
/**
* Write closed polygons.
*/
public static final int POLYGON_SHAPE_TYPE = 5;
/**
* This type of shapes will be exported.
*/
private int shapeType = POLYLINE_SHAPE_TYPE;
/**
* Count the exported shapes, start counting at 1. This is needed to
* sequentially number the records written to the file.
*/
private int recordCounter = 1;
/**
* Store the beginning of each record in this array. This is an array of
* offsets in bytes counted from the end of the file header. This
* information is needed to generate the shx file, which is required by the
* specification.
*/
private final ArrayList shxRecords = new ArrayList();
/**
* Creates a new instance of ShapeGeometryExporter
*/
public ShapeGeometryExporter() {
}
public String getFileFormatName() {
return "Shape";
}
public String getFileExtension() {
return "shp";
}
protected void write(GeometryCollection geoSet, OutputStream outputStream) throws IOException {
this.recordCounter = 1;
this.shxRecords.clear();
// Accumulate the data in a ByteArrayOutputStream.
// This allows for finding the size of the resulting file.
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
BufferedOutputStream buffOs = new BufferedOutputStream(byteArrayOutputStream);
MixedEndianDataOutputStream geom = new MixedEndianDataOutputStream(buffOs);
writeGeometryCollection(geom, geoSet);
// add total size of geometry to shx records
this.shxRecords.add(geom.size());
// Close the ByteArrayOutputStream.
// This is not closing the destination outputStream
geom.close();
// write file header
MixedEndianDataOutputStream head = new MixedEndianDataOutputStream(outputStream);
this.writeHeader(geoSet, head, geom.size());
// copy the geometry to the outputStream
byteArrayOutputStream.writeTo(outputStream);
}
/**
* Writes the file header.
*
* @param geoSet The GeoSet to export.
* @param mos The stream to write to.
* @dataSize The header contains a file length field. dataSize is in bytes,
* not including the header size.
*/
private void writeHeader(GeometryCollection geoSet,
MixedEndianDataOutputStream mos,
int dataSize)
throws IOException {
Rectangle2D bbox = geoSet.getBoundingBox();
mos.writeInt(9994); // file code
for (int i = 0; i < 5; i++) // unused
{
mos.writeInt(0);
}
mos.writeInt(dataSize / 2 + 50); // file length
mos.writeLittleEndianInt(1000); // version
mos.writeLittleEndianInt(this.shapeType); // shape type
mos.writeLittleEndianDouble(bbox.getMinX()); // xmin
mos.writeLittleEndianDouble(bbox.getMinY()); // ymin
mos.writeLittleEndianDouble(bbox.getMaxX()); // xmax
mos.writeLittleEndianDouble(bbox.getMaxY()); // ymax
mos.writeLittleEndianDouble(0); // zmin
mos.writeLittleEndianDouble(0); // zmax
mos.writeLittleEndianDouble(0); // mmin
mos.writeLittleEndianDouble(0); // mmax
}
/**
* Writes a record header. Assigns a unique id to the new record.
*
* @param mos The destination stream.
* @param length the length of the record content in bytes.
*/
private void writeRecordHeader(MixedEndianDataOutputStream mos, int length)
throws IOException {
mos.writeInt(recordCounter++); // record number, starting at 1
mos.writeInt(length / 2); // content length in 16 bit words
}
/**
* Writes a GeoSet to a stream.
*/
private void writeGeometryCollection(MixedEndianDataOutputStream mos, GeometryCollection geoSet)
throws IOException {
final int numberOfChildren = geoSet.getNumGeometries();
for (int i = 0; i < numberOfChildren; i++) {
Geometry geoObject = geoSet.getGeometryN(i);
if (geoObject instanceof LineString
&& (this.shapeType == POLYGON_SHAPE_TYPE
|| this.shapeType == POLYLINE_SHAPE_TYPE)) {
LineString geoPath = (LineString) geoObject;
if (geoPath.getNumPoints() == 0) {
continue;
}
this.shxRecords.add(new Integer(mos.size()));
writePolyline(mos, geoPath);
} else if (geoObject instanceof Point
&& this.shapeType == POINT_SHAPE_TYPE) {
// FIXME
//this.shxRecords.add(new Integer(mos.size()));
//writePoint(mos, (Point) geoObject);
} else if (geoObject instanceof GeometryCollection) {
this.writeGeometryCollection(mos, (GeometryCollection) geoObject);
}
}
}
/**
* Writes a path to a stream.
*/
private void writePolyline(MixedEndianDataOutputStream mos, LineString geoPath)
throws IOException {
Rectangle2D bbox = geoPath.getBoundingBox();
final double xmin = bbox.getMinX();
final double xmax = bbox.getMaxX();
final double ymin = bbox.getMinY();
final double ymax = bbox.getMaxY();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
LittleEndianOutputStream los = new LittleEndianOutputStream(bos);
los.writeInt(shapeType); // polyline or polygon
los.writeDouble(xmin); // xmin
los.writeDouble(ymin); // ymin
los.writeDouble(xmax); // xmax
los.writeDouble(ymax); // ymax
final int partsCount = 1; // geoPath.getCompoundCount();
los.writeInt(partsCount); // number of parts
final int pointsCount = geoPath.getNumPoints();
los.writeInt(pointsCount); // number of points
int pointsCounter = 0;
los.writeInt(pointsCounter);
// write the path geometry
for (int i = 0; i < pointsCount; i++) {
Point p = geoPath.getPointN(i);
los.writeDouble(p.getX());
los.writeDouble(p.getY());
}
bos.flush();
bos.close(); // close the local ByteArrayOutputStream
this.writeRecordHeader(mos, los.size());
mos.write(bos.toByteArray());
}
/**
* Returns the number of records written to the geometry file.
*
* @return The number of features written so far.
*/
public int getWrittenRecordCount() {
return this.recordCounter - 1;
}
/**
* Writes a SHX file to the passed stream.
*
* @param shxOutputStream The stream to write to. This stream is not closed.
* @param geoSet The GeoSet that is exported.
*/
public void writeSHXFile(OutputStream shxOutputStream, GeometryCollection geoSet)
throws IOException {
MixedEndianDataOutputStream mos = new MixedEndianDataOutputStream(shxOutputStream);
// write the file header
final int dataSize = (this.shxRecords.size() - 1) * 8;
this.writeHeader(geoSet, mos, dataSize);
// write the records
final int recordsCount = this.shxRecords.size();
for (int i = 0; i < recordsCount - 1; i++) {
final int offset = ((Integer) this.shxRecords.get(i)).intValue();
final int nextOffset = ((Integer) this.shxRecords.get(i + 1)).intValue();
final int contentLength = nextOffset - offset;
mos.writeInt(offset / 2 + 50); // + 50 for the file header
mos.writeInt(contentLength / 2 - 4);
}
mos.flush();
}
public int getShapeType() {
return shapeType;
}
/**
* Set the type of shape file that will be generated. Valid values are
* POINT_SHAPE_TYPE, POLYLINE_SHAPE_TYPE, and POLYGON_SHAPE_TYPE. The
* default value is POLYLINE_SHAPE_TYPE. use setShapeTypeFromFirstGeoObject
* to automatically determine the type of shape file based on the first
* GeoObject in a GeoSet.
*/
public void setShapeType(int shapeType) {
if (shapeType != POINT_SHAPE_TYPE
&& shapeType != POLYLINE_SHAPE_TYPE
&& shapeType != POLYGON_SHAPE_TYPE) {
throw new IllegalArgumentException("invalid shape type");
}
this.shapeType = shapeType;
}
}