Geotools操作GeoJSON:解析FeatureCollection對象文件
GeoJSON是基於JavaScript的對象的地理信息數據格式。
GeoJSON格式示例:
{ "type":"FeatureCollection", "features":[ { "type":"Feature", "properties":{ "area": 3865207830, "text": null }, "id":"polygon.1", "geometry":{ "type":"Polygon", "coordinates":[ [ [ 116.19827270507814, 39.78321267821705 ], [ 116.04446411132814, 39.232253141714914 ], [ 116.89590454101562, 39.3831409542565 ], [ 116.86981201171876, 39.918162846609455 ], [ 116.19827270507814, 39.78321267821705 ] ] ] } } ], "crs":{ "type":"name", "properties":{ "name":"EPSG:4326" } } }
一、Maven中GeoTools的引入
引入的geojson模塊
<dependency> <groupId>org.geotools</groupId> <artifactId>gt-geojson</artifactId> <version>19.0</version> </dependency>
然后添加GeoTools官方的倉庫
如果在Maven中引入GeoTools時找不到jar包,則參考:https://www.cnblogs.com/SkyTreeDelivery/p/12667972.html<repository> <id>osgeo</id> <name>OSGeo Release Repository</name> <url>https://repo.osgeo.org/repository/release/</url> <snapshots><enabled>false</enabled></snapshots> <releases><enabled>true</enabled></releases> </repository> <repository> <id>osgeo-snapshot</id> <name>OSGeo Snapshot Repository</name> <url>https://repo.osgeo.org/repository/snapshot/</url> <snapshots><enabled>true</enabled></snapshots> <releases><enabled>false</enabled></releases> </repository>
二、解析FeatureCollection對象文件
一個FeatureCollection對象文本,包含一個Feature要素。
1.1 geotools操作GeoJSON過程中的問題及相關源碼
public static void main(String[] a) throws Exception { // 坐標順序是EAST_NORTH,即經度在前 String json = "{\"type\":\"FeatureCollection\",\"features\":[{\"type\":\"Feature\",\"properties\":{\"area\":3865207830, \"text\": null},\"id\":\"polygon.1\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[[116.19827270507814,39.78321267821705],[116.04446411132814,39.232253141714914],[116.89590454101562,39.3831409542565],[116.86981201171876,39.918162846609455],[116.19827270507814,39.78321267821705]]]}}],\"crs\":{\"type\":\"name\",\"properties\":{\"name\":\"EPSG:4326\"}}}"; // 指定GeometryJSON構造器,15位小數 FeatureJSON fjson_15 = new FeatureJSON(new GeometryJSON(15)); // 讀取為FeatureCollection FeatureCollection featureCollection = fjson_15.readFeatureCollection(json); // 獲取SimpleFeatureType SimpleFeatureType simpleFeatureType = (SimpleFeatureType) featureCollection.getSchema(); // 第1個問題。坐標順序與實際坐標順序不符合 System.out.println(CRS.getAxisOrder(simpleFeatureType.getCoordinateReferenceSystem())); //輸出:NORTH_EAST //第2個問題。查看空間列名稱 System.out.println(simpleFeatureType.getGeometryDescriptor().getLocalName()); //輸出:geometry //第3個問題。坐標精度丟失 //第4個問題。默認無坐標系和空值輸出 OutputStream ostream = new ByteArrayOutputStream(); GeoJSON.write(featureCollection, ostream); // 輸出:{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Polygon","coordinates":[[[116.1983,39.7832],[116.0445,39.2323],[116.8959,39.3831],[116.8698,39.9182],[116.1983,39.7832]]]},"properties":{"area":3865207830},"id":"polygon.1"}]} System.out.println(ostream); // 第5個問題。坐標變換問題,由坐標順序引發 SimpleFeatureIterator iterator = (SimpleFeatureIterator) featureCollection.features(); SimpleFeature simpleFeature = iterator.next(); Geometry geom = (Geometry) simpleFeature.getDefaultGeometry(); iterator.close(); System.out.println(geom.getArea()); // 輸出:0.4043554020447081 MathTransform transform_1 = CRS.findMathTransform(CRS.decode("EPSG:4326"), CRS.decode("EPSG:3857"),true); // 下面一行代碼會報異常:Exception in thread "main" org.geotools.referencing.operation.projection.ProjectionException: Latitude 116°11.8'N is too close to a pole. /*Geometry geom_3857 = JTS.transform(geom, transform_1); System.out.println(geom_3857.getArea());*/ }
上述事例代碼給出了將GeoJSON解析成FeatureCollection時出現的一些問題。 第1個問題是得到的FeatureCollection坐標順序是錯誤的,給出的GeoJSON坐標順序是經度(EAST)在前,geotools讀取時給出了默認的坐標順序(緯度在前),看下面的org.geotools.geojson.feature.FeatureJSON的readFeatureCollection(Object input)方法源碼:
// org.geotools.geojson.feature.FeatureJSON // input可以是File,Reader,InputStream等 public FeatureCollection readFeatureCollection(Object input) throws IOException { // 新建一個DefaultFeatureCollection對象, DefaultFeatureCollection features = new DefaultFeatureCollection(null, null); // FeatureCollectionIterator實現了FeatureIterator接口,是一個內部類,用於控制從geojson文本中讀取要素和坐標系等信息。 FeatureCollectionIterator it = (FeatureCollectionIterator) streamFeatureCollection(input); while(it.hasNext()) { features.add(it.next()); } if (features.getSchema() != null && features.getSchema().getCoordinateReferenceSystem() == null && it.getHandler().getCRS() != null ) { try { // 只將坐標系信息寫入,即只更改了坐標系 return new ForceCoordinateSystemFeatureResults(features, it.getHandler().getCRS()); } catch (SchemaException e) { throw (IOException) new IOException().initCause(e); } } return features; }
//org.geotools.geojson.feature.CRSHandler public boolean primitive(Object value) throws ParseException, IOException { if (state == 2) { try { try { crs = CRS.decode(value.toString()); //坐標順序默認NORTH_EAST,與實際數據不符 } catch(NoSuchAuthorityCodeException e) { //try pending on EPSG try { crs = CRS.decode("EPSG:" + value.toString()); } catch(Exception e1) { //throw the original throw e; } } } catch(Exception e) { throw (IOException) new IOException("Error parsing " + value + " as crs id").initCause(e); } state = -1; } return true; }
String srs = CRS.lookupIdentifier(simpleFeatureType.getCoordinateReferenceSystem(),true); // 獲取EPSG featureCollection = new ForceCoordinateSystemFeatureResults(featureCollection, CRS.decode(srs, true));
//org.geotools.geojson.feature.FeatureHandler void addGeometryType(SimpleFeatureTypeBuilder typeBuilder, Geometry geometry) { // 空間列名"geometry",而不是"the_geom" typeBuilder.add("geometry", geometry != null ? geometry.getClass() : Geometry.class); typeBuilder.setDefaultGeometry("geometry"); }
// 構建新的SimpleFeatureType public static SimpleFeatureType retype(SimpleFeatureType oldType){ SimpleFeatureTypeBuilder typeBuilder = new SimpleFeatureTypeBuilder(); typeBuilder.init(oldType); // the_geom if("geometry".equals(oldType.getGeometryDescriptor().getLocalName())){ typeBuilder.remove("geometry"); typeBuilder.add("the_geom",oldType.getType("geometry").getBinding()); } //生成新的SimpleFeatureType return typeBuilder.buildFeatureType(); } // 新建一個方法,用於變換feature的type public static SimpleFeature retypeFeature(SimpleFeature feature,SimpleFeatureType newType) { SimpleFeatureBuilder builder = new SimpleFeatureBuilder(newType); // 遍歷屬性 for (AttributeDescriptor att : newType.getAttributeDescriptors()) { Object value = feature.getAttribute(att.getName()); // 空間列 if(Geometry.class.isAssignableFrom(att.getType().getBinding())){ builder.set("the_geom", feature.getDefaultGeometry()); continue; } builder.set(att.getName(), value); } return builder.buildFeature(feature.getID()); }
SimpleFeatureType newType = retype(simpleFeatureType); // ListFeatureCollection是FeatureCollection的一個子類 ListFeatureCollection listFeatureCollection = new ListFeatureCollection(newType); SimpleFeatureIterator iterator_3 = (SimpleFeatureIterator) featureCollection.features(); while (iterator_3.hasNext()){ SimpleFeature newFeature = retypeFeature(iterator_3.next(),newType); listFeatureCollection.add(newFeature); } iterator_3.close();
第3(坐標精度丟失)、第4(默認無坐標系和空值輸出)、第5(由坐標順序引發坐標變換)這三個問題。我用GeoJSON的static void write(Object obj, Object output)靜態方法將FeatureCollection轉化成了json文本輸出,先看org.geotools.geojson.GeoJSON源碼:
// 該類用於FeatureCollection、Feature和坐標系的JSON輸出 public class GeoJSON { static GeometryJSON gjson = new GeometryJSON(); static FeatureJSON fjson = new FeatureJSON(); // 用的默認構造器 public static Object read(Object input) throws IOException { throw new UnsupportedOperationException(); } public static void write(Object obj, Object output) throws IOException { if (obj instanceof Geometry) { gjson.write((Geometry)obj, output); } else if (obj instanceof Feature || obj instanceof FeatureCollection || obj instanceof CoordinateReferenceSystem) { if (obj instanceof SimpleFeature) { fjson.writeFeature((SimpleFeature)obj, output); } else if (obj instanceof FeatureCollection) { fjson.writeFeatureCollection((FeatureCollection)obj, output); } else if (obj instanceof CoordinateReferenceSystem) { fjson.writeCRS((CoordinateReferenceSystem)obj, output); } else { throw new IllegalArgumentException("Unable able to encode object of type " + obj.getClass()); } } } }
// org.geotools.geojson.feature.FeatureJSON GeometryJSON gjson; // 決定坐標保留的位數 SimpleFeatureType featureType; AttributeIO attio; boolean encodeFeatureBounds = false; // true表示json文本中Feature輸出bbox boolean encodeFeatureCollectionBounds = false; // true表示json文本中FeatureCollection輸出bbox boolean encodeFeatureCRS = false; // true表示json文本中Feature輸出坐標系 boolean encodeFeatureCollectionCRS = false; // true表示json文本中FeatureCollection輸出坐標系 boolean encodeNullValues = false; // true表示識別值為null的屬性 public FeatureJSON() { this(new GeometryJSON()); // GeometryJSON默認保留4為小數 } public FeatureJSON(GeometryJSON gjson) { // 自定義GeometryJSON,可控制小數位數 this.gjson = gjson; attio = new DefaultAttributeIO(); }
// 重寫后的GeoJSON public class GeoJSON { static GeometryJSON gjson = new GeometryJSON(15); // 15位小數 static FeatureJSON fjson = new FeatureJSON(gjson); // 指定GeometryJSON public static Object read(Object input) throws IOException { throw new UnsupportedOperationException(); } public static void write(Object obj, Object output) throws IOException { if (obj instanceof Geometry) { gjson.write((Geometry)obj, output); } else if (obj instanceof Feature || obj instanceof FeatureCollection || obj instanceof CoordinateReferenceSystem) { // 值為null的屬性也識別 fjson.setEncodeNullValues(true); // 輸出坐標系文本 fjson.setEncodeFeatureCollectionCRS(true); if (obj instanceof SimpleFeature) { fjson.writeFeature((SimpleFeature)obj, output); } else if (obj instanceof FeatureCollection) { fjson.writeFeatureCollection((FeatureCollection)obj, output); } else if (obj instanceof CoordinateReferenceSystem) { fjson.writeCRS((CoordinateReferenceSystem)obj, output); } else { throw new IllegalArgumentException("Unable able to encode object of type " + obj.getClass()); } } } }
// fjson_15已經保留15位 fjson_15.setEncodeFeatureCollectionCRS(true); fjson_15.setEncodeNullValues(true); fjson_15.writeFeatureCollection(featureCollection,System.out); // 控制台輸出和原始geojson一致
針對第5(由坐標順序引發坐標變換)個問題,“org.geotools.referencing.operation.projection.ProjectionException: Latitude 116°11.8’N is too close to a pole”異常其實是由坐標順序不正確導致,經緯度順序調換后識別的坐標超出了范圍,不是當前坐標系能表示的值了。這是一個隱藏問題,在處理另一些原數據或變換不同的坐標系時,不一定會產生這個異常,那用不合理的坐標順序得到的結果是不正確的。調整代碼如下即可以解決:
String srs = CRS.lookupIdentifier(simpleFeatureType.getCoordinateReferenceSystem(),true); // CRS.decode()方法可以設置經緯度順序 MathTransform transform_2 = CRS.findMathTransform(CRS.decode(srs,true), CRS.decode("EPSG:3857",true),true); Geometry geom_3857 = JTS.transform(geom, transform_2); System.out.println(geom_3857.getArea()); // 輸出:6.501222710260582E9
測試代碼里輸出了幾何對象geom的面積,但這個面積很粗糙,只做測試用。給出的GeoJSON文本中的"area"值也只做參考,不是最精確的面積。大家都知道,EPSG:3857以EPSG:4326地理坐標系和投影方式為偽墨卡托的平面坐標系,給出的面積偏差較大。
1.2 方法二:讀取本地txt文件進行解析
FeatJson.class
import com.geomesa.spark.SparkJTS.Operation; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.operation.distance.DistanceOp; import org.geotools.data.simple.SimpleFeatureIterator; import org.geotools.feature.FeatureCollection; import org.geotools.geojson.GeoJSON; import org.geotools.geojson.feature.FeatureJSON; import org.geotools.geojson.geom.GeometryJSON; import org.geotools.geometry.jts.JTSFactoryFinder; import org.opengis.feature.simple.SimpleFeature; import org.opengis.feature.simple.SimpleFeatureType; import java.io.*; public class FeatJson { static GeometryFactory geometryFactory = JTSFactoryFinder.getGeometryFactory(null); public static void main(String[] args) throws IOException { //讀取本地文件 FileReader reader = new FileReader("D:/GitProjects/GeoMesa/GeoMesaSpark/src/main/resources/gsmc.txt"); BufferedReader bufferReader = new BufferedReader(reader); String dict = bufferReader.readLine(); //按行讀取文件 //構造FeatureJSON對象,GeometryJSON保留15位小數 FeatureJSON featureJSON = new FeatureJSON(new GeometryJSON(15)); FeatureCollection featureCollection = featureJSON.readFeatureCollection(dict); SimpleFeatureType simpleFeatureType = (SimpleFeatureType) featureCollection.getSchema(); System.out.println(simpleFeatureType.getGeometryDescriptor().getLocalName()); OutputStream ostream = new ByteArrayOutputStream(); GeoJSON.write(featureCollection, ostream); System.out.println(ostream); SimpleFeatureIterator iterator = (SimpleFeatureIterator) featureCollection.features(); SimpleFeature simpleFeature = iterator.next(); Geometry geom = (Geometry) simpleFeature.getDefaultGeometry(); iterator.close(); System.out.println(geom.getLength()); System.out.println(geom.getCoordinate()); System.out.println(geom.getBoundary()); System.out.println(geom.getGeometryType()); //新建一個經緯度坐標對象 Coordinate coordinate1 = new Coordinate(1.357846020181606E7, 4505819.87283728); Coordinate[] coordinates2 = geom.getCoordinates(); Operation op = new Operation(); //求點到線的距離 System.out.println("距離:"+op.distanceGeo(geometryFactory.createPoint(coordinate1),geometryFactory.createLineString(coordinates2))); //求點到線的最近一個點 System.out.println(DistanceOp.nearestPoints(geometryFactory.createPoint(coordinate1),geometryFactory.createLineString(coordinates2))); bufferReader.close(); reader.close(); } }
三、Java Code Examples
Java Code Examples for org.geotools.data.simple.simplefeaturecollection.features()
一些Java的示例代碼:https://www.programcreek.com/java-api-examples/?class=org.geotools.data.simple.simplefeaturecollection&method=features
四、API
FeatureJSON:http://docs.geotools.org/stable/javadocs/org/geotools/geojson/feature/FeatureJSON.html