001/* 002 * Copyright (c) 2014-2015 The Regents of the University of California. 003 * All rights reserved. 004 * 005 * '$Author: crawl $' 006 * '$Date: 2015-12-23 23:10:24 +0000 (Wed, 23 Dec 2015) $' 007 * '$Revision: 34418 $' 008 * 009 * Permission is hereby granted, without written agreement and without 010 * license or royalty fees, to use, copy, modify, and distribute this 011 * software and its documentation for any purpose, provided that the above 012 * copyright notice and the following two paragraphs appear in all copies 013 * of this software. 014 * 015 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 016 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 017 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 018 * THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF 019 * SUCH DAMAGE. 020 * 021 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 022 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 023 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 024 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF 025 * CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, 026 * ENHANCEMENTS, OR MODIFICATIONS. 027 * 028 */ 029package org.kepler.gis.actor.io; 030 031import java.io.File; 032import java.io.FileOutputStream; 033import java.io.IOException; 034import java.io.OutputStream; 035import java.io.Serializable; 036import java.net.MalformedURLException; 037import java.util.ArrayList; 038import java.util.HashMap; 039import java.util.List; 040import java.util.Map; 041 042import org.apache.commons.io.FilenameUtils; 043import org.geotools.data.DataStoreFactorySpi; 044import org.geotools.data.DataUtilities; 045import org.geotools.data.DefaultTransaction; 046import org.geotools.data.Transaction; 047import org.geotools.data.collection.ListFeatureCollection; 048import org.geotools.data.ogr.bridj.BridjOGRDataStoreFactory; 049import org.geotools.data.shapefile.ShapefileDataStore; 050import org.geotools.data.shapefile.ShapefileDataStoreFactory; 051import org.geotools.data.simple.SimpleFeatureCollection; 052import org.geotools.data.simple.SimpleFeatureIterator; 053import org.geotools.data.simple.SimpleFeatureSource; 054import org.geotools.data.simple.SimpleFeatureStore; 055import org.geotools.feature.NameImpl; 056import org.geotools.feature.simple.SimpleFeatureTypeImpl; 057import org.geotools.feature.type.GeometryDescriptorImpl; 058import org.geotools.feature.type.GeometryTypeImpl; 059import org.geotools.geojson.feature.FeatureJSON; 060import org.geotools.kml.KML; 061import org.geotools.kml.KMLConfiguration; 062import org.geotools.referencing.crs.DefaultGeographicCRS; 063import org.geotools.xml.Encoder; 064import org.kepler.gis.data.VectorToken; 065import org.kepler.gis.util.GISUtilities; 066import org.kepler.gis.util.VectorUtilities; 067import org.opengis.feature.simple.SimpleFeature; 068import org.opengis.feature.simple.SimpleFeatureType; 069import org.opengis.feature.type.AttributeDescriptor; 070import org.opengis.feature.type.AttributeType; 071import org.opengis.feature.type.GeometryDescriptor; 072import org.opengis.feature.type.GeometryType; 073import org.opengis.filter.identity.FeatureId; 074import org.opengis.referencing.crs.CoordinateReferenceSystem; 075 076import ptolemy.data.StringToken; 077import ptolemy.kernel.CompositeEntity; 078import ptolemy.kernel.util.IllegalActionException; 079import ptolemy.kernel.util.NameDuplicationException; 080 081/** Write a vector/feature data set to a file. 082 * <p> 083 * If the coordinate reference system is specified in <i>crs</i> 084 * and is different than the crs in the input data set, then the 085 * data set will be reprojected. 086 * 087 * @author Daniel Crawl 088 * @version $Id: VectorWriter.java 34418 2015-12-23 23:10:24Z crawl $ 089 */ 090public class VectorWriter extends GISWriter { 091 092 public VectorWriter(CompositeEntity container, String name) 093 throws IllegalActionException, NameDuplicationException { 094 super(container, name); 095 096 data.setTypeEquals(VectorToken.VECTOR); 097 098 // TODO add to type combo box 099 type.addChoice("GeoJSON"); 100 type.addChoice("KML"); 101 type.addChoice("Shapefile"); 102 } 103 104 @Override 105 public void fire() throws IllegalActionException { 106 107 super.fire(); 108 109 VectorToken token = (VectorToken) data.get(0); 110 SimpleFeatureCollection features = token.getVectors(); 111 //CoordinateReferenceSystem currentCRS = vectorData.getCRS(); 112 CoordinateReferenceSystem currentCRS = null; 113 SimpleFeatureType currentSchema = features.getSchema(); 114 if(currentSchema != null) { 115 currentCRS = currentSchema.getCoordinateReferenceSystem(); 116 } 117 118 boolean outputIsGeoJSON = false; 119 boolean outputIsKML = false; 120 DataStoreFactorySpi storeFactory = null; 121 122 // see if type was specified 123 if(_typeStr != null && !_typeStr.isEmpty()) { 124 125 if(_typeStr.equalsIgnoreCase("ESRI Shapefile") || 126 _typeStr.equalsIgnoreCase("Shapefile")) { 127 storeFactory = new ShapefileDataStoreFactory(); 128 } else if(_typeStr.equalsIgnoreCase("GeoJSON")) { 129 outputIsGeoJSON = true; 130 } else if(_typeStr.equalsIgnoreCase("KML")) { 131 outputIsKML = true; 132 } else { 133 storeFactory = new BridjOGRDataStoreFactory(); 134 } 135 } else if(_fileNameStr.toLowerCase().endsWith(".json") || 136 _fileNameStr.toLowerCase().endsWith(".geojson")) { 137 outputIsGeoJSON = true; 138 } else if(_fileNameStr.toLowerCase().endsWith(".kml")) { 139 outputIsKML = true; 140 } else if(_outputFile.getName().endsWith(".shp")) { 141 storeFactory = new ShapefileDataStoreFactory(); 142 } else { 143 storeFactory = new BridjOGRDataStoreFactory(); 144 } 145 146 if(outputIsKML) { 147 if(_crs == null) { 148 System.out.println(getName() + " Destination CRS not specified; using WGS84."); 149 _crs = DefaultGeographicCRS.WGS84; 150 } else if(!GISUtilities.isCRSWGS84(_crs)) { 151 System.err.println("WARNING: changing CRS to WGS84 for KML."); 152 _crs = DefaultGeographicCRS.WGS84; 153 } 154 } 155 156 // see if we need to reproject the data 157 158 //System.out.println("source crs = " + currentCRS); 159 //System.out.println("dest crs = " + destCRS); 160 161 if(currentCRS == null) { 162 System.out.println("Source CRS is not specified; assuming WGS84."); 163 currentCRS = DefaultGeographicCRS.WGS84; 164 } 165 166 SimpleFeatureCollection transformedFeatures; 167 168 if(_crs == null) { 169 System.out.println("Destination CRS is not specified; assuming same as source CRS: " + currentCRS.getName()); 170 _crs = currentCRS; 171 transformedFeatures = features; 172 } else { 173 transformedFeatures = VectorUtilities.reproject(features, currentCRS, _crs); 174 } 175 176 //System.out.println("new schema = " + newSchema); 177 178 if(outputIsGeoJSON) { 179 180 FeatureJSON featureJSON = new FeatureJSON(); 181 //featureJSON.setEncodeFeatureCRS(true); 182 featureJSON.setEncodeFeatureCollectionCRS(true); 183 try(OutputStream stream = new FileOutputStream(_fileNameStr);) { 184 featureJSON.writeFeatureCollection(transformedFeatures, stream); 185 } catch (IOException e) { 186 throw new IllegalActionException(this, e, "Error writing GeoJSON output."); 187 } 188 189 } else if(outputIsKML) { 190 191 //System.out.println("size = " + features.size()); 192 193 Encoder encoder = new Encoder(new KMLConfiguration()); 194 encoder.setIndenting(true); 195 encoder.setIndentSize(4); 196 197 try(OutputStream stream = new FileOutputStream(_fileNameStr);) { 198 encoder.encode(transformedFeatures, KML.kml, stream); 199 } catch (IOException e) { 200 throw new IllegalActionException(this, e, "Error writing KML to " + _fileNameStr); 201 } 202 203 } else if(storeFactory == null) { 204 throw new IllegalActionException(this, "Could not find DataStore for output."); 205 } else if(storeFactory instanceof ShapefileDataStoreFactory) { 206 207 Map<String, Serializable> params = new HashMap<String, Serializable>(); 208 try { 209 params.put("url", _outputFile.toURI().toURL()); 210 System.out.println("writing to " + _outputFile.toURI().toURL()); 211 } catch (MalformedURLException e) { 212 throw new IllegalActionException(this, e, "Bad URL."); 213 } 214 params.put("create spatial index", Boolean.FALSE); 215 216 217 ShapefileDataStore store; 218 try { 219 store = (ShapefileDataStore) ((ShapefileDataStoreFactory)storeFactory).createNewDataStore(params); 220 } catch (IOException e) { 221 throw new IllegalActionException(this, e, "Error create new data store."); 222 } 223 224 // Geotools internally calls File.canWrite() to see if 225 // the Shapefile .shp and .dbf files can be written to. 226 // canWrite() returns true if the file exists and is writeable, 227 // so create the file if it does not exist. 228 File[] files = new File[] { 229 _outputFile, 230 new File(_outputFile.getParentFile(), 231 FilenameUtils.getBaseName(_outputFile.getAbsolutePath()) + ".dbf") 232 }; 233 for(File file : files) { 234 if(!file.exists()) { 235 try { 236 if(file.createNewFile()) { 237 System.out.println("created empty file " + file); 238 } else { 239 System.out.println("WARNING: file already existed(?) " + file); 240 } 241 } catch (IOException e) { 242 throw new IllegalActionException(this, e, 243 "Error creating " + file); 244 } 245 } 246 247 if(!file.canWrite()) { 248 throw new IllegalActionException(this, "Cannot write to " + file); 249 } 250 } 251 252 SimpleFeatureType transformedSchema = transformedFeatures.getSchema(); 253 254 // from https://gitlab.com/snippets/9275 255 256 GeometryDescriptor geom = transformedSchema.getGeometryDescriptor(); 257 String oldGeomAttrib = ""; 258 259 try { 260 261 /* 262 * Write the features to the shapefile 263 */ 264 Transaction transaction = new DefaultTransaction("create"); 265 266 String typeName = store.getTypeNames()[0]; 267 SimpleFeatureSource featureSource = store.getFeatureSource(typeName); 268 269 /* 270 * The Shapefile format has a couple limitations: - "the_geom" 271 * is always first, and used for the geometry attribute name - 272 * "the_geom" must be of type Point, MultiPoint, 273 * MuiltiLineString, MultiPolygon - Attribute names are limited 274 * in length - Not all data types are supported (example 275 * Timestamp represented as Date) 276 * 277 * Because of this we have to rename the geometry element and 278 * then rebuild the features to make sure that it is the first 279 * attribute. 280 */ 281 282 List<AttributeDescriptor> attributes = transformedSchema.getAttributeDescriptors(); 283 GeometryType geomType = null; 284 List<AttributeDescriptor> attribs = new ArrayList<AttributeDescriptor>(); 285 for (AttributeDescriptor attrib : attributes) { 286 AttributeType attributeType = attrib.getType(); 287 if (attributeType instanceof GeometryType) { 288 geomType = (GeometryType) attributeType; 289 oldGeomAttrib = attrib.getLocalName(); 290 } else { 291 attribs.add(attrib); 292 } 293 } 294 295 GeometryTypeImpl gt = new GeometryTypeImpl(new NameImpl("the_geom"), geomType.getBinding(), 296 geomType.getCoordinateReferenceSystem(), geomType.isIdentified(), geomType.isAbstract(), 297 geomType.getRestrictions(), geomType.getSuper(), geomType.getDescription()); 298 299 GeometryDescriptor geomDesc = new GeometryDescriptorImpl(gt, new NameImpl("the_geom"), 300 geom.getMinOccurs(), geom.getMaxOccurs(), geom.isNillable(), geom.getDefaultValue()); 301 302 attribs.add(0, geomDesc); 303 304 SimpleFeatureType shpType = new SimpleFeatureTypeImpl(transformedSchema.getName(), attribs, geomDesc, 305 transformedSchema.isAbstract(), transformedSchema.getRestrictions(), transformedSchema.getSuper(), transformedSchema.getDescription()); 306 307 store.createSchema(shpType); 308 309 if (featureSource instanceof SimpleFeatureStore) { 310 SimpleFeatureStore featureStore = (SimpleFeatureStore) featureSource; 311 312 List<SimpleFeature> feats = new ArrayList<SimpleFeature>(); 313 314 try(SimpleFeatureIterator features2 = transformedFeatures.features()) { 315 while (features2.hasNext()) { 316 SimpleFeature f = features2.next(); 317 SimpleFeature reType = DataUtilities.reType(shpType, f, true); 318 // set the default Geom (the_geom) from the original 319 // Geom 320 reType.setAttribute("the_geom", f.getAttribute(oldGeomAttrib)); 321 322 feats.add(reType); 323 } 324 } 325 SimpleFeatureCollection collection = new ListFeatureCollection(shpType, feats); 326 327 featureStore.setTransaction(transaction); 328 try { 329 List<FeatureId> ids = featureStore.addFeatures(collection); 330 transaction.commit(); 331 } catch (Exception problem) { 332 problem.printStackTrace(); 333 transaction.rollback(); 334 } finally { 335 transaction.close(); 336 } 337 store.dispose(); 338 } else { 339 transaction.close(); 340 store.dispose(); 341 throw new IllegalActionException(this, "ShapefileStore not writable"); 342 } 343 } catch (IOException e) { 344 e.printStackTrace(); 345 } 346 347 } else { 348 349 // TODO 350 throw new IllegalActionException(this, "Unsupport output type: " + storeFactory); 351 352 /* 353 Transaction transaction = new DefaultTransaction("create"); 354 355 String typeName; 356 try { 357 typeName = store.getTypeNames()[0]; 358 } catch (IOException e) { 359 throw new IllegalActionException(this, e, "Error get type names from data store."); 360 } 361 362 363 System.out.println("type name = " + typeName); 364 365 SimpleFeatureSource source; 366 try { 367 source = store.getFeatureSource(typeName); 368 } catch (IOException e) { 369 throw new IllegalActionException(this, e, "Error getting feature source for type " + typeName); 370 } 371 372 if(source instanceof SimpleFeatureStore) { 373 374 SimpleFeatureStore simpleFeatureStore = (SimpleFeatureStore) source; 375 simpleFeatureStore.setTransaction(transaction); 376 try { 377 try { 378 simpleFeatureStore.addFeatures((FeatureCollection<SimpleFeatureType, SimpleFeature>) features); 379 transaction.commit(); 380 } catch (IOException e) { 381 transaction.rollback(); 382 throw new IllegalActionException(this, e, "Error writing features."); 383 } finally { 384 transaction.close(); 385 } 386 } catch(IOException e) { 387 throw new IllegalActionException(this, e, "Transaction I/O error."); 388 } 389 390 391 } else { 392 throw new IllegalActionException(this, "Unsupported type of store: " + store.getClass()); 393 } 394 */ 395 396 } 397 398 // finally send output name 399 done.broadcast(new StringToken(_fileNameStr)); 400 401 } 402}