/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.data.ogr;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
import java.util.Arrays;
import java.util.Vector;
import org.gdal.gdal.gdal;
import org.gdal.ogr.DataSource;
import org.gdal.ogr.Driver;
import org.gdal.ogr.FeatureDefn;
import org.gdal.ogr.FieldDefn;
import org.gdal.ogr.Layer;
import org.gdal.ogr.ogr;
import org.gdal.osr.SpatialReference;
import org.geotools.data.AbstractDataStore;
import org.geotools.data.DataSourceException;
import org.geotools.data.FeatureReader;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Query;
import org.geotools.data.Transaction;
import org.geotools.factory.FactoryRegistryException;
import org.geotools.feature.AttributeType;
import org.geotools.feature.AttributeTypeFactory;
import org.geotools.feature.FeatureType;
import org.geotools.feature.FeatureTypes;
import org.geotools.feature.GeometryAttributeType;
import org.geotools.feature.SchemaException;
import org.geotools.feature.type.BasicFeatureTypes;
import org.geotools.referencing.CRS;
import org.opengis.filter.Filter;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* A datastore based on the the <a href="http://www.gdal.org/ogr">OGR</a>
* spatial data abstraction library
*
* @author aaime
*
* @source $URL$
*/
public class OGRDataStore extends AbstractDataStore {
/** C compatible FALSE */
static final int FALSE = 0;
/** C compatible TRUE */
static final int TRUE = 1;
/**
* The OGRwkbGeometryType enum from ogr_core.h, reduced to represent only 2D
* classes (I hope the 2.5D will be handled transparently by JTS)
*/
static final Class[] OGR_GEOM_TYPES = new Class[] { //
Geometry.class, // wkbUnknown = 0
Point.class, // wkbPoint = 1
MultiLineString.class, // wkbLineString = 2,
MultiPolygon.class, // wkbPolygon = 3,
MultiPoint.class, // wkbMultiPoint = 4,
MultiLineString.class, // wkbMultiLineString = 5,
MultiPolygon.class, // wkbMultiPolygon = 6,
GeometryCollection.class, // wkbGeometryCollection = 7
};
/**
* The source name that OGR should open and handle
*/
private String ogrSourceName;
/**
* Datastore namespace
*/
private URI namespace;
/**
* OGR driver to be used for the creation of new data sources
*/
private String ogrDriverName;
static {
// perform OGR format registration once
if (ogr.GetDriverCount() == 0)
ogr.RegisterAll();
}
/**
* Creates a new OGRDataStore
*
* @param ogrSourceName
* a references to the source that needs to be opened. May be a
* file system path, or a database reference. See the OGR driver
* documentation for valid formats of this string.
*/
public OGRDataStore(String ogrSourceName, String ogrDriverName, URI namespace)
throws IOException {
this.ogrSourceName = ogrSourceName;
this.ogrDriverName = ogrDriverName;
this.namespace = (namespace != null) ? namespace : FeatureTypes.DEFAULT_NAMESPACE;
int update = FALSE;
if (ogrDriverName == null) {
DataSource ds = getOGRDataSource(update);
ds.delete();
}
}
protected FeatureReader getFeatureReader(String typeName) throws IOException {
return getFeatureReader(typeName, false);
}
protected FeatureReader getFeatureReader(String typeName, boolean openForUpdate)
throws IOException {
DataSource ds = getOGRDataSource(openForUpdate ? TRUE : FALSE);
Layer layer = getOGRLayer(ds, typeName);
FeatureType schema = getSchema(typeName);
return new OGRFeatureReader(ds, layer, schema);
}
public FeatureType getSchema(String typeName) throws IOException {
DataSource ds = getOGRDataSource(FALSE);
Layer layer = getOGRLayer(ds, typeName);
if (layer == null) {
ds.delete();
throw new IOException("No such type : " + typeName);
}
FeatureDefn featureDef = null;
try {
featureDef = layer.GetLayerDefn();
AttributeType[] types = new AttributeType[featureDef.GetFieldCount() + 1];
// handle the geometry
CoordinateReferenceSystem crs = getCRS(layer);
Class geomClass = getGeometryClass(featureDef.GetGeomType());
types[0] = AttributeTypeFactory.newAttributeType("the_geom", geomClass, true, 0, null,
crs);
// compute a default parent feature type
Class geomType = types[0].getType();
FeatureType parent = null;
if ((geomType == Point.class) || (geomType == MultiPoint.class)) {
parent = BasicFeatureTypes.POINT;
} else if ((geomType == Polygon.class) || (geomType == MultiPolygon.class)) {
parent = BasicFeatureTypes.POLYGON;
} else if ((geomType == LineString.class) || (geomType == MultiLineString.class)) {
parent = BasicFeatureTypes.LINE;
}
// handle the other fields
for (int i = 1; i < types.length; i++) {
FieldDefn fd = featureDef.GetFieldDefn(i - 1);
types[i] = AttributeTypeFactory.newAttributeType(fd.GetNameRef(),
getFieldClass(fd), true, fd.GetWidth());
fd.delete();
}
// finally build the geometry type
return FeatureTypes.newFeatureType(types, layer.GetName(), namespace, false,
parent != null ? new FeatureType[] { parent } : null);
} catch (FactoryException e) {
throw new DataSourceException("Could not determine geometry SRS", e);
} catch (FactoryRegistryException e) {
throw new DataSourceException("Could not create feature type", e);
} catch (SchemaException e) {
throw new DataSourceException("Could not create feature type", e);
} finally {
if (featureDef != null)
featureDef.delete();
if (layer != null)
layer.delete();
if (ds != null)
ds.delete();
}
}
public String[] getTypeNames() throws IOException {
DataSource ds;
try {
ds = getOGRDataSource(FALSE);
} catch (IOException e) {
return new String[0];
}
String[] typeNames = new String[ds.GetLayerCount()];
for (int i = 0; i < typeNames.length; i++) {
Layer l = ds.GetLayerByIndex(i);
typeNames[i] = l.GetName();
l.delete();
}
ds.delete();
return typeNames;
}
protected int getCount(Query query) throws IOException {
if (!Filter.INCLUDE.equals(query.getFilter()))
return -1;
DataSource ds = getOGRDataSource(FALSE);
Layer l = getOGRLayer(ds, query.getTypeName());
if (l == null)
throw new IOException("Unknown feature type: " + query.getTypeName());
// go for the quick computation, return -1 otherwise
int count = l.GetFeatureCount(FALSE);
l.delete();
ds.delete();
return count;
}
protected FeatureWriter createFeatureWriter(String typeName, Transaction transaction)
throws IOException {
if (supportsInPlaceWrite(typeName)) {
OGRFeatureReader reader = (OGRFeatureReader) getFeatureReader(typeName, true);
return new OGRDirectFeatureWriter(reader);
} else {
throw new UnsupportedOperationException(
"This file format does not support in place write, "
+ "can't perform updates/deletes");
}
}
public boolean supportsInPlaceWrite(String typeName) throws IOException {
// if it's not able to open the datasource in update mode, there's no
// change it'll be able to support in place write (no need to throw an
// exception in
// this case, thus the direct call instead of getOGRDataSource())
DataSource ds = ogr.OpenShared(ogrSourceName, TRUE);
if (ds == null)
return false;
Layer l = getOGRLayer(ds, typeName);
boolean retval = l.TestCapability(ogr.OLCDeleteFeature)
&& l.TestCapability(ogr.OLCRandomWrite) && l.TestCapability(ogr.OLCSequentialWrite);
l.delete();
ds.delete();
return retval;
}
// given up on this one. Some drivers tell you that you can write on a layer
// only if you opened in update mode. We could try to create a new data
// source,
// but unfortunately there is no way to have a generic ogr name that works
// for every data store. Finally, not all drivers that do write do support
// datastore deletion. So, in the end, either we can update directly an OGR
// datasource content, or we have to create a new one and roll our own
// mechanism to have it replace the old one (this includes moving and
// deleting
// the elements that make up a data store).
public boolean supportsWriteNewLayer(String typeName) throws IOException {
DataSource ds = getOGRDataSource(FALSE);
Layer l = getOGRLayer(ds, typeName);
boolean retval = ds.TestCapability(ogr.ODsCCreateLayer)
&& l.TestCapability(ogr.OLCSequentialWrite);
l.delete();
ds.delete();
return retval;
}
public Envelope getBounds(Query q) throws IOException {
if (!q.getFilter().equals(Filter.INCLUDE))
return null;
DataSource ds = getOGRDataSource(OGRDataStore.FALSE);
Layer l = getOGRLayer(ds, q.getTypeName());
if (l.TestCapability(ogr.OLCFastGetExtent)) {
double[] bbox = new double[4];
// TODO: hum... going thru this forcing the computation is anyways
// faster than loading all the features just for the sake of
// getting out the bbox.... but we don't have this
// explicit middle ground in Geotools
l.GetExtent(bbox, OGRDataStore.FALSE);
// TODO: return a ReferencedEnvelope?
return new Envelope(bbox[0], bbox[1], bbox[2], bbox[3]);
} else {
return null;
}
}
public void createSchema(FeatureType schema) throws IOException {
// TODO: add a field to allow approximate definitions
createSchema(schema, false, null);
}
/**
* Creates a new OGR layer with provided schema and options
*
* @param schema
* the geotools schema
* @param approximateFields
* if true, OGR will try to create fields that are approximations
* of the required ones when an exact match cannt be provided
* @param options
* OGR data source/layer creation options
* @throws IOException
*/
public void createSchema(FeatureType schema, boolean approximateFields, String[] options)
throws IOException {
DataSource ds = null;
Layer l = null;
try {
// either open datasource, or try creating one
Vector optVector = options != null ? new Vector(Arrays.asList(options)) : null;
try {
ds = getOGRDataSource(TRUE);
} catch (IOException e) {
if (ogrDriverName != null) {
Driver d = ogr.GetDriverByName(ogrDriverName);
ds = d.CreateDataSource(ogrSourceName, optVector);
d.delete();
if (ds == null)
throw new IOException("Could not create OGR data source with driver "
+ ogrDriverName + " and options " + optVector);
} else {
throw new DataSourceException("Driver not provided, and could not "
+ "open data source neither");
}
}
// get the spatial reference corresponding to the default geometry
GeometryAttributeType geomType = schema.getDefaultGeometry();
int ogrGeomType = getOGRGeometryType(geomType);
SpatialReference sr = null;
if (geomType.getCoordinateSystem() != null) {
String wkt = geomType.getCoordinateSystem().toString();
sr = new SpatialReference(null);
if (sr.ImportFromWkt(wkt) != 0) {
sr = null;
LOGGER.warning("OGR could not parse the geometry WKT," + " detailed error is: "
+ gdal.GetLastErrorMsg() + "\n" + "WKT was: " + wkt);
}
}
// create the layer
l = ds.CreateLayer(schema.getTypeName(), sr, ogrGeomType, optVector);
if (l == null) {
throw new DataSourceException("Could not create the OGR layer: "
+ gdal.GetLastErrorMsg());
}
// create fields
for (int i = 0; i < schema.getAttributeCount(); i++) {
AttributeType at = schema.getAttributeType(i);
if (at == schema.getDefaultGeometry())
continue;
FieldDefn definition = getOGRFieldDefinition(at);
l.CreateField(definition, approximateFields ? TRUE : FALSE);
}
} finally {
if (l != null) {
l.delete();
}
if (ds != null)
ds.delete();
}
}
// ---------------------------------------------------------------------------------------
// PRIVATE SUPPORT METHODS
// ---------------------------------------------------------------------------------------
private FieldDefn getOGRFieldDefinition(AttributeType at) throws IOException {
final Class type = at.getType();
final FieldDefn def;
// set type, width, precision and justification where:
// * width is the number of chars needed to format the strings
// equivalent of
// the number
// * precision is the number of chars after decimal pont
// * justification: right or left (in outputs)
// TODO: steal code from Shapefile data store to guess eventual size
// limitations
if (Boolean.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTString);
def.SetWidth(5);
} else if (Byte.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTInteger);
def.SetWidth(3);
def.SetJustify(ogr.OJRight);
} else if (Short.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTInteger);
def.SetWidth(5);
def.SetJustify(ogr.OJRight);
} else if (Integer.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTInteger);
def.SetWidth(9);
def.SetJustify(ogr.OJRight);
} else if (Long.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTInteger);
def.SetWidth(19);
def.SetJustify(ogr.OJRight);
} else if (BigInteger.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTInteger);
def.SetWidth(32);
def.SetJustify(ogr.OJRight);
} else if (BigDecimal.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTReal);
def.SetWidth(32);
def.SetPrecision(15);
def.SetJustify(ogr.OJRight);
} else if (Float.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTReal);
def.SetWidth(12);
def.SetPrecision(7);
def.SetJustify(ogr.OJRight);
} else if (Double.class.equals(type) || Number.class.isAssignableFrom(type)) {
def = new FieldDefn(at.getName(), ogr.OFTReal);
def.SetWidth(22);
def.SetPrecision(16);
def.SetJustify(ogr.OJRight);
} else if (String.class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTString);
def.SetWidth(255);
// TODO: do a serious attempt to cover blob and clob too
} else if (byte[].class.equals(type)) {
def = new FieldDefn(at.getName(), ogr.OFTBinary);
// } else if (java.sql.Date.class.isAssignableFrom(type)) {
// def = new FieldDefn(at.getName(), ogr.OFTDate);
// } else if (java.sql.Time.class.isAssignableFrom(type)) {
// def = new FieldDefn(at.getName(), ogr.OFTTime);
} else if (java.util.Date.class.isAssignableFrom(type)) {
def = new FieldDefn(at.getName(), ogr.OFTDateTime);
} else {
throw new IOException("Cannot map " + type + " to an OGR type");
}
return def;
}
/**
* Tries to open the specified source in either read only or read/write mode
*
* @param update
* open read/write if TRUE, otherwise open read only
*
* @return
* @throws IOException
*/
DataSource getOGRDataSource(int update) throws IOException {
DataSource ds = ogr.OpenShared(ogrSourceName, update);
if (ds == null)
throw new IOException("OGR could not open '" + ogrSourceName + "'");
return ds;
}
/**
* Internal utility method, returns a layer if available, otherwise closes
* the provided datasource and throws and exception stating the feature type
* is not available
*
* @param ds
* @param typeName
* @return
* @throws IOException
*/
Layer getOGRLayer(DataSource ds, String typeName) throws IOException {
Layer l = ds.GetLayerByName(typeName);
if (l == null) {
ds.delete();
throw new IOException("No feature type " + typeName);
}
return l;
}
/**
* Turns an ogrFieldType into the best corresponding class we can figure out
*
* @param ogrFieldType
* @return
*/
private Class getFieldClass(FieldDefn field) {
final int ogrFieldType = field.GetFieldType();
final int width = field.GetWidth();
if (ogrFieldType < 0)
throw new IllegalArgumentException("Can't have a negative type");
if (ogrFieldType == ogr.OFTInteger) {
if (width <= 9)
return Integer.class;
else if (width <= 19)
return Long.class;
else
return BigDecimal.class;
} else if (ogrFieldType == ogr.OFTReal) {
if (width < 13)
return Float.class;
else
return Double.class;
} else if (ogrFieldType == ogr.OFTDate || ogrFieldType == ogr.OFTTime || ogrFieldType == ogr.OFTDateTime) {
// return java.sql.Date.class;
// } else if (ogrFieldType == ogr.OFTTime) {
// return java.sql.Time.class;
// } else if (ogrFieldType == ogr.OFTDateTime) {
return java.util.Date.class;
} else {
return String.class;
}
}
/**
* Turns an OGR {@link SpatialReference} object into a
* {@link CoordinateReferenceSystem} one (using WKT parsing)
*
* @param reference
* @return
* @throws FactoryException
*/
private CoordinateReferenceSystem getCRS(Layer layer) throws FactoryException {
SpatialReference reference = layer.GetSpatialRef();
if (reference == null)
return null;
String[] wkt = new String[1];
reference.ExportToWkt(wkt);
reference.delete();
return CRS.parseWKT(wkt[0]);
}
/**
* Turns a wkbGeometryType into the best corresponding geometry class we can
* figure out
*
* @param wkbGeometryType
* @return
*/
private Class getGeometryClass(int wkbGeometryType) {
if (wkbGeometryType < 0)
throw new IllegalArgumentException("Can't have a negative type");
int mask25d = 0x80000000;
int unmaskedType = wkbGeometryType & ~mask25d;
if (wkbGeometryType >= OGR_GEOM_TYPES.length) {
LOGGER.warning("Could not recognize geometry type " + wkbGeometryType
+ ". Assuming it's a new type and handling as a string");
return Geometry.class;
}
return OGR_GEOM_TYPES[unmaskedType];
}
/**
* Returns the OGR geometry type constant ginve a geometry attribute type
*
* @param type
* @return
* @throws IOException
*/
private int getOGRGeometryType(GeometryAttributeType type) throws IOException {
for (int i = 0; i < OGR_GEOM_TYPES.length; i++) {
if (type.getType().equals(OGR_GEOM_TYPES[i]))
return i;
}
throw new IOException("Could not map " + type.getType() + " to an OGR geometry type");
}
}