为了方便查看 shp(shapefile) 文件,不用每次都打开 Arcgis 或者 QGIS 软件,搞了一个在线解析 shp,并且转为 Geojson 格式数据的小工具。
使用了一段时间,都还比较稳定,不过在一次上传 CGCS2000_3_Degree_GK_Zone_39 投影数据的时候,后台解析错误,经过长时间调试(资料比较少,多数都是介绍坐标转换的),终于解决问题,记录一下。
本文主要介绍:Geotools Error looking up crs identifier 错误原因、geotools定义坐标系、解决问题方法。
首先,错误提示为 Geotools Error looking up crs identifier ,提示很明确:即找不到 crs 定义,也就是不认识数据的投影坐标系。
看到错误提示,笔者认为设置坐标系就可以解决问题,于是搜索 Geotools 定义坐标系的方法:最后找到 CRS.decode(“EPSG:4326”);
在 Geotools 中是通过 EPSG 的码来创建投影坐标系对象的,因此需要先获取 EPSG 码。
经过 Coordinate Systems Worldwide 查询可以获得 CGCS2000_3_Degree_GK_Zone_39 的 EPSG 码为 EPSG: 4527 。
当时初步的想法是,定义好投影坐标系之后,将数据转为 Geotools 支持的 EPSG:4326,应该就可以了。
贴上坐标转换工具代码:
public static SimpleFeatureCollection transfer(
SimpleFeatureCollection featureCollection,
CoordinateReferenceSystem source,
CoordinateReferenceSystem target) throws FactoryException, TransformException, IOException {
boolean lenient = true; // allow for some error due to different datums
//定义坐标转换
MathTransform transform = CRS.findMathTransform(source, target, lenient);
SimpleFeatureIterator features = featureCollection.features();
DefaultFeatureCollection simpleFeatures = new DefaultFeatureCollection();
try {
while (features.hasNext()) {
SimpleFeature feature = features.next();
//坐标系转换
Geometry geometry = (Geometry) feature.getDefaultGeometry();
Geometry geometry2 = JTS.transform(geometry, transform);
feature.setDefaultGeometry(geometry2);
simpleFeatures.add(feature);
}
} catch (NoSuchElementException e) {
e.printStackTrace();
} catch (MismatchedDimensionException e) {
e.printStackTrace();
} catch (TransformException e) {
e.printStackTrace();
} finally {
features.close();
}
return simpleFeatures.collection();
}
尝试之后并没有解决,依然报错,错误信息也还是一样。
经过 debugger 调试 Geotools 源码,发现是 CRS.lookupIdentifier() 方法抛出的异常,找到相关代码块,排查问题。最后确认是此方法的 throw 异常,如果不支持投影坐标系,就是抛出异常,导致直接跳出,终止执行。
贴上源码:
/**
* Create a properties map for the provided crs.
*
* @param crs CoordinateReferenceSystem or null for default
* @return properties map naming crs identifier
* @throws IOException
*/
Map<String, Object> createCRS(CoordinateReferenceSystem crs) throws IOException {
Map<String, Object> obj = new LinkedHashMap<String, Object>();
obj.put("type", "name");
Map<String, Object> props = new LinkedHashMap<String, Object>();
if (crs == null) {
props.put("name", "EPSG:4326");
} else {
try {
String identifier = CRS.lookupIdentifier(crs, true);
props.put("name", identifier);
} catch (FactoryException e) {
throw (IOException) new IOException("Error looking up crs identifier").initCause(e);
}
}
obj.put("properties", props);
return obj;
}
按理说,Geotools 不认识的投影坐标,直接跳出,也没什么问题,属于方法的正统操作。
后来,在调试过程发现,EPSG: 4527 的数据已经转换为 EPSG: 4326,手动叠加到地图上,位置也是正确的。
这里基本就确定了,Geotools 是可以识别 EPSG: 4527 投影坐标系的,而且数据转换也是成功的,只是在转换过程中 CRS.lookupIdentifier() 出了问题,导致抛出异常,使方法不生效。
初步认为是 Geotools 源码的问题,由于没有克隆 Geotools 源码,就不在源码层面解决这个问题了。
笔者的处理方式是,转换坐标之后,手动拼接 Geojson数据(主要是 FeatureCollection)。其中,坐标转换过程中,源数据的投影坐标系,读取数据会自动识别,统一转为 EPSG: 4326。
Geojson 数据中返回的 crs,不能直接通过数据获取,可以通过 CRS.lookupIdentifier() 方法获取,但是源码的方法遇到未知投影坐标会出现问题,本文通过重写 CRS.lookupIdentifier() 方法来解决问题,获取 crs 之后,存入 Geojson 对象中即可。
核心代码:
1. shp 获取 Feature,转为 json 字符串数组
//获取图形数组
SimpleFeatureCollection result = featureSource.getFeatures();
// 源数据投影坐标系
CoordinateReferenceSystem crs = shpDataStore.getSchema().getCoordinateReferenceSystem();
// EPSG:4326
CoordinateReferenceSystem worldCRS = DefaultGeographicCRS.WGS84;
//定义坐标转换
MathTransform transform = CRS.findMathTransform(crs, worldCRS, true);
// 获取图形要素
SimpleFeatureIterator features = result.features();
// 定义图形要素容器,存放转换后图形要素数据
DefaultFeatureCollection simpleFeatures = new DefaultFeatureCollection();
// 存放 geojson 数据
JSONArray jsonArray = new JSONArray();
try {
while (features.hasNext()) {
SimpleFeature feature = features.next();
// 源几何数据
Geometry geometry = (Geometry) feature.getDefaultGeometry();
// 转换后几何数据
Geometry geometry2 = JTS.transform(geometry, transform);
// 图形要素设置几何数据
feature.setDefaultGeometry(geometry2);
//将feature转为geojson
StringWriter writer = new StringWriter();
// 创建 FeatureJSON 对象,注意参数:精度 8 位小数
FeatureJSON json = new FeatureJSON(new GeometryJSON(8));
json.writeFeature(feature, writer);
// 保存 geojson 数组
jsonArray.add(JSON.parse(writer.toString()));
simpleFeatures.add(feature);
}
} catch (NoSuchElementException e) {
e.printStackTrace();
} catch (MismatchedDimensionException e) {
e.printStackTrace();
} catch (TransformException e) {
e.printStackTrace();
} finally {
features.close();
}
2. 拼接最终 Geojson 数据
// 图形要素数组
// json 字符数组
public Map writeFeatureCollection(FeatureCollection features, JSONArray featuresJson) throws IOException {
LinkedHashMap<String, Object> obj = new LinkedHashMap();
// 设置 geojson 类型
obj.put("type", "FeatureCollection");
if (features.getSchema().getGeometryDescriptor() != null) {
ReferencedEnvelope bounds = features.getBounds();
// 获取数据投影坐标系
CoordinateReferenceSystem crs = bounds != null ? bounds.getCoordinateReferenceSystem() : null;
if (bounds != null) {
// 设置数据四至范围
obj.put("bbox", Arrays.asList(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY()));
}
if (crs != null) {
// 设置 geojson 数据投影坐标系(方法在文章后边)
obj.put("crs", createCRS(crs));
}
}
// 设置图形要素 json 字符串数组
obj.put("features", featuresJson);
return obj;
}
创建 CRS 投影坐标系对象, 注意:如果是 Geotools 不认识的投影坐标系,CRS.lookupIdentifier() 的第一个参数必须设置为 (Citations.EPSG) !!! 不设置的话,方法会抛出异常,导致定义坐标系失败。
当然也可以选择不抛出,直接不做任何操作,但是建议还是沿用 Geotools 的做法,设置第一个参数: Citations.EPSG。
对于 Geotools 认识的投影坐标系,可以直接:
CoordinateReferenceSystem crs = CRS.decode("EPSG:4326");
对于其他不认识的标准投影坐标系,可以:
/**
* Create a properties map for the provided crs.
*
* @param crs CoordinateReferenceSystem or null for default
* @return properties map naming crs identifier
*/
Map<String, Object> createCRS(CoordinateReferenceSystem crs) throws IOException {
Map<String, Object> obj = new LinkedHashMap<>();
obj.put("type", "name");
Map<String, Object> props = new LinkedHashMap<>();
if (crs == null) {
props.put("name", "EPSG:4326");
} else {
try {
// 获取 EPSG 码
String code = crs.getCoordinateSystem().getName().getCode();
// 如果不设置第一个参数,会抛出异常
// String identifier = CRS.lookupIdentifier(crs, true);
// 注意,这里的方法需要设置第一个参数,设置为标准投影坐标系
String identifier = CRS.lookupIdentifier(Citations.EPSG, crs, true);
props.put("name", StringUtils.isNotEmpty(identifier) ? identifier : EPSGChina.defaultEPSG8.get(code));
} catch (FactoryException e) {
throw (IOException) new IOException("Error looking up crs identifier").initCause(e);
}
}
obj.put("properties", props);
return obj;
}
参考博客:
GeoTools入门(五)-- CRS操作
org.geotools.referencing.CRS.lookupIdentifier()方法的使用及代码示例
geotools 官网 CRS