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"
}
}
}
一、解析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;
}
ForceCoordinateSystemFeatureResults是FeatureCollection接口的一個子類,直接更改座標系信息,原數據中的座標信息不變。座標系的生成是在org.geotools.geojson.feature.CRSHandler類中,相關代碼如下:
//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;
}
第1個問題可做如下修改,使座標系正常:
String srs = CRS.lookupIdentifier(simpleFeatureType.getCoordinateReferenceSystem(),true); // 獲取EPSG
featureCollection = new ForceCoordinateSystemFeatureResults(featureCollection, CRS.decode(srs, true));
第2個問題,空間列名稱不是我們想要的"the_geom",而是"geometry",這使和另外一些數據源的FeatureCollection一起做操作時空間列不一致。
相關代碼在org.geotools.geojson.feature.FeatureHandler類中:
//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");
}
SimpleFeatureTypeBuilder類用於構建SimpleFeatureType,SimpleFeatureType描述了FeatureCollection對象屬性、數據類型、座標系等信息。
第2個問題可做如下修改,使空間列變爲"the_geom":
// 構建新的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());
}
在測試代碼中加入如下,得到最終的FeatureCollection:
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());
}
}
}
}
該類除寫Geometry外都是調用FeatureJSON的方法,在看下FeatureJSON的構造器和實例變量:
// 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();
}
GeometryJSON的相關代碼就不列出來了。
解決第3(座標精度丟失)、第4(默認無座標系和空值輸出)問題我們只需做一些設置。
如果想統一用GeoJSON.write()方法寫json文本,可以重寫該類,設置精度,代碼如下:
// 重寫後的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());
}
}
}
}
也可以不進行重寫,目前源碼已經支持在初始化的時候直接傳參即可,舉例如下:
GeometryJSON gjson = new GeometryJSON(15); // 初始化 精度爲15位小數
FeatureJSON fjson = new FeatureJSON(gjson);
如果就想用FeatureJSON操作輸出,可以在測試代碼中添加如下代碼解決:
// 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地理座標系和投影方式爲僞墨卡託的平面座標系,給出的面積偏差較大。
讀取本地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();
}
}
推薦api: FeatureJSON