001: /*
002: * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
003: * for visualizing and manipulating spatial features with geometry and attributes.
004: *
005: * Copyright (C) 2003 Vivid Solutions
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: *
021: * For more information, contact:
022: *
023: * Vivid Solutions
024: * Suite #1A
025: * 2328 Government Street
026: * Victoria BC V8T 5G5
027: * Canada
028: *
029: * (250)385-6040
030: * www.vividsolutions.com
031: */
032: package com.vividsolutions.jump.io;
033:
034: import com.vividsolutions.jts.algorithm.CGAlgorithms;
035: import com.vividsolutions.jts.algorithm.RobustCGAlgorithms;
036: import com.vividsolutions.jts.geom.*;
037:
038: import com.vividsolutions.jump.feature.*;
039:
040: import org.geotools.dbffile.DbfFieldDef;
041: import org.geotools.dbffile.DbfFile;
042: import org.geotools.dbffile.DbfFileWriter;
043:
044: import org.geotools.shapefile.Shapefile;
045:
046: import java.io.*;
047:
048: import java.net.URL;
049:
050: import java.util.*;
051:
052: /**
053: *
054: * ShapefileWriter is a {@link JUMPWriter} specialized to write Shapefiles.
055: *
056: * <p>
057: * DataProperties for the ShapefileWriter write(DataProperties) interface:<br><br>
058: * </p>
059: *
060: * <p>
061: * <table border='1' cellspacing='0' cellpadding='4'>
062: * <tr>
063: * <th>Parameter</th>
064: * <th>Meaning</th>
065: * </tr>
066: * <tr>
067: * <td>OutputFile or DefaultValue</td>
068: * <td>File name for the output .shp file</td>
069: * </tr>
070: * <tr>
071: * <td>ShapeType</td>
072: * <td>
073: * Dimentionality of the Shapefile - 'xy', 'xym' or 'xyz'. 'xymz' and
074: * 'xyzm' are the same as 'xyz'
075: * </td>
076: * </tr>
077: * </table><br>
078: *
079: * <p>
080: * NOTE: The input .dbf and .shx is assumed to be 'beside' (in the
081: * same directory) as the .shp file.
082: * </p>
083: *
084: * The shapefile writer consists of two parts: writing attributes
085: * (.dbf) and writing geometries (.shp).
086: *
087: * <p>
088: * JUMP columns are converted to DBF columns by:
089: * </p>
090: *
091: * <table border='1' cellspacing='0' cellpadding='4'>
092: * <tr>
093: * <th>JUMP Column</th>
094: * <th>DBF column</th>
095: * </tr>
096: * <tr>
097: * <td>STRING</td>
098: * <td>Type 'C' – length is size of longest string in the FeatureCollection </td>
099: * </tr>
100: * <tr>
101: * <td>DOUBLE</td>
102: * <td>Type 'N' – length is 33, with 16 digits right of the decimal</td>
103: * </tr>
104: * <tr>
105: * <td>INTEGER</td>
106: * <td>Type 'N' – length is 16, with 0 digits right of the decimal</td>
107: * </tr>
108: * </table>
109: *
110: *
111: * <p>
112: * For more information on the DBF file format, see the
113: * <a
114: * target='_new'
115: * href='http://www.apptools.com/dbase/faq/qformt.htm'>DBF Specification FAQ</a>
116: * </p>
117: *
118: * <p>
119: * Since shape files may contain only one type of geometry (POINT,
120: * MULTPOINT, POLYLINE, POLYGON, POINTM, MULTPOINTM, POLYLINEM,
121: * POLYGONM, POINTZ, MULTPOINTZ, POLYLINEZ, or POLYGONZ), the
122: * FeatureCollection must be first be normalized to one type:
123: * </p>
124: *
125: * <table border='1' cellspacing='0' cellpadding='4'>
126: * <tr>
127: * <th>First non-NULL non-Point geometry in FeatureCollection</th>
128: * <th>Coordinate Dimensionality</th>
129: * <th>Shape Type</th>
130: * </tr>
131: * <tr>
132: * <td>
133: * MULTIPOINT
134: * </td>
135: * <td>
136: * xy xym xyzm
137: * </td>
138: * <td>
139: * MULTIPOINT MULTIPOINTM MULTIPOINTZ
140: * </td>
141: * </tr>
142: * <tr>
143: * <td>
144: * LINESTRING/MULTILINESTRING
145: * </td>
146: * <td>
147: * xy xym xyzm
148: * </td>
149: * <td>
150: * POLYLINE POLYLINEM POLYLINEZ
151: * </td>
152: * </tr>
153: * <tr>
154: * <td>
155: * POLYGON/MULTIPOLYGON
156: * </td>
157: * <td>
158: * xy xym xyzm
159: * </td>
160: * <td>
161: * POLYGON POLYGONM POLYGONZ
162: * </td>
163: * </tr>
164: * <tr>
165: * <th>All geometries in FeatureCollection are</th>
166: * <th>Coordinate Dimensionality</th>
167: * <th>Shape Type</th>
168: * </tr>
169: * <tr>
170: * <td>
171: * POINT
172: * </td>
173: * <td>
174: * xy xym xyzm
175: * </td>
176: * <td>
177: * POINT POINTM POINTZ
178: * </td>
179: * </tr>
180: * </table>
181: *
182: * <p>
183: * During this normalization process any non-consistent geometry will
184: * be replaced by a NULL geometry.
185: * </p>
186: *
187: * <p>
188: * For example, if the shapetype is determined to be 'POLYLINE' any
189: * POINT, MULTIPOINT, or POLYGON geometries in the FeatureCollection
190: * will be replaced with a NULL geometry.
191: * </p>
192: *
193: * <p>
194: * The coordinate dimensionality can be explicitly set with a
195: * DataProperties tag of 'ShapeType': 'xy', 'xym', or 'xyz' ('xymz'
196: * and 'xyzm' are pseudonyms for 'xyz'). If this DataProperties is
197: * unspecified, it will be auto set to 'xy' or 'xyz' based on the
198: * first non-NULL geometry having a Z coordinate.
199: * </p>
200: *
201: * <p>
202: * Since JUMP and JTS do not currently support a M (measure)
203: * coordinate, it will always be set to –10E40 in the shape file
204: * (type 'xym' or 'xyzm'). This value represents the Measure "no
205: * data" value (page 2, ESRI Shapefile Technical Description). Since
206: * the 'NaN' DOUBLE values for Z coordinates is invalid in a
207: * shapefile, it is converted to '0.0'.
208: * </p>
209: *
210: * <p>
211: * For more information on the shapefile format, see the
212: * <a href='http://www.esri.com/library/whitepapers/pdfs/shapefile.pdf'>ESRI
213: * Shapefile Spec</a>
214: * </p>
215: *
216: * @todo The link referencing the DBF format specification is broken - fix it!
217: **/
218: public class ShapefileWriter implements JUMPWriter {
219:
220: public static final String FILE_PROPERTY_KEY = "File";
221: public static final String DEFAULT_VALUE_PROPERTY_KEY = "DefaultValue";
222: public static final String SHAPE_TYPE_PROPERTY_KEY = "ShapeType";
223:
224: protected static CGAlgorithms cga = new RobustCGAlgorithms();
225:
226: /** Creates new ShapefileWriter */
227: public ShapefileWriter() {
228: }
229:
230: /**
231: * Main method - write the featurecollection to a shapefile (2d, 3d or 4d).
232: *
233: * @param featureCollection collection to write
234: * @param dp 'OutputFile' or 'DefaultValue' to specify where to write, and 'ShapeType' to specify dimentionality.
235: */
236: public void write(FeatureCollection featureCollection,
237: DriverProperties dp) throws IllegalParametersException,
238: Exception {
239: String shpfileName;
240: String dbffname;
241: String shxfname;
242:
243: String path;
244: String fname;
245: String fname_withoutextention;
246: int shapeType;
247: int loc;
248:
249: GeometryCollection gc;
250:
251: //-- sstein: check first if features are of different geometry type.
252: int i = 0;
253: Class firstClass = null;
254: for (Iterator iter = featureCollection.iterator(); iter
255: .hasNext();) {
256: Feature myf = (Feature) iter.next();
257: if (i == 0) {
258: firstClass = myf.getGeometry().getClass();
259: } else {
260: if (firstClass != myf.getGeometry().getClass()) {
261: throw new IllegalParametersException(
262: "mixed geometry types found, please separate Polygons from Lines and Points when saving to *.shp");
263: }
264: }
265: i++;
266: }
267:
268: shpfileName = dp.getProperty(FILE_PROPERTY_KEY);
269:
270: if (shpfileName == null) {
271: shpfileName = dp.getProperty(DEFAULT_VALUE_PROPERTY_KEY);
272: }
273:
274: if (shpfileName == null) {
275: throw new IllegalParametersException(
276: "no output filename specified");
277: }
278:
279: loc = shpfileName.lastIndexOf(File.separatorChar);
280:
281: if (loc == -1) {
282: // loc = 0; // no path - ie. "hills.shp"
283: // path = "";
284: // fname = shpfileName;
285: //probably using the wrong path separator character.
286: throw new Exception(
287: "couldn't find the path separator character '"
288: + File.separatorChar
289: + "' in your shape file name. This you're probably using the unix (or dos) one.");
290: } else {
291: path = shpfileName.substring(0, loc + 1); // ie. "/data1/hills.shp" -> "/data1/"
292: fname = shpfileName.substring(loc + 1); // ie. "/data1/hills.shp" -> "hills.shp"
293: }
294:
295: loc = fname.lastIndexOf(".");
296:
297: if (loc == -1) {
298: throw new IllegalParametersException(
299: "Filename must end in '.shp'");
300: }
301:
302: fname_withoutextention = fname.substring(0, loc); // ie. "hills.shp" -> "hills."
303: dbffname = path + fname_withoutextention + ".dbf";
304:
305: writeDbf(featureCollection, dbffname);
306:
307: // this gc will be a collection of either multi-points, multi-polygons, or multi-linestrings
308: // polygons will have the rings in the correct order
309: gc = makeSHAPEGeometryCollection(featureCollection);
310:
311: shapeType = 2; //x,y
312:
313: if (dp.getProperty(SHAPE_TYPE_PROPERTY_KEY) != null) {
314: String st = dp.getProperty(SHAPE_TYPE_PROPERTY_KEY);
315:
316: if (st.equalsIgnoreCase("xy")) {
317: shapeType = 2;
318: } else if (st.equalsIgnoreCase("xym")) {
319: shapeType = 3;
320: } else if (st.equalsIgnoreCase("xymz")) {
321: shapeType = 4;
322: } else if (st.equalsIgnoreCase("xyzm")) {
323: shapeType = 4;
324: } else if (st.equalsIgnoreCase("xyz")) {
325: shapeType = 4;
326: } else {
327: throw new IllegalParametersException(
328: "ShapefileWriter.write() - dataproperties has a 'ShapeType' that isnt 'xy', 'xym', or 'xymz'");
329: }
330: } else {
331: if (gc.getNumGeometries() > 0) {
332: shapeType = guessCoorinateDims(gc.getGeometryN(0));
333: }
334: }
335:
336: URL url = new URL("file", "localhost", shpfileName);
337: Shapefile myshape = new Shapefile(url);
338: myshape.write(gc, shapeType);
339:
340: shxfname = path + fname_withoutextention + ".shx";
341:
342: BufferedOutputStream in = new BufferedOutputStream(
343: new FileOutputStream(shxfname));
344: EndianDataOutputStream sfile = new EndianDataOutputStream(in);
345:
346: myshape.writeIndex(gc, sfile, shapeType);
347: }
348:
349: /**
350: *Returns: <br>
351: *2 for 2d (default) <br>
352: *4 for 3d - one of the oordinates has a non-NaN z value <br>
353: *(3 is for x,y,m but thats not supported yet) <br>
354: *@param g geometry to test - looks at 1st coordinate
355: **/
356: public int guessCoorinateDims(Geometry g) {
357: Coordinate[] cs = g.getCoordinates();
358:
359: for (int t = 0; t < cs.length; t++) {
360: if (!(Double.isNaN(cs[t].z))) {
361: return 4;
362: }
363: }
364:
365: return 2;
366: }
367:
368: /**
369: * Write a dbf file with the information from the featureCollection.
370: * @param featureCollection column data from collection
371: * @param fname name of the dbf file to write to
372: */
373: void writeDbf(FeatureCollection featureCollection, String fname)
374: throws Exception {
375: DbfFileWriter dbf;
376: FeatureSchema fs;
377: int t;
378: int f;
379: int u;
380: int num;
381:
382: fs = featureCollection.getFeatureSchema();
383:
384: // -1 because one of the columns is geometry
385: DbfFieldDef[] fields = new DbfFieldDef[fs.getAttributeCount() - 1];
386:
387: // dbf column type and size
388: f = 0;
389:
390: for (t = 0; t < fs.getAttributeCount(); t++) {
391: AttributeType columnType = fs.getAttributeType(t);
392: String columnName = fs.getAttributeName(t);
393:
394: if (columnType == AttributeType.INTEGER) {
395: fields[f] = new DbfFieldDef(columnName, 'N', 16, 0);
396: f++;
397: } else if (columnType == AttributeType.DOUBLE) {
398: fields[f] = new DbfFieldDef(columnName, 'N', 33, 16);
399: f++;
400: } else if (columnType == AttributeType.STRING) {
401: int maxlength = findMaxStringLength(featureCollection,
402: t);
403:
404: if (maxlength > 255) {
405: throw new Exception(
406: "ShapefileWriter does not support strings longer than 255 characters");
407: }
408:
409: fields[f] = new DbfFieldDef(columnName, 'C', maxlength,
410: 0);
411: f++;
412: } else if (columnType == AttributeType.DATE) {
413: fields[f] = new DbfFieldDef(columnName, 'D', 8, 0);
414: f++;
415: } else if (columnType == AttributeType.GEOMETRY) {
416: //do nothing - the .shp file handles this
417: } else {
418: throw new Exception(
419: "Shapewriter: unsupported AttributeType found in featurecollection.");
420: }
421: }
422:
423: // write header
424: dbf = new DbfFileWriter(fname);
425: dbf.writeHeader(fields, featureCollection.size());
426:
427: //write rows
428: num = featureCollection.size();
429:
430: List features = featureCollection.getFeatures();
431:
432: for (t = 0; t < num; t++) {
433: //System.out.println("dbf: record "+t);
434: Feature feature = (Feature) features.get(t);
435: Vector DBFrow = new Vector();
436:
437: //make data for each column in this feature (row)
438: for (u = 0; u < fs.getAttributeCount(); u++) {
439: AttributeType columnType = fs.getAttributeType(u);
440:
441: if (columnType == AttributeType.INTEGER) {
442: Object a = feature.getAttribute(u);
443:
444: if (a == null) {
445: DBFrow.add(new Integer(0));
446: } else {
447: DBFrow.add((Integer) a);
448: }
449: } else if (columnType == AttributeType.DOUBLE) {
450: Object a = feature.getAttribute(u);
451:
452: if (a == null) {
453: DBFrow.add(new Double(0.0));
454: } else {
455: DBFrow.add((Double) a);
456: }
457: } else if (columnType == AttributeType.DATE) {
458: Object a = feature.getAttribute(u);
459: if (a == null) {
460: DBFrow.add("");
461: } else {
462: DBFrow
463: .add(DbfFile.DATE_PARSER
464: .format((Date) a));
465: }
466: } else if (columnType == AttributeType.STRING) {
467: Object a = feature.getAttribute(u);
468:
469: if (a == null) {
470: DBFrow.add(new String(""));
471: } else {
472: // MD 16 jan 03 - added some defensive programming
473: if (a instanceof String) {
474: DBFrow.add(a);
475: } else {
476: DBFrow.add(a.toString());
477: }
478: }
479: }
480: }
481:
482: dbf.writeRecord(DBFrow);
483: }
484:
485: dbf.close();
486: }
487:
488: /**
489: *look at all the data in the column of the featurecollection, and find the largest string!
490: *@param fc features to look at
491: *@param attributeNumber which of the column to test.
492: */
493: int findMaxStringLength(FeatureCollection fc, int attributeNumber) {
494: int l;
495: int maxlen = 0;
496: Feature f;
497:
498: for (Iterator i = fc.iterator(); i.hasNext();) {
499: f = (Feature) i.next();
500: l = f.getString(attributeNumber).length();
501:
502: if (l > maxlen) {
503: maxlen = l;
504: }
505: }
506:
507: return maxlen;
508: }
509:
510: /**
511: * Find the generic geometry type of the feature collection.
512: * Simple method - find the 1st non null geometry and its type
513: * is the generic type.
514: * returns 0 - all empty/invalid <br>
515: * 1 - point <br>
516: * 2 - line <br>
517: * 3 - polygon <br>
518: *@param fc feature collection containing tet geometries.
519: **/
520: int findBestGeometryType(FeatureCollection fc) {
521: Geometry geom;
522: // [mmichaud 2007-06-12] : add the type variable to test if
523: // all geometries are single Point
524: // maybe it would be clearer using shapefile types integer for type
525: int type = 0;
526:
527: for (Iterator i = fc.iterator(); i.hasNext();) {
528: geom = ((Feature) i.next()).getGeometry();
529:
530: if (geom instanceof Point) {
531: // [mmichaud 2007-06-12] type is -1 while geometries are Point
532: type = -1;
533: }
534:
535: if (geom instanceof MultiPoint) {
536: return 1;
537: }
538:
539: if (geom instanceof Polygon) {
540: return 3;
541: }
542:
543: if (geom instanceof MultiPolygon) {
544: return 3;
545: }
546:
547: if (geom instanceof LineString) {
548: return 2;
549: }
550:
551: if (geom instanceof MultiLineString) {
552: return 2;
553: }
554: }
555:
556: return type; // return 0 if all geometries are null
557: // return -1 if all geometries are single point
558: }
559:
560: /**
561: * reverses the order of points in lr (is CW -> CCW or CCW->CW)
562: */
563: LinearRing reverseRing(LinearRing lr) {
564: int numPoints = lr.getNumPoints();
565: Coordinate[] newCoords = new Coordinate[numPoints];
566:
567: for (int t = 0; t < numPoints; t++) {
568: newCoords[t] = lr.getCoordinateN(numPoints - t - 1);
569: }
570:
571: return new LinearRing(newCoords, new PrecisionModel(), 0);
572: }
573:
574: /**
575: * make sure outer ring is CCW and holes are CW
576: *@param p polygon to check
577: */
578: Polygon makeGoodSHAPEPolygon(Polygon p) {
579: LinearRing outer;
580: LinearRing[] holes = new LinearRing[p.getNumInteriorRing()];
581: Coordinate[] coords;
582:
583: coords = p.getExteriorRing().getCoordinates();
584:
585: if (cga.isCCW(coords)) {
586: outer = reverseRing((LinearRing) p.getExteriorRing());
587: } else {
588: outer = (LinearRing) p.getExteriorRing();
589: }
590:
591: for (int t = 0; t < p.getNumInteriorRing(); t++) {
592: coords = p.getInteriorRingN(t).getCoordinates();
593:
594: if (!(cga.isCCW(coords))) {
595: holes[t] = reverseRing((LinearRing) p
596: .getInteriorRingN(t));
597: } else {
598: holes[t] = (LinearRing) p.getInteriorRingN(t);
599: }
600: }
601:
602: return new Polygon(outer, holes, new PrecisionModel(), 0);
603: }
604:
605: /**
606: * make sure outer ring is CCW and holes are CW for all the polygons in the Geometry
607: *@param mp set of polygons to check
608: */
609: MultiPolygon makeGoodSHAPEMultiPolygon(MultiPolygon mp) {
610: MultiPolygon result;
611: Polygon[] ps = new Polygon[mp.getNumGeometries()];
612:
613: //check each sub-polygon
614: for (int t = 0; t < mp.getNumGeometries(); t++) {
615: ps[t] = makeGoodSHAPEPolygon((Polygon) mp.getGeometryN(t));
616: }
617:
618: result = new MultiPolygon(ps, new PrecisionModel(), 0);
619:
620: return result;
621: }
622:
623: /**
624: * return a single geometry collection <Br>
625: * result.GeometryN(i) = the i-th feature in the FeatureCollection<br>
626: * All the geometry types will be the same type (ie. all polygons) - or they will be set to<br>
627: * NULL geometries<br>
628: *<br>
629: * GeometryN(i) = {Multipoint,Multilinestring, or Multipolygon)<br>
630: *
631: *@param fc feature collection to make homogeneous
632: */
633: public GeometryCollection makeSHAPEGeometryCollection(
634: FeatureCollection fc) throws Exception {
635: GeometryCollection result;
636: Geometry[] allGeoms = new Geometry[fc.size()];
637:
638: int geomtype = findBestGeometryType(fc);
639:
640: if (geomtype == 0) {
641: throw new Exception(
642: "Could not determine shapefile type - data is either all GeometryCollections or empty");
643: }
644:
645: List features = fc.getFeatures();
646:
647: for (int t = 0; t < features.size(); t++) {
648: Geometry geom;
649: geom = ((Feature) features.get(t)).getGeometry();
650:
651: switch (geomtype) {
652: // 2007/06/12 : add -1 case for collections with only single points
653: // maybe it would be clearer using shapefile types integer for geomtype
654: case -1: //single point
655:
656: if ((geom instanceof Point)) {
657: allGeoms[t] = (Point) geom;
658: } else {
659: allGeoms[t] = new Point(null, new PrecisionModel(),
660: 0);
661: }
662:
663: break;
664:
665: case 1: //point
666:
667: if ((geom instanceof Point)) {
668: //good!
669: Point[] p = new Point[1];
670: p[0] = (Point) geom;
671:
672: allGeoms[t] = new MultiPoint(p,
673: new PrecisionModel(), 0);
674: } else if (geom instanceof MultiPoint) {
675: allGeoms[t] = geom;
676: } else {
677: allGeoms[t] = new MultiPoint(null,
678: new PrecisionModel(), 0);
679: }
680:
681: break;
682:
683: case 2: //line
684:
685: if ((geom instanceof LineString)) {
686: LineString[] l = new LineString[1];
687: l[0] = (LineString) geom;
688:
689: allGeoms[t] = new MultiLineString(l,
690: new PrecisionModel(), 0);
691: } else if (geom instanceof MultiLineString) {
692: allGeoms[t] = geom;
693: } else {
694: allGeoms[t] = new MultiLineString(null,
695: new PrecisionModel(), 0);
696: }
697:
698: break;
699:
700: case 3: //polygon
701:
702: if (geom instanceof Polygon) {
703: //good!
704: Polygon[] p = new Polygon[1];
705: p[0] = (Polygon) geom;
706:
707: allGeoms[t] = makeGoodSHAPEMultiPolygon(new MultiPolygon(
708: p, new PrecisionModel(), 0));
709: } else if (geom instanceof MultiPolygon) {
710: allGeoms[t] = makeGoodSHAPEMultiPolygon((MultiPolygon) geom);
711: } else {
712: allGeoms[t] = new MultiPolygon(null,
713: new PrecisionModel(), 0);
714: }
715:
716: break;
717: }
718: }
719:
720: result = new GeometryCollection(allGeoms, new PrecisionModel(),
721: 0);
722:
723: return result;
724: }
725: }
|