/*
* ShapeGeometryExporter.java
*
* Created on March 28, 2007, 10:23 PM
*
*/
package ika.geoexport;
import ika.utils.LittleEndianOutputStream;
import ika.geo.GeoObject;
import ika.geo.GeoPath;
import ika.geo.GeoPathIterator;
import ika.geo.GeoPathModel;
import ika.geo.GeoPoint;
import ika.geo.GeoSet;
import java.awt.geom.Rectangle2D;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import ika.utils.MixedEndianDataOutputStream;
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 extends GeoSetExporter {
/**
* 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 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(GeoSet 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);
this.writeGeoSet(geom, geoSet);
// add total size of geometry to shx records
this.shxRecords.add(new Integer(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(GeoSet geoSet,
MixedEndianDataOutputStream mos,
int dataSize)
throws IOException {
Rectangle2D bbox = getExportedExtension(geoSet);
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
}
public Rectangle2D getExportedExtension(GeoSet geoSet) {
if (geoSet.isVisible() == false) {
return new Rectangle2D.Double();
}
double minX = Double.MAX_VALUE;
double maxX = -Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double maxY = -Double.MAX_VALUE;
final int numberOfChildren = geoSet.getNumberOfChildren();
for (int i = 0; i < numberOfChildren; i++) {
GeoObject geoObject = geoSet.getGeoObject(i);
// only write visible objects
if (geoObject.isVisible() == false) {
continue;
}
Rectangle2D bbox = null;
if (geoObject instanceof GeoPath
&& (shapeType == POLYGON_SHAPE_TYPE
|| shapeType == POLYLINE_SHAPE_TYPE)) {
GeoPath geoPath = (GeoPath) geoObject;
if (geoPath.hasOneOrMorePoints()) {
bbox = geoPath.getBounds2D(GeoObject.UNDEFINED_SCALE);
}
} else if (geoObject instanceof GeoPoint && shapeType == POINT_SHAPE_TYPE) {
bbox = ((GeoPoint) geoObject).getBounds2D(GeoObject.UNDEFINED_SCALE);
} else if (geoObject instanceof GeoSet) {
bbox = getExportedExtension((GeoSet) geoObject);
}
if (bbox != null) {
minX = Math.min(bbox.getMinX(), minX);
maxX = Math.max(bbox.getMaxX(), maxX);
minY = Math.min(bbox.getMinY(), minY);
maxY = Math.max(bbox.getMaxY(), maxY);
}
}
return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
}
/**
* 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 writeGeoSet(MixedEndianDataOutputStream mos, GeoSet geoSet)
throws IOException {
if (geoSet.isVisible() == false) {
return;
}
final int numberOfChildren = geoSet.getNumberOfChildren();
for (int i = 0; i < numberOfChildren; i++) {
if (this.progressIndicator != null) {
int perc = (i + 1) * 100 / numberOfChildren;
System.out.println("ShapeExporter " + perc);
if (!this.progressIndicator.progress(perc)) {
return;
}
}
GeoObject geoObject = geoSet.getGeoObject(i);
// only write visible objects
if (geoObject.isVisible() == false) {
continue;
}
if (geoObject instanceof GeoPath
&& (this.shapeType == POLYGON_SHAPE_TYPE
|| this.shapeType == POLYLINE_SHAPE_TYPE)) {
GeoPath geoPath = (GeoPath) geoObject;
if (!geoPath.hasOneOrMorePoints()) {
continue;
}
this.shxRecords.add(new Integer(mos.size()));
writePolyline(mos, geoPath);
} else if (geoObject instanceof GeoPoint
&& this.shapeType == POINT_SHAPE_TYPE) {
this.shxRecords.add(new Integer(mos.size()));
writePoint(mos, (GeoPoint) geoObject);
} else if (geoObject instanceof GeoSet) {
this.writeGeoSet(mos, (GeoSet) geoObject);
}
}
}
/**
* Writes a point to a stream.
*/
private void writePoint(MixedEndianDataOutputStream mos, GeoPoint geoPoint)
throws IOException {
this.writeRecordHeader(mos, 20);
final double x = geoPoint.getX();
final double y = geoPoint.getY();
mos.writeLittleEndianInt(POINT_SHAPE_TYPE); // shape type
mos.writeLittleEndianDouble(x); // x coordinate
mos.writeLittleEndianDouble(y); // y coordinate
}
/**
* Writes a path to a stream.
*/
private void writePolyline(MixedEndianDataOutputStream mos, GeoPath geoPath)
throws IOException {
if (geoPath.hasBezierSegment()) {
// geoPath = geoPath.toFlattenedPath(bezierConversionTolerance); FIXME
}
Rectangle2D bbox = geoPath.getBounds2D(GeoObject.UNDEFINED_SCALE);
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 = geoPath.getCompoundCount();
los.writeInt(partsCount); // number of parts
final int pointsCount = geoPath.getDrawingInstructionCount();
los.writeInt(pointsCount); // number of points
// An array of length partsCount. Stores, for each PolyLine, the index of its
// first point in the points array. Array indexes are relative to 0.
int pointsCounter = 0;
GeoPathIterator pi = geoPath.getIterator();
do {
if (pi.getInstruction() == GeoPathModel.MOVETO) {
los.writeInt(pointsCounter);
}
++pointsCounter;
} while (pi.next());
// write the path geometry
pi = geoPath.getIterator();
double lastX = Double.NaN;
double lastY = Double.NaN;
double lastMoveToX = Double.NaN;
double lastMoveToY = Double.NaN;
do {
switch (pi.getInstruction()) {
case GeoPathModel.MOVETO:
los.writeDouble(lastX = lastMoveToX = pi.getX());
los.writeDouble(lastY = lastMoveToY = pi.getY());
break;
case GeoPathModel.CLOSE:
if (!Double.isNaN(lastMoveToX) && !Double.isNaN(lastMoveToY)) {
los.writeDouble(lastX = lastMoveToX);
los.writeDouble(lastY = lastMoveToY);
}
break;
case GeoPathModel.LINETO:
los.writeDouble(lastX = pi.getX());
los.writeDouble(lastY = pi.getY());
break;
default:
System.err.println("ShapeGeometryExporter: unsupported path segment");
}
} while (pi.next());
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, GeoSet 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;
}
/**
* Automatically determines the type of shape file that will be generated
* based on the clas of the first GeoObject found in the passed GeoSet.
*/
public void setShapeTypeFromFirstGeoObject(GeoSet geoSet) {
GeoObject firstGeoObj = geoSet.getFirstGeoObject(GeoSet.class, true, false);
if (firstGeoObj instanceof GeoPoint) {
shapeType = POINT_SHAPE_TYPE;
} else if (firstGeoObj instanceof GeoPath) {
GeoPath geoPath = (GeoPath) firstGeoObj;
shapeType = geoPath.isClosed() ? POLYGON_SHAPE_TYPE : POLYLINE_SHAPE_TYPE;
}
}
}