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}