/* * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI * for visualizing and manipulating spatial features with geometry and attributes. * * Copyright (C) 2003 Vivid Solutions * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.vividsolutions.jump.io; import com.vividsolutions.jts.algorithm.CGAlgorithms; import com.vividsolutions.jts.algorithm.RobustCGAlgorithms; import com.vividsolutions.jts.geom.*; import com.vividsolutions.jump.feature.*; import org.geotools.dbffile.DbfFieldDef; import org.geotools.dbffile.DbfFile; import org.geotools.dbffile.DbfFileWriter; import org.geotools.shapefile.Shapefile; import java.io.*; import java.net.URL; import java.nio.charset.Charset; import java.util.*; /** * * ShapefileWriter is a {@link JUMPWriter} specialized to write Shapefiles. * * <p> * DataProperties for the ShapefileWriter write(DataProperties) interface:<br><br> * </p> * * <p> * <table border='1' cellspacing='0' cellpadding='4'> * <tr> * <th>Parameter</th> * <th>Meaning</th> * </tr> * <tr> * <td>OutputFile or DefaultValue</td> * <td>File name for the output .shp file</td> * </tr> * <tr> * <td>ShapeType</td> * <td> * Dimentionality of the Shapefile - 'xy', 'xym' or 'xyz'. 'xymz' and * 'xyzm' are the same as 'xyz' * </td> * </tr> * </table><br> * * <p> * NOTE: The input .dbf and .shx is assumed to be 'beside' (in the * same directory) as the .shp file. * </p> * * The shapefile writer consists of two parts: writing attributes * (.dbf) and writing geometries (.shp). * * <p> * JUMP columns are converted to DBF columns by: * </p> * * <table border='1' cellspacing='0' cellpadding='4'> * <tr> * <th>JUMP Column</th> * <th>DBF column</th> * </tr> * <tr> * <td>STRING</td> * <td>Type 'C' length is size of longest string in the FeatureCollection </td> * </tr> * <tr> * <td>DOUBLE</td> * <td>Type 'N' length is 33, with 16 digits right of the decimal</td> * </tr> * <tr> * <td>INTEGER</td> * <td>Type 'N' length is 16, with 0 digits right of the decimal</td> * </tr> * </table> * * * <p> * For more information on the DBF file format, see the * <a * target='_new' * href='http://www.apptools.com/dbase/faq/qformt.htm'>DBF Specification FAQ</a> * </p> * * <p> * Since shape files may contain only one type of geometry (POINT, * MULTPOINT, POLYLINE, POLYGON, POINTM, MULTPOINTM, POLYLINEM, * POLYGONM, POINTZ, MULTPOINTZ, POLYLINEZ, or POLYGONZ), the * FeatureCollection must be first be normalized to one type: * </p> * * <table border='1' cellspacing='0' cellpadding='4'> * <tr> * <th>First non-NULL non-Point geometry in FeatureCollection</th> * <th>Coordinate Dimensionality</th> * <th>Shape Type</th> * </tr> * <tr> * <td> * MULTIPOINT * </td> * <td> * xy xym xyzm * </td> * <td> * MULTIPOINT MULTIPOINTM MULTIPOINTZ * </td> * </tr> * <tr> * <td> * LINESTRING/MULTILINESTRING * </td> * <td> * xy xym xyzm * </td> * <td> * POLYLINE POLYLINEM POLYLINEZ * </td> * </tr> * <tr> * <td> * POLYGON/MULTIPOLYGON * </td> * <td> * xy xym xyzm * </td> * <td> * POLYGON POLYGONM POLYGONZ * </td> * </tr> * <tr> * <th>All geometries in FeatureCollection are</th> * <th>Coordinate Dimensionality</th> * <th>Shape Type</th> * </tr> * <tr> * <td> * POINT * </td> * <td> * xy xym xyzm * </td> * <td> * POINT POINTM POINTZ * </td> * </tr> * </table> * * <p> * During this normalization process any non-consistent geometry will * be replaced by a NULL geometry. * </p> * * <p> * For example, if the shapetype is determined to be 'POLYLINE' any * POINT, MULTIPOINT, or POLYGON geometries in the FeatureCollection * will be replaced with a NULL geometry. * </p> * * <p> * The coordinate dimensionality can be explicitly set with a * DataProperties tag of 'ShapeType': 'xy', 'xym', or 'xyz' ('xymz' * and 'xyzm' are pseudonyms for 'xyz'). If this DataProperties is * unspecified, it will be auto set to 'xy' or 'xyz' based on the * first non-NULL geometry having a Z coordinate. * </p> * * <p> * Since JUMP and JTS do not currently support a M (measure) * coordinate, it will always be set to �10E40 in the shape file * (type 'xym' or 'xyzm'). This value represents the Measure "no * data" value (page 2, ESRI Shapefile Technical Description). Since * the 'NaN' DOUBLE values for Z coordinates is invalid in a * shapefile, it is converted to '0.0'. * </p> * * <p> * For more information on the shapefile format, see the * <a href='http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf'>ESRI * Shapefile Spec</a> * </p> * * <TODO> The link referencing the DBF format specification is broken - fix it!</TODO> **/ public class ShapefileWriter implements JUMPWriter { public static final String FILE_PROPERTY_KEY = "File"; public static final String DEFAULT_VALUE_PROPERTY_KEY = "DefaultValue"; public static final String SHAPE_TYPE_PROPERTY_KEY = "ShapeType"; protected static CGAlgorithms cga = new RobustCGAlgorithms(); /** Creates new ShapefileWriter */ public ShapefileWriter() { } /** * Main method - write the featurecollection to a shapefile (2d, 3d or 4d). * * @param featureCollection collection to write * @param dp 'OutputFile' or 'DefaultValue' to specify where to write, and 'ShapeType' to specify dimentionality. */ public void write(FeatureCollection featureCollection, DriverProperties dp) throws IllegalParametersException, Exception { String shpfileName; String dbffname; String shxfname; String path; String fname; String fname_withoutextention; int shapeType; int loc; GeometryCollection gc; //sstein: check for mixed geometry types in the FC this.checkIfGeomsAreMixed(featureCollection); shpfileName = dp.getProperty(FILE_PROPERTY_KEY); if (shpfileName == null) { shpfileName = dp.getProperty(DEFAULT_VALUE_PROPERTY_KEY); } if (shpfileName == null) { throw new IllegalParametersException("no output filename specified"); } loc = shpfileName.lastIndexOf(File.separatorChar); if (loc == -1) { // probably using the wrong path separator character. throw new Exception("couldn't find the path separator character '" + File.separatorChar + "' in your shape file name. This means you're probably using the unix (or dos) one."); } else { path = shpfileName.substring(0, loc + 1); // ie. "/data1/hills.shp" -> "/data1/" fname = shpfileName.substring(loc + 1); // ie. "/data1/hills.shp" -> "hills.shp" } loc = fname.lastIndexOf("."); if (loc == -1) { throw new IllegalParametersException("Filename must end in '.shp'"); } fname_withoutextention = fname.substring(0, loc); // ie. "hills.shp" -> "hills." dbffname = path + fname_withoutextention + ".dbf"; String charsetName = dp.getProperty("charset"); if (charsetName == null) charsetName = Charset.defaultCharset().name(); writeDbf(featureCollection, dbffname, Charset.forName(charsetName)); // this gc will be a collection of either multi-points, multi-polygons, or multi-linestrings // polygons will have the rings in the correct order gc = makeSHAPEGeometryCollection(featureCollection); shapeType = 2; //x,y if (dp.getProperty(SHAPE_TYPE_PROPERTY_KEY) != null) { String st = dp.getProperty(SHAPE_TYPE_PROPERTY_KEY); if (st.equalsIgnoreCase("xy")) { shapeType = 2; } else if (st.equalsIgnoreCase("xym")) { shapeType = 3; } else if (st.equalsIgnoreCase("xymz")) { shapeType = 4; } else if (st.equalsIgnoreCase("xyzm")) { shapeType = 4; } else if (st.equalsIgnoreCase("xyz")) { shapeType = 4; } else { throw new IllegalParametersException( "ShapefileWriter.write() - dataproperties has a 'ShapeType' that isn't 'xy', 'xym', or 'xymz'"); } } else { if (gc.getNumGeometries() > 0) { shapeType = guessCoordinateDims(gc.getGeometryN(0)); } } URL url = new URL("file", "localhost", shpfileName); Shapefile myshape = new Shapefile(url); myshape.write(gc, shapeType); shxfname = path + fname_withoutextention + ".shx"; BufferedOutputStream in = new BufferedOutputStream(new FileOutputStream( shxfname)); EndianDataOutputStream sfile = new EndianDataOutputStream(in); myshape.writeIndex(gc, sfile, shapeType); } /** * Returns: <br> * 2 for 2d (default) <br> * 4 for 3d - one of the oordinates has a non-NaN z value <br> * (3 is for x,y,m but thats not supported yet) <br> * @param g geometry to test - looks at 1st coordinate */ public int guessCoordinateDims(Geometry g) { Coordinate[] cs = g.getCoordinates(); for (int t = 0; t < cs.length; t++) { if (!(Double.isNaN(cs[t].z))) { return 4; } } return 2; } /** * Write a dbf file with the information from the featureCollection. * For compatibilty reasons, this method is * is now a wrapper for the changed/new one with Charset functions. * * @see writeDbf(FeatureCollection featureCollection, String fname, Charset charset) * * @param featureCollection * @param fname * @throws Exception */ void writeDbf(FeatureCollection featureCollection, String fname) throws Exception { writeDbf(featureCollection, fname, Charset.defaultCharset()); } /** * Write a dbf file with the information from the featureCollection. * @param featureCollection column data from collection * @param fname name of the dbf file to write to * July 2, 2010 - modified by beckerl to read existing dbf file header * and use the existing numeric field definitions. */ void writeDbf(FeatureCollection featureCollection, String fname, Charset charset) throws Exception { DbfFileWriter dbf; FeatureSchema fs; int t; int f; int u; int num; HashMap fieldMap = null; if (new File(fname).exists()){ DbfFile dbfFile = new DbfFile(fname); int numFields = dbfFile.getNumFields(); fieldMap = new HashMap(numFields); for (int i = 0; i<numFields; i++) { String fieldName = dbfFile.getFieldName(i); fieldMap.put(fieldName, dbfFile.fielddef[i]); } dbfFile.close(); } fs = featureCollection.getFeatureSchema(); // -1 because one of the columns is geometry DbfFieldDef[] fields = new DbfFieldDef[fs.getAttributeCount() - 1]; // dbf column type and size f = 0; for (t = 0; t < fs.getAttributeCount(); t++) { AttributeType columnType = fs.getAttributeType(t); String columnName = fs.getAttributeName(t); if (columnType == AttributeType.INTEGER) { fields[f] = new DbfFieldDef(columnName, 'N', 11, 0); //LDB: previously 16 fields[f] = overrideWithExistingCompatibleDbfFieldDef(fields[f], fieldMap); f++; } else if (columnType == AttributeType.DOUBLE) { fields[f] = new DbfFieldDef(columnName, 'N', 33, 16); fields[f] = overrideWithExistingCompatibleDbfFieldDef(fields[f], fieldMap); f++; } else if (columnType == AttributeType.STRING) { int maxlength = findMaxStringLength(featureCollection, t); if (maxlength > 255) { throw new Exception( "ShapefileWriter does not support strings longer than 255 characters"); } fields[f] = new DbfFieldDef(columnName, 'C', maxlength, 0); //fields[f] = overrideWithExistingCompatibleDbfFieldDef(fields[f], fieldMap); f++; } else if (columnType == AttributeType.DATE) { fields[f] = new DbfFieldDef(columnName, 'D', 8, 0); f++; } else if (columnType == AttributeType.GEOMETRY) { //do nothing - the .shp file handles this } else { throw new Exception( "Shapewriter: unsupported AttributeType found in featurecollection."); } } // write header dbf = new DbfFileWriter(fname); dbf.setCharset(charset); dbf.writeHeader(fields, featureCollection.size()); //write rows num = featureCollection.size(); List features = featureCollection.getFeatures(); for (t = 0; t < num; t++) { //System.out.println("dbf: record "+t); Feature feature = (Feature) features.get(t); Vector DBFrow = new Vector(); //make data for each column in this feature (row) for (u = 0; u < fs.getAttributeCount(); u++) { AttributeType columnType = fs.getAttributeType(u); if (columnType == AttributeType.INTEGER) { Object a = feature.getAttribute(u); if (a == null) { DBFrow.add(new Integer(0)); } else { DBFrow.add((Integer) a); } } else if (columnType == AttributeType.DOUBLE) { Object a = feature.getAttribute(u); if (a == null) { DBFrow.add(new Double(0.0)); } else { DBFrow.add((Double) a); } } else if (columnType == AttributeType.DATE) { Object a = feature.getAttribute(u); if (a == null) { DBFrow.add(""); } else { DBFrow.add(DbfFile.DATE_PARSER.format((Date)a)); } } else if (columnType == AttributeType.STRING) { Object a = feature.getAttribute(u); if (a == null) { DBFrow.add(new String("")); } else { // MD 16 jan 03 - added some defensive programming if (a instanceof String) { DBFrow.add(a); } else { DBFrow.add(a.toString()); } } } } dbf.writeRecord(DBFrow); } dbf.close(); } private DbfFieldDef overrideWithExistingCompatibleDbfFieldDef(DbfFieldDef field, Map columnMap) { String fieldname = field.fieldname.toString().trim(); if ((columnMap != null) && (columnMap.containsKey(fieldname))) { DbfFieldDef dbfFieldDef = (DbfFieldDef) columnMap.get(fieldname); dbfFieldDef.fieldname = field.fieldname; //must have null padded version to work switch(dbfFieldDef.fieldtype){ case 'C': case 'c': //character case not working yet if (field.fieldtype == 'C') if (field.fieldlen > dbfFieldDef.fieldlen) //allow string expansion if needed return field; else { dbfFieldDef.fieldtype = field.fieldtype; return dbfFieldDef; } break; case 'N': case 'n': case 'F': case 'f': if (field.fieldtype == 'N') { dbfFieldDef.fieldtype = field.fieldtype; return dbfFieldDef; } break; } } return field; } /** *look at all the data in the column of the featurecollection, and find the largest string! *@param fc features to look at *@param attributeNumber which of the column to test. */ int findMaxStringLength(FeatureCollection fc, int attributeNumber) { int l; int maxlen = 0; Feature f; for (Iterator i = fc.iterator(); i.hasNext();) { f = (Feature) i.next(); //patch from Hisaji Ono for Double byte characters l = f.getString(attributeNumber).getBytes().length; if (l > maxlen) { maxlen = l; } } return Math.max(1, maxlen); //LDB: don't allow zero length strings } /** * Find the generic geometry type of the feature collection. * Simple method - find the 1st non null geometry and its type * is the generic type. * returns 0 : only empty geometry collection <br> * 1 : only single points<br> * 3 : at least one line or multiline<br> * 5 : at least one polygon or multipolygon <br> * 8 : at least one multipoint<br> * 31 : only non empty geometry collection<br> *@param fc feature collection containing tet geometries. **/ int findBestGeometryType(FeatureCollection fc) { Geometry geom; boolean onlyPoints = true; boolean onlyEmptyGeometryCollection = true; // [mmichaud 2007-06-12] : add the type variable to test if // all geometries are single Point // maybe it would be clearer using shapefile types integer for type for (Iterator i = fc.iterator(); i.hasNext();) { geom = ((Feature) i.next()).getGeometry(); if (onlyPoints && !(geom instanceof Point)) { onlyPoints = false; } if (onlyEmptyGeometryCollection && !(geom.isEmpty())) { onlyEmptyGeometryCollection = false; } if (geom instanceof MultiPoint) { return 8; } if (geom instanceof Polygon) { return 5; } if (geom instanceof MultiPolygon) { return 5; } if (geom instanceof LineString) { return 3; } if (geom instanceof MultiLineString) { return 3; } } if (onlyPoints) return 1; else if (onlyEmptyGeometryCollection) return 0; else return 31; } public void checkIfGeomsAreMixed(FeatureCollection featureCollection) throws IllegalParametersException, Exception { //-- sstein: check first if features are of different geometry type. int i= 0; Class firstClass = null; Geometry firstGeom = null; //System.out.println("ShapeFileWriter: start mixed-geom-test"); for (Iterator iter = featureCollection.iterator(); iter.hasNext();) { Feature myf = (Feature) iter.next(); if (i==0){ firstClass = myf.getGeometry().getClass(); firstGeom = myf.getGeometry(); } else{ if (firstClass != myf.getGeometry().getClass()){ // System.out.println("first test failed"); if((firstGeom instanceof Polygon) && (myf.getGeometry() instanceof MultiPolygon)){ //everything is ok } else if((firstGeom instanceof MultiPolygon) && (myf.getGeometry() instanceof Polygon)){ //everything is ok } else if((firstGeom instanceof Point) && (myf.getGeometry() instanceof MultiPoint)){ //everything is ok } else if((firstGeom instanceof MultiPoint) && (myf.getGeometry() instanceof Point)){ //everything is ok } else if((firstGeom instanceof LineString) && (myf.getGeometry() instanceof MultiLineString)){ //everything is ok } else if((firstGeom instanceof MultiLineString) && (myf.getGeometry() instanceof LineString)){ //everything is ok } else{ System.out.println("test completely failed - throw exception"); throw new IllegalParametersException( "mixed geometry types found, please separate Polygons from Lines and Points when saving to *.shp"); } } } i++; } } /** * Reverses the order of points in lr (is CW -> CCW or CCW->CW) */ LinearRing reverseRing(LinearRing lr) { int numPoints = lr.getNumPoints(); Coordinate[] newCoords = new Coordinate[numPoints]; for (int t = 0; t < numPoints; t++) { newCoords[t] = lr.getCoordinateN(numPoints - t - 1); } return new LinearRing(newCoords, new PrecisionModel(), 0); } /** * make sure outer ring is CCW and holes are CW * @param p polygon to check */ Polygon makeGoodSHAPEPolygon(Polygon p) { if (p.isEmpty()) return p; LinearRing outer; LinearRing[] holes = new LinearRing[p.getNumInteriorRing()]; Coordinate[] coords; coords = p.getExteriorRing().getCoordinates(); if (cga.isCCW(coords)) { outer = reverseRing((LinearRing) p.getExteriorRing()); } else { outer = (LinearRing) p.getExteriorRing(); } for (int t = 0; t < p.getNumInteriorRing(); t++) { coords = p.getInteriorRingN(t).getCoordinates(); if (!(cga.isCCW(coords))) { holes[t] = reverseRing((LinearRing) p.getInteriorRingN(t)); } else { holes[t] = (LinearRing) p.getInteriorRingN(t); } } return new Polygon(outer, holes, new PrecisionModel(), 0); } /** * make sure outer ring is CCW and holes are CW for all the polygons in the Geometry *@param mp set of polygons to check */ MultiPolygon makeGoodSHAPEMultiPolygon(MultiPolygon mp) { MultiPolygon result; Polygon[] ps = new Polygon[mp.getNumGeometries()]; //check each sub-polygon for (int t = 0; t < mp.getNumGeometries(); t++) { ps[t] = makeGoodSHAPEPolygon((Polygon) mp.getGeometryN(t)); } result = new MultiPolygon(ps, new PrecisionModel(), 0); return result; } /** * return a single geometry collection <Br> * result.GeometryN(i) = the i-th feature in the FeatureCollection<br> * All the geometry types will be the same type (ie. all polygons) - or they will be set to<br> * NULL geometries<br> * <br> * GeometryN(i) = {Multipoint,Multilinestring, or Multipolygon)<br> * *@param fc feature collection to make homogeneous */ public GeometryCollection makeSHAPEGeometryCollection(FeatureCollection fc) throws Exception { GeometryCollection result; Geometry[] allGeoms = new Geometry[fc.size()]; int geomtype = findBestGeometryType(fc); if (geomtype == 31) { throw new Exception( "Could not determine shapefile type - data is all GeometryCollections"); } List features = fc.getFeatures(); for (int t = 0; t < features.size(); t++) { Geometry geom = ((Feature) features.get(t)).getGeometry(); switch (geomtype) { case 0: //empty geometry collection // empty geometry collections are arbitrarily written in a Point shapefile allGeoms[t] = geom.getFactory().createGeometryCollection(new Geometry[0]); break; case 1: //single point if ((geom instanceof Point)) { allGeoms[t] = (Point) geom; } else { allGeoms[t] = new Point(null, new PrecisionModel(), 0); } break; case 8: //point if ((geom instanceof Point)) { //good! Point[] p = new Point[1]; p[0] = (Point) geom; allGeoms[t] = new MultiPoint(p, new PrecisionModel(), 0); } else if (geom instanceof MultiPoint) { allGeoms[t] = geom; } else { allGeoms[t] = new MultiPoint(null, new PrecisionModel(), 0); } break; case 3: //line if ((geom instanceof LineString)) { LineString[] l = new LineString[1]; l[0] = (LineString) geom; allGeoms[t] = new MultiLineString(l, new PrecisionModel(), 0); } else if (geom instanceof MultiLineString) { allGeoms[t] = geom; } else { allGeoms[t] = new MultiLineString(null, new PrecisionModel(), 0); } break; case 5: //polygon if (geom instanceof Polygon) { //good! Polygon[] p = new Polygon[1]; p[0] = (Polygon) geom; allGeoms[t] = makeGoodSHAPEMultiPolygon(new MultiPolygon( p, new PrecisionModel(), 0)); } else if (geom instanceof MultiPolygon) { allGeoms[t] = makeGoodSHAPEMultiPolygon((MultiPolygon) geom); } else { allGeoms[t] = new MultiPolygon(null, new PrecisionModel(), 0); } break; } } result = new GeometryCollection(allGeoms, new PrecisionModel(), 0); return result; } }