解析FeatureCollection(Geotools对geojson操作出现的问题)

微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路

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

在地图上画出各省市边界,北京市密云县json内容如下: {"type": "FeatureCollection", "features": [{"type": "Feature","properties":{"id":"110228","name":"密云县","cp":[117.0923,40.5121],"childNum":1},"geometry":{"type":"Polygon","coordinates":[[[116.7586,40.7064],[116.7847,40.7016],[116.7888,40.714],[116.7902,40.727],[116.7819,40.7524],[116.7833,40.7579],[116.7902,40.7517],[116.8025,40.7462],[116.819,40.7504],[116.8272,40.749],[116.8396,40.7607],[116.8341,40.7703],[116.8506,40.7751],[116.8506,40.7785],[116.8671,40.7847],[116.8629,40.7922],[116.8712,40.7943],[116.8739,40.7991],[116.8808,40.7977],[116.8863,40.8012],[116.8959,40.7936],[116.8945,40.7819],[116.8973,40.7778],[116.9234,40.7737],[116.9247,40.7627],[116.9289,40.7565],[116.9234,40.7524],[116.9261,40.7449],[116.9412,40.7401],[116.9453,40.7284],[116.9563,40.7229],[116.9659,40.714],[116.9646,40.7092],[117.0044,40.6968],[117.0195,40.6954],[117.0305,40.692],[117.0497,40.7002],[117.084,40.7023],[117.0964,40.7057],[117.1115,40.7071],[117.117,40.6995],[117.1225,40.7009],[117.1527,40.6968],[117.1664,40.6989],[117.1788,40.6934],[117.1829,40.6975],[117.2063,40.6947],[117.231,40.6844],[117.242,40.6769],[117.2612,40.681],[117.2777,40.6666],[117.2914,40.6597],[117.3257,40.6604],[117.3367,40.6631],[117.3367,40.6666],[117.3463,40.6741],[117.3615,40.6748],[117.3903,40.6837],[117.4068,40.6865],[117.4205,40.6865],[117.4507,40.6796],[117.4644,40.6734],[117.4796,40.6776],[117.4947,40.6748],[117.5056,40.6666],[117.507,40.6549],[117.5029,40.6528],[117.5015,40.6364],[117.4796,40.6357],[117.4713,40.6474],[117.448,40.6419],[117.4301,40.6405],[117.4219,40.6377],[117.4178,40.6185],[117.4136,40.6062],[117.4205,40.5945],[117.4232,40.5821],[117.4301,40.5787],[117.426,40.5732],[117.4205,40.5691],[117.404,40.5739],[117.4013,40.5698],[117.3944,40.567],[117.3889,40.5615],[117.3766,40.5663],[117.3656,40.5759],[117.3505,40.5801],[117.334,40.5766],[117.3106,40.5766],[117.2955,40.567],[117.2667,40.5581],[117.2502,40.5485],[117.253,40.5416],[117.2502,40.5375],[117.2598,40.5196],
表情包
插入表情
评论将由博主筛选后显示,对所有人可见 | 还能输入1000个字符
相关推荐
©️2020 CSDN 皮肤主题: 点我我会动 设计师:白松林 返回首页