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}