/*
* 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.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import org.geotoolkit.data.shapefile.shx.ShxWriter;
import org.apache.sis.storage.DataStoreException;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import org.geotoolkit.data.dbf.Closeable;
/**
* ShapefileWriter allows for the storage of geometries in esris shp format.
* During writing, an index will also be created. To create a ShapefileWriter,
* do something like<br>
* <code>
* GeometryCollection geoms;
* File shp = new File("myshape.shp");
* File shx = new File("myshape.shx");
* ShapefileWriter writer = new ShapefileWriter(
* shp.getChannel(),shx.getChannel()
* );
* writer.write(geoms,ShapeType.ARC);
* </code>
* This example assumes that each shape in the collection is a LineString.
*
* @author jamesm
* @author aaime
* @author Ian Schneider
* @module
*/
public class ShapefileWriter implements Closeable{
final ShxWriter shx;
FileChannel shpChannel;
ByteBuffer shapeBuffer;
ShapeHandler handler;
ShapeType type;
int offset;
int lp;
int cnt;
/**
* Creates a new instance of ShapeFileWriter
*
* @throws IOException
*/
public ShapefileWriter(final FileChannel shpChannel, final FileChannel shxChannel)
throws IOException {
this.shpChannel = shpChannel;
this.shx = new ShxWriter(shxChannel);
}
/**
* Allocate some buffers for writing.
*/
private void allocateBuffers() {
shapeBuffer = ByteBuffer.allocateDirect(16 * 1024);
}
/**
* Make sure our buffer is of size.
*/
private void checkShapeBuffer(final int size) {
if (shapeBuffer.capacity() < size) {
shapeBuffer = ByteBuffer.allocateDirect(size);
}
}
/**
* Drain internal buffers into underlying channels.
*/
private void drain() throws IOException {
shapeBuffer.flip();
while (shapeBuffer.remaining() > 0)
shpChannel.write(shapeBuffer);
shapeBuffer.flip().limit(shapeBuffer.capacity());
}
private void writeHeaders(final GeometryCollection geometries, final ShapeType type)
throws IOException {
// ShapefileHeader header = new ShapefileHeader();
// Envelope bounds = geometries.getEnvelopeInternal();
// header.write(shapeBuffer, type, geometries.getNumGeometries(),
// fileLength / 2,
// bounds.getMinX(),bounds.getMinY(), bounds.getMaxX(),bounds.getMaxY()
// );
// header.write(indexBuffer, type, geometries.getNumGeometries(), 50 + 4
// * geometries.getNumGeometries(),
// bounds.getMinX(),bounds.getMinY(), bounds.getMaxX(),bounds.getMaxY()
// );
int fileLength = 100;
// int largestShapeSize = 0;
for (int i = geometries.getNumGeometries() - 1; i >= 0; i--) {
// shape length + record (2 ints)
int size = handler.getLength(geometries.getGeometryN(i)) + 8;
fileLength += size;
// if (size > largestShapeSize)
// largestShapeSize = size;
}
writeHeaders(geometries.getEnvelopeInternal(), type, geometries.getNumGeometries(), fileLength);
}
/**
* Write the headers for this shapefile including the bounds, shape type,
* the number of geometries and the total fileLength (in actual bytes, NOT
* 16 bit words).
*/
public void writeHeaders(final Envelope bounds, final ShapeType type,
final int numberOfGeometries, final int fileLength) throws IOException {
try {
handler = type.getShapeHandler(true);
} catch (DataStoreException se) {
throw new RuntimeException("unexpected Exception", se);
}
if (shapeBuffer == null)
allocateBuffers();
ShapefileHeader.write(shapeBuffer, type, fileLength / 2,
bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY());
shx.moveToHeaderStart();
shx.writeHeader(type, 50 + 4 * numberOfGeometries, bounds.getMinX(),
bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY());
offset = 50;
this.type = type;
cnt = 0;
shpChannel.position(0);
drain();
}
/**
* Allocate internal buffers and position the channels to the beginning or
* the record section of the shapefile. The headers MUST be rewritten after
* this operation, or the file may be corrupt...
*/
public void skipHeaders() throws IOException {
if (shapeBuffer == null)
allocateBuffers();
shpChannel.position(100);
shx.moveToRecordStart();
}
/**
* Write a single Geometry to this shapefile. The Geometry must be
* compatable with the ShapeType assigned during the writing of the headers.
*/
public void writeGeometry(final Geometry g) throws IOException {
if (shapeBuffer == null)
throw new IOException("Must write headers first");
lp = shapeBuffer.position();
//see doc for handling null geometries
//http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf
int length;
if(g == null){
length = writeNullGeometry();
} else {
length = writeNonNullGeometry(g);
}
assert (length * 2 == (shapeBuffer.position() - lp) - 8);
lp = shapeBuffer.position();
// write to the shx
shx.writeRecord(offset, length);
offset += length + 4;
drain();
assert (shapeBuffer.position() == 0);
}
private int writeNonNullGeometry(final Geometry g) {
int length = handler.getLength(g);
// must allocate enough for shape + header (2 ints)
checkShapeBuffer(length + 8);
length /= 2;
shapeBuffer.order(ByteOrder.BIG_ENDIAN);
shapeBuffer.putInt(++cnt);
shapeBuffer.putInt(length);
shapeBuffer.order(ByteOrder.LITTLE_ENDIAN);
shapeBuffer.putInt(type.id);
handler.write(shapeBuffer, g);
return length;
}
private int writeNullGeometry() throws IOException {
// two for the headers + the null shape mark
int length = 4;
checkShapeBuffer(8 + length);
length /= 2;
shapeBuffer.order(ByteOrder.BIG_ENDIAN);
shapeBuffer.putInt(++cnt);
shapeBuffer.putInt(length);
shapeBuffer.order(ByteOrder.LITTLE_ENDIAN);
shapeBuffer.putInt(ShapeType.NULL.id);
return length;
}
/**
* Close the underlying Channels.
*/
@Override
public void close() throws IOException {
try {
if (shpChannel != null && shpChannel.isOpen()) {
shpChannel.close();
}
} finally {
shx.close();
}
shpChannel = null;
handler = null;
shapeBuffer = null;
}
@Override
public boolean isClosed() {
if(shpChannel != null){
return !shpChannel.isOpen();
}
return true;
}
/**
* Bulk write method for writing a collection of (hopefully) like geometries
* of the given ShapeType.
*/
public void write(final GeometryCollection geometries, final ShapeType type)
throws IOException, DataStoreException {
handler = type.getShapeHandler(true);
writeHeaders(geometries, type);
lp = shapeBuffer.position();
for (int i = 0, ii = geometries.getNumGeometries(); i < ii; i++) {
Geometry g = geometries.getGeometryN(i);
writeGeometry(g);
}
close();
}
}