001/*
002 * Copyright (c) 2014-2015 The Regents of the University of California.
003 * All rights reserved.
004 *
005 * '$Author: crawl $'
006 * '$Date: 2018-03-15 18:14:49 +0000 (Thu, 15 Mar 2018) $' 
007 * '$Revision: 34676 $'
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.util;
030
031import java.io.ByteArrayInputStream;
032import java.io.File;
033import java.io.FileInputStream;
034import java.io.IOException;
035import java.io.InputStream;
036import java.util.Collection;
037import java.util.HashMap;
038import java.util.HashSet;
039import java.util.LinkedList;
040import java.util.Map;
041import java.util.Set;
042
043import javax.xml.parsers.ParserConfigurationException;
044
045import org.apache.commons.io.FileUtils;
046import org.apache.commons.io.FilenameUtils;
047import org.geotools.GML;
048import org.geotools.GML.Version;
049import org.geotools.data.DataStore;
050import org.geotools.data.DataStoreFinder;
051import org.geotools.data.DataUtilities;
052import org.geotools.data.FeatureSource;
053import org.geotools.data.FileDataStoreFinder;
054import org.geotools.data.ogr.OGRDataStoreFactory;
055import org.geotools.data.ogr.bridj.BridjOGRDataStoreFactory;
056import org.geotools.data.simple.SimpleFeatureCollection;
057import org.geotools.data.simple.SimpleFeatureIterator;
058import org.geotools.feature.DefaultFeatureCollection;
059import org.geotools.feature.simple.SimpleFeatureBuilder;
060import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
061import org.geotools.filter.text.ecql.ECQL;
062import org.geotools.geojson.feature.FeatureJSON;
063import org.geotools.geometry.jts.JTS;
064import org.geotools.geometry.jts.ReferencedEnvelope;
065import org.geotools.kml.KMLConfiguration;
066import org.geotools.referencing.CRS;
067import org.geotools.xml.Parser;
068import org.json.JSONException;
069import org.json.JSONObject;
070import org.kepler.gis.data.VectorToken;
071import org.opengis.feature.Feature;
072import org.opengis.feature.simple.SimpleFeature;
073import org.opengis.feature.simple.SimpleFeatureType;
074import org.opengis.filter.Filter;
075import org.opengis.geometry.MismatchedDimensionException;
076import org.opengis.geometry.coordinate.Polygon;
077import org.opengis.referencing.FactoryException;
078import org.opengis.referencing.crs.CoordinateReferenceSystem;
079import org.opengis.referencing.operation.MathTransform;
080import org.opengis.referencing.operation.TransformException;
081import org.xml.sax.SAXException;
082
083import com.vividsolutions.jts.geom.Geometry;
084
085import ptolemy.kernel.util.IllegalActionException;
086import ptolemy.util.MessageHandler;
087
088/** A collection of utilities for vectors/features.
089 * 
090 *  @author Daniel Crawl
091 *  @version $Id: VectorUtilities.java 34676 2018-03-15 18:14:49Z crawl $
092 *  
093 */
094public class VectorUtilities {
095
096    /** Close all data stores opened in this thread while reading files. */
097    public static void closeDataStores() {
098        
099        synchronized(_dataStoresMap) {
100            Set<DataStore> dataStores = _dataStoresMap.get(Thread.currentThread());
101            if(dataStores != null) {
102                for(DataStore store : dataStores) {
103                    //System.out.println(Thread.currentThread().getId() + " closing " + store);
104                    store.dispose();
105                }
106                dataStores.clear();
107                _dataStoresMap.remove(Thread.currentThread());
108            }
109        }
110    }
111
112    /** Create an empty feature schema
113     *  @param defaultCRS The default CRS. can be null.
114     *  @return an empty feature schema.
115     */
116    public static SimpleFeatureType createEmptySchema(CoordinateReferenceSystem defaultCRS) {
117        SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
118        builder.setName("unknown");
119        if(defaultCRS != null) {
120            builder.setCRS(defaultCRS);
121        }
122        builder.add("geom", Polygon.class);
123        return builder.buildFeatureType();
124    }
125
126    /** Create a vector token from reading a file containing features. */
127    public static VectorToken readFile(File inputFile)
128            throws IllegalActionException, IOException {
129        return readFile(inputFile, null, null);
130    }
131    
132    /** Create a vector token from a specific type in a file containing features.
133     *  @param inputFile the file to read
134     *  @param typeNameStr the feature type in the file to read. this can be
135     *  null, but must be specified if more than one feature type is present
136     *  in the file.
137     */
138    public static VectorToken readFile(File inputFile, String typeNameStr) 
139            throws IllegalActionException, IOException {
140        return readFile(inputFile, typeNameStr, null);
141    }
142
143    /** Create a vector token from a specific type in a file containing features.
144     *  @param inputFile the file to read
145     *  @param typeNameStr the feature type in the file to read. this can be
146     *  null, but must be specified if more than one feature type is present
147     *  in the file.
148     *  @param crs The coordinate reference system used if not specified in the file.
149     */
150    public static VectorToken readFile(File inputFile, String typeNameStr, CoordinateReferenceSystem defaultCRS)
151            throws IllegalActionException, IOException {
152        
153        //System.out.println("reading " + inputFile);
154        
155        SimpleFeatureCollection features;
156        
157        final String filenameStr = inputFile.getAbsolutePath();
158        
159        if(filenameStr.toLowerCase().endsWith(".json") ||
160                filenameStr.toLowerCase().endsWith(".geojson")) {
161            try {
162                return readGeoJSONString(FileUtils.readFileToString(inputFile), defaultCRS);
163            } catch (JSONException e) {
164                throw new IOException("Error parsing " + inputFile +
165                    ": " + e.getMessage(), e);
166            }
167        } else if(filenameStr.toLowerCase().endsWith(".gml")) {
168            try {
169                // TODO defaultCRS
170                return readGMLString(FileUtils.readFileToString(inputFile));
171            } catch(IOException | IllegalActionException e) {
172                throw new IOException("Error parsing " + inputFile +
173                    ": " + e.getMessage(), e);
174            }            
175        } else if(filenameStr.toLowerCase().endsWith(".kml")) {
176            
177            Parser parser = new Parser(new KMLConfiguration());
178            
179            try(FileInputStream stream = new FileInputStream(inputFile)) {
180                SimpleFeature feature = (SimpleFeature) parser.parse(stream);
181                Collection<SimpleFeature> collection =
182                        (Collection<SimpleFeature>) feature.getAttribute("Feature");
183                features = new DefaultFeatureCollection();
184                ((DefaultFeatureCollection) features).addAll(collection);
185                
186                if(defaultCRS != null &&
187                    features.getSchema().getCoordinateReferenceSystem() == null) {
188                    features = setCRS(features, defaultCRS);
189                }                
190                return new VectorToken(features);
191            } catch (IOException | SAXException | ParserConfigurationException e) {
192                throw new IOException("Error reading KML file " + filenameStr, e);
193            }
194
195        }
196        
197        DataStore store = _getDataStoreForFile(inputFile);
198        
199        try {
200            return _getVectorDataFromStore(store, typeNameStr, defaultCRS);
201        } catch(Exception e) {
202            throw new IOException("Error reading vector data from file " + inputFile, e);
203        }
204    }
205
206    /** Read a string containing either GML or GeoJSON. */
207    public static VectorToken readGeoString(String geoString, CoordinateReferenceSystem defaultCRS)
208        throws IllegalActionException, IOException, JSONException {
209    
210        // see if it looks like GML
211        if(geoString.startsWith("<?xml") || geoString.startsWith("<gml")) {
212            return readGMLString(geoString /*, TODO defaultCRS*/);
213        }
214        
215        // try geojson
216        return readGeoJSONString(geoString, defaultCRS);
217    }
218
219    /** Read a string containing GeoJSON. */
220    public static VectorToken readGeoJSONString(String geoJSON) throws IOException, JSONException, IllegalActionException {
221        return readGeoJSONString(geoJSON, null);
222    }
223    
224    /** Read a string containing GeoJSON using a default CRS. The default
225     *  CRS is only used if the GeoJSON string does not contain the CRS. 
226     */
227    public static VectorToken readGeoJSONString(String geoJSON, CoordinateReferenceSystem defaultCRS) throws IOException, JSONException, IllegalActionException {
228        
229        SimpleFeatureCollection features = readGeoJSONString(geoJSON, defaultCRS, null);
230        return new VectorToken(features);
231    }
232    
233    /** Read a string containing GeoJSON.
234     *  @param geoJSON the geojson string.
235     *  @param defaultCRS CRS used if one not found in geojson string.
236     *  @param defaultType feture type used if one not found in geojson string.
237     *  @return Features in geojson string.
238     */
239    public static SimpleFeatureCollection readGeoJSONString(String geoJSON, CoordinateReferenceSystem defaultCRS, SimpleFeatureType defaultType) throws IOException, JSONException, IllegalActionException {
240        
241        FeatureJSON featureJSON = new FeatureJSON();
242        
243        if(defaultType != null) {
244            featureJSON.setFeatureType(defaultType);
245        }
246        
247        byte[] bytes = geoJSON.getBytes();
248        
249        JSONObject json;
250        try {
251            json = new JSONObject(geoJSON);
252        } catch (JSONException e) {
253            throw new IOException("Error converting string to JSON.", e);
254        }
255        
256        if(!json.has("type")) {
257            throw new IOException("No type field in GeoJSON string.");
258        }
259        String type = json.getString("type");
260        
261        SimpleFeatureCollection features = null;
262        
263        if(type.equals("FeatureCollection")) {
264            try(InputStream stream = new ByteArrayInputStream(bytes)) {
265                features =  (SimpleFeatureCollection) featureJSON.readFeatureCollection(stream);
266            } 
267        } else if(type.equals("Feature")) {            
268            try(InputStream stream = new ByteArrayInputStream(bytes)) {
269                SimpleFeature feature = featureJSON.readFeature(stream);
270                features = DataUtilities.collection(feature);
271            }
272        } else {
273            System.err.println("WARNING: unhandled type of GeoJSON: " + type);
274            features = DataUtilities.collection(new LinkedList<SimpleFeature>());
275        }
276        
277        // see if the GeoJSON string does not have a CRS and we were
278        // given a default CRS
279        if(defaultCRS != null && !json.has("crs")) {
280            features = setCRS(features, defaultCRS);       
281        }
282        
283        return features;
284    }
285        
286    /** Read a string containing GML. */
287    public static VectorToken readGMLString(String gmlString) throws IOException, IllegalActionException {
288        
289        // TODO try version 3, too. how can we distinguish?
290        
291        GML gml = new GML(Version.GML2);
292        
293        try(InputStream stream = new ByteArrayInputStream(gmlString.getBytes())) {
294            SimpleFeatureCollection features;
295            try {
296                features = gml.decodeFeatureCollection(stream);
297            } catch (SAXException | ParserConfigurationException e) {
298                throw new IOException("Error parsing GML: " + e.getMessage(), e);
299            }
300            //System.out.println(toGeoJSONString(features));
301            return new VectorToken(features);
302        }
303    }
304
305    /** Get a vector token from a WFS service.
306     *  @param urlStr the WFS url
307     *  @param typeNameStr the feature type to read. can be null, but must
308     *  be specified if more than one type exists on in the WFS service.
309     * 
310     */
311    public static VectorToken readWFS(String urlStr, String typeNameStr) throws IOException, IllegalActionException {
312        
313        String getCapabilities = null;
314        
315        if(urlStr.toLowerCase().contains("request=getcapabilities")) {
316            getCapabilities = urlStr;
317        } else {
318            getCapabilities = urlStr + "?service=wfs&version=1.0.0&REQUEST=GetCapabilities";
319        }
320
321        System.out.println("Using WFS url: " + getCapabilities);
322        
323        Map<String,Object> connectionParameters = new HashMap<String,Object>();
324        connectionParameters.put("WFSDataStoreFactory:GET_CAPABILITIES_URL", getCapabilities);
325        // NOTE: the default timeout is 3000ms, which is too small
326        // for some servers
327        connectionParameters.put("WFSDataStoreFactory:TIMEOUT", 20000);
328        
329        DataStore store = DataStoreFinder.getDataStore(connectionParameters);
330
331        return _getVectorDataFromStore(store, typeNameStr, null);
332    }
333
334    /** Reproject a set of features.
335     *  @param features The set of features.
336     *  @param destCRS The new coordinate reference system.
337     *  @return The reprojected set of features.
338     */
339    public static SimpleFeatureCollection reproject(SimpleFeatureCollection features,
340        CoordinateReferenceSystem destCRS) throws IllegalActionException {
341        return reproject(features, features.getSchema().getCoordinateReferenceSystem(),
342            destCRS);
343    }
344    
345    /** Reproject a set of features from the specified coordinate reference system.
346     *  @param features The set of features.
347     *  @param crs The source coordinate reference system.
348     *  @param destCRS The new coordinate reference system.
349     *  @return The reprojected set of features.
350     */
351    public static SimpleFeatureCollection reproject(SimpleFeatureCollection features,
352        CoordinateReferenceSystem crs, CoordinateReferenceSystem destCRS)
353        throws IllegalActionException {
354
355        if(crs == null) {
356            throw new IllegalActionException("Source CRS is null.");
357        }
358        
359        if(crs.equals(destCRS)) {
360            return features;
361        }
362
363        MathTransform transform;
364        try {
365            transform = CRS.findMathTransform(crs, destCRS, true);
366        } catch (FactoryException e) {
367            throw new IllegalActionException(
368                "Error finding reprojection transformation: " + e.getMessage());
369        }
370        
371        SimpleFeatureType transformedSchema = SimpleFeatureTypeBuilder.retype(features.getSchema(), destCRS);
372            
373        DefaultFeatureCollection newCollection =
374            new DefaultFeatureCollection(features.getID(), transformedSchema);
375        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(transformedSchema);
376        
377        try(SimpleFeatureIterator iterator = features.features();) {
378            while(iterator.hasNext()) {
379                SimpleFeature feature = (SimpleFeature) iterator.next();
380                Geometry geometry = (Geometry) feature.getDefaultGeometry();
381                Geometry reprojectedGeometry = JTS.transform(geometry, transform);
382                
383                builder.init(feature);
384                SimpleFeature newFeature = builder.buildFeature(feature.getID());
385                newFeature.setDefaultGeometry(reprojectedGeometry);
386                newCollection.add(newFeature);
387            }
388            
389        } catch (MismatchedDimensionException | TransformException e) {
390            throw new IllegalActionException("Error reprojecting features: " + e.getMessage());
391        }
392        
393        return newCollection;
394    }
395    
396    /** Set the coordinate reference system for a set of features. */
397    public static SimpleFeatureCollection setCRS(SimpleFeatureCollection features, CoordinateReferenceSystem defaultCRS) {
398        
399        SimpleFeatureType schema = features.getSchema();
400        
401        SimpleFeatureType transformedSchema;
402        if(schema == null) {
403            transformedSchema = createEmptySchema(defaultCRS);
404        } else {
405            transformedSchema = SimpleFeatureTypeBuilder.retype(schema, defaultCRS);
406        }
407        
408        DefaultFeatureCollection newCollection =
409            new DefaultFeatureCollection(features.getID(), transformedSchema);
410        SimpleFeatureBuilder builder = new SimpleFeatureBuilder(transformedSchema);
411        
412        try(SimpleFeatureIterator iterator = features.features();) {
413            while(iterator.hasNext()) {
414                SimpleFeature feature = (SimpleFeature) iterator.next();
415                Geometry geometry = (Geometry) feature.getDefaultGeometry();
416                
417                builder.init(feature);
418                SimpleFeature newFeature = builder.buildFeature(feature.getID());
419                newFeature.setDefaultGeometry(geometry);
420                newCollection.add(newFeature);
421            }
422        }
423        return newCollection;
424    }
425
426    /** Convert a set of features into a GeoJSON string. */
427    public static String toGeoJSONString(SimpleFeatureCollection features) {
428        
429        FeatureJSON featureJSON = new FeatureJSON();
430        featureJSON.setEncodeFeatureCollectionCRS(true);
431        try {
432            return featureJSON.toString(features);
433        } catch (IOException e) {
434            MessageHandler.error("Error writing feature collection.", e);
435            return "";
436        }
437    }
438
439    ////////////////////////////////////////////////////////////////////////
440    //// private methods                                                ////
441
442    /** Get the data store for a file. */
443    private static DataStore _getDataStoreForFile(File file) throws IOException {
444        
445        if(file.getName().endsWith(".shp")) {        
446            return FileDataStoreFinder.getDataStore(file);
447        } else {
448            String extension = FilenameUtils.getExtension(file.getName());            
449            Map<String, String> connectionParams = new HashMap<String, String>();
450            connectionParams.put("DriverName", extension.toUpperCase());
451            connectionParams.put("DatasourceName", file.getAbsolutePath());
452            return _ogrFactory.createDataStore(connectionParams);
453        }        
454    }
455
456    /** Create a vector token from reading a data store.
457     *  @param store the data store
458     *  @param typeNameStr the feature type to read. can be null, but must be
459     *  specified if the data store contains more than one type.
460     *  @param defaultCRS The coordinate reference system used if not specified in the file.
461     */
462    private static VectorToken _getVectorDataFromStore(DataStore store,
463            String typeNameStr, CoordinateReferenceSystem defaultCRS)
464            throws IllegalActionException, IOException {
465        
466        synchronized(_dataStoresMap) {
467            Set<DataStore> stores = _dataStoresMap.get(Thread.currentThread());
468            if(stores == null) {
469                stores = new HashSet<DataStore>();
470                _dataStoresMap.put(Thread.currentThread(), stores);
471            }
472            stores.add(store);
473            //System.out.println(Thread.currentThread().getId() + " opened " + inputFile);
474        }
475
476        String[] typeNames = store.getTypeNames();
477        
478        if(typeNames.length == 0) {
479            throw new IllegalActionException("No type names found in data store.");
480        }
481                
482        if(typeNameStr == null) {
483            if(typeNames.length == 1) {
484                return new VectorToken(store.getFeatureSource(typeNames[0]), defaultCRS);
485            } else {
486                throw new IllegalActionException("More than one feature type found in the input.\n" +
487                        "Specify the type in the typeName parameter.");
488            }
489        }
490        
491        for(String name : typeNames) {
492            if(name.equals(typeNameStr)) {
493                return new VectorToken(store.getFeatureSource(typeNameStr), defaultCRS);
494            }
495        }
496        
497        throw new IllegalActionException("Type " + typeNameStr +
498                " was not found in the data store.");
499        
500    }
501        
502    ////////////////////////////////////////////////////////////////////////
503    //// private variables                                              ////
504
505    /** Factory to read vector/feature files using GDAL via Bridj. */
506    private static OGRDataStoreFactory _ogrFactory = new BridjOGRDataStoreFactory();
507
508    /** Mapping of thread to data store. */
509    private static Map<Thread,Set<DataStore>> _dataStoresMap =
510            new HashMap<Thread,Set<DataStore>>();
511    
512    
513    /** For debugging, ignore. */
514    public static void main(String[] args) {
515        
516        try {
517            String getCapabilities = "http://mapserver.flightgear.org/ms?Service=WFS&Version=1.0.0&request=GetCapabilities";
518    
519            Map<String,Object> connectionParameters = new HashMap<String,Object>();
520            connectionParameters.put("WFSDataStoreFactory:GET_CAPABILITIES_URL", getCapabilities );
521            connectionParameters.put("WFSDataStoreFactory:TIMEOUT", 20000);
522
523            // Step 2 - connection
524            DataStore data = DataStoreFinder.getDataStore( connectionParameters );
525    
526            // Step 3 - discovery
527            //String typeNames[] = data.getTypeNames();
528            //String typeName = typeNames[0];
529            String typeName = "cs_lake";
530            SimpleFeatureType schema = data.getSchema( typeName );
531            
532            // Step 4 - target
533            FeatureSource<SimpleFeatureType, SimpleFeature> source = data.getFeatureSource( typeName );
534            System.out.println( "Metadata Bounds:"+ source.getBounds() );
535    
536            // Step 5 - query
537            String geomName = schema.getGeometryDescriptor().getLocalName();
538            System.out.println("geom name = " + geomName);
539            
540            /*
541            ReferencedEnvelope bbox = new ReferencedEnvelope(-117.6488, 32.501, -116.058, 33.66,
542                    DefaultGeographicCRS.WGS84 );
543    
544            FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2( GeoTools.getDefaultHints() );
545            Filter filter = ff.bbox(ff.property(geomName), bbox);
546             */
547            
548            Filter filter = ECQL.toFilter("BBOX(msGeometry, -117.6488, 32.501, -116.058, 33.66)");
549            
550            SimpleFeatureCollection features = (SimpleFeatureCollection) source.getFeatures( filter );
551    
552            System.out.println("filter = " + filter);
553
554            System.out.println("there are " + features.size() + " features");
555            
556            ReferencedEnvelope bounds = new ReferencedEnvelope();
557            try(SimpleFeatureIterator iterator = features.features()) {
558                while( iterator.hasNext() ){
559                    Feature feature = (Feature) iterator.next();
560                    bounds.include( feature.getBounds() );
561                }
562                System.out.println( "Calculated Bounds:"+ bounds );    
563            }       
564            
565            //System.out.println(toGeoJSONString(features));
566            
567        } catch(Throwable t) {
568            System.err.println("Error: " + t.getMessage());
569        }
570    }
571}