1、背景
本周突然想到本科实习的时候遇到的一个需求:要求在Web端对Shp数据进行编辑同时实现数据的持久化保存。当初并没有实现这个需求,现在来解决这个需求。
2、前期准备
2.1 数据库准备
数据库选择的是Postgresql
数据库,同时安装了Postgresql
的空间数据插件PostGIS
,为了实现这个需求表结构不设计的过于复杂。表结构如下:
sql
# sequence
CREATE SEQUENCE public.seq_geojson
INCREMENT BY 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1
NO CYCLE;
-- Permissions
ALTER SEQUENCE public.seq_geojson OWNER TO postgres;
GRANT ALL ON SEQUENCE public.seq_geojson TO postgres;
# 表结构
CREATE TABLE public.t_geojson (
id int4 NULL DEFAULT nextval('seq_geojson'::regclass),
"type" varchar NULL, -- 类型
properties varchar NULL, -- 属性
geometry public.geometry NULL, -- 几何属性
tag varchar NULL -- 标志
);
COMMENT ON TABLE public.t_geojson IS 'geojson数据表';
-- Column comments
COMMENT ON COLUMN public.t_geojson."type" IS '类型';
COMMENT ON COLUMN public.t_geojson.properties IS '属性';
COMMENT ON COLUMN public.t_geojson.geometry IS '几何属性';
COMMENT ON COLUMN public.t_geojson.tag IS '标志';
-- Permissions
ALTER TABLE public.t_geojson OWNER TO postgres;
GRANT ALL ON TABLE public.t_geojson TO postgres;
向t_geojson表中插入几条测试数据:
sql
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(1, 'Feature', '{"name": "NanJing"}', 'SRID=4326;POINT (118.816774 32.030948)'::public.geometry, 'NanJing');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(2, 'Feature', '{"name": "GuangZhou111"}', 'SRID=4326;POINT (113.26884226045374 23.13098556126629)'::public.geometry, 'GuangZhou111');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(3, 'Feature', '{"name": "WuHu"}', 'SRID=4326;POINT (118.4238775891948 31.33825273874809)'::public.geometry, 'WuHu');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(4, 'Feature', '{"name": "ShangHai"}', 'SRID=4326;POINT (121.47379740064838 31.230593115867556)'::public.geometry, 'ShangHai');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(5, 'Feature', '{"name": "hangzhou"}', 'SRID=4326;POINT (120.21472717385683 30.238835185805506)'::public.geometry, 'hangzhou');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(6, 'Feature', '{"name": "WuHan"}', 'SRID=4326;POINT (114.295643 30.595123)'::public.geometry, 'WuHan');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(7, 'Feature', '{"name": "HeFei"}', 'SRID=4326;POINT (117.22856522330525 31.816157928366565)'::public.geometry, 'HeFei');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(8, 'Feature', '{"name": "test"}', 'SRID=4326;LINESTRING (111.28377737193817 34.37862157174361, 106.43221379905457 33.42940322407496, 106.34432317405457 25.78291884907496)'::public.geometry, 'test');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(9, 'Feature', '{"name": "test"}', 'SRID=4326;POLYGON ((100.36776134460682 28.718465187633157, 101.174337896 31.343555398, 102.766815391 29.984736477, 100.36776134460682 28.718465187633157))'::public.geometry, 'ChengDu');
INSERT INTO public.t_geojson (id, "type", properties, geometry, tag) VALUES(10, 'Feature', '{"name": "test"}', 'SRID=4326;MULTIPOLYGON (((105.27612173916928 31.827695798757404, 105.80786028864019 29.27447320956263, 104.40600475283496 28.993223142507404, 102.6262194289469 29.037168455007404, 103.10961803408496 29.850156736257404, 102.96899283291928 31.59918014022979, 105.27612173916928 31.827695798757404)))'::public.geometry, 'test');
插入的数据包括POINT
、LINESTRING
、POLYGON
和MULTIPOLYGON
四种空间数据类型。
2.2 后端项目准备
在后端使用Springboot
和mybatis-plus
简单搭建了后端工程。同时引入了jts
和geotools
对数据格式进行转换。pom
依赖如下:
xml
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-geojson</artifactId>
<version>29.2</version>
</dependency>
<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.18.1</version>
</dependency>
定义表结构GeoJSON
实体类、VO
类、GeoJsonFormat
模板类和GeoJsonCovert
类型转换类:
java
/**
* @author : potro_hugh
* @date : 2023/10/18 11:07
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@TableName(value = "t_geojson", autoResultMap = true)
public class GeoJSON {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "type")
private String type;
@TableField(value = "properties")
private String properties;
@TableField(value = "tag")
private String tag;
@TableField(value = "geometry", typeHandler = GeometryTypeHandler.class)
private String geometry;
}
java
/**
* @author : potro_hugh
* @date : 2023/10/18 11:15
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GeoJSONVO {
private Integer id;
private String type;
private String properties;
private String tag;
private String geometry;
}
java
/**
* @author : potro_hugh
* @date : 2023/10/18 14:28
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GeoJsonFormat {
private String type = "FeatureCollection";
private List<GeoJsonCovert> features;
}
java
/**
* @author : potro_hugh
* @date : 2023/10/18 17:35
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class GeoJsonCovert {
private Integer id;
private String type;
private JSONObject properties;
private String tag;
private JSONObject geometry;
}
为了实现数据格式的转换和数据类型转换,参考前人的代码编写DataConvert
和GeometryTypeHandler
两个类。
java
import org.geotools.geojson.geom.GeometryJSON;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.ParseException;
import org.locationtech.jts.io.WKTReader;
import org.locationtech.jts.io.WKTWriter;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
/**
* @author : potro_hugh
* @date : 2023/10/19 10:51
*/
public class DataConvert {
/**
* GeoJSON转Geometry
* @param geoJson GeoJSON
* @return Geometry
* @throws IOException 转换异常
*/
public Geometry geojson2Geometry(String geoJson) throws IOException {
GeometryJSON geometryJson = new GeometryJSON(7);
return geometryJson.read(new StringReader(geoJson));
}
/**
* Geometry转GeoJSON
* @param geometry Geometry
* @return GeoJSON
* @throws IOException 转换异常
*/
public String geometry2GeoJson(Geometry geometry) throws IOException {
GeometryJSON geometryJson = new GeometryJSON(7);
StringWriter writer = new StringWriter();
geometryJson.write(geometry, writer);
return writer.toString();
}
/**
* WKT转Geometry
* @param wkt wkt
* @return Geometry
* @throws ParseException 解析异常
*/
public Geometry wkt2Geometry(String wkt) throws ParseException {
WKTReader reader = new WKTReader();
return reader.read(wkt);
}
/**
* Geometry转WKT
* @param geometry Geometry
* @return wkt
* @throws ParseException 解析异常
*/
public String geometry2Wkt(Geometry geometry) throws ParseException {
WKTWriter writer = new WKTWriter();
return writer.write(geometry);
}
}
java
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.postgis.PGgeometry;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author : potro_hugh
* @date : 2023/10/18 16:42
*/
@MappedTypes({String.class})
public class GeometryTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
PGgeometry pGgeometry = new PGgeometry(parameter);
ps.setObject(i, pGgeometry);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
PGgeometry pGgeometry = new PGgeometry(rs.getString(columnName));
return pGgeometry.toString();
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
PGgeometry pGgeometry = new PGgeometry(rs.getString(columnIndex));
return pGgeometry.toString();
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
PGgeometry pGgeometry = new PGgeometry(cs.getString(columnIndex));
if (pGgeometry == null) {
return null;
}
return pGgeometry.toString();
}
}
编写数据查询getGeoJSONData
和数据更新insertData
两个接口。getGeoJSONData
接口查询数据库将数据转换为指定的格式:
java
// interface代码
/**
* @author : potro_hugh
* @date : 2023/10/18 11:13
*/
public interface GeoJSONService {
/**
* @return 返回GeoJSON格式数据
*/
GeoJsonFormat getGeoJSONData();
/**
* @param geoJSONVO data
* 新增数据
*/
void insertData(GeoJSONVO geoJSONVO);
/**
* @param geoJsonCovert data
* 更新数据
*/
void updateData(GeoJsonCovert geoJsonCovert);
}
java
/**
* @author : potro_hugh
* @date : 2023/10/18 11:14
*/
@Slf4j
@Service
public class GeoJSONServiceImpl implements GeoJSONService {
private final GeoJSONMapper geoJSONMapper;
public GeoJSONServiceImpl(GeoJSONMapper geoJsonMapper) {
this.geoJSONMapper = geoJsonMapper;
}
@Override
public GeoJsonFormat getGeoJSONData() {
List<GeoJSONVO> geoJSONVOS = geoJSONMapper.getData();
List<GeoJsonCovert> geoJsonCoverts = new ArrayList<>();
for(GeoJSONVO data: geoJSONVOS){
GeoJsonCovert covert = GeoJsonCovert.builder().id(data.getId()).properties(JSON.parseObject(data.getProperties()))
.type(data.getType()).tag(data.getTag()).geometry(JSON.parseObject(data.getGeometry())).build();
geoJsonCoverts.add(covert);
}
GeoJsonFormat geoJsonFormat = new GeoJsonFormat();
geoJsonFormat.setFeatures(geoJsonCoverts);
return geoJsonFormat;
}
@Override
public void insertData(GeoJSONVO geoJSONVO) {
geoJSONMapper.insertData(geoJSONVO);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateData(GeoJsonCovert geoJsonCovert) {
DataConvert dataConvert = new DataConvert();
GeoJSONVO data = new GeoJSONVO();
try{
data.setId(geoJsonCovert.getId());
data.setType(geoJsonCovert.getType());
data.setProperties(geoJsonCovert.getProperties().toString());
data.setTag(geoJsonCovert.getTag());
data.setGeometry(dataConvert.geojson2Geometry(String.valueOf(geoJsonCovert.getGeometry())).toString());
log.info(JSON.toJSONString(data));
}catch (IOException e){
log.debug("Covert Error Info:{}", e.getMessage());
}
geoJSONMapper.updateData(data);
}
}
java
/**
* @author : potro_hugh
* @date : 2023/10/18 11:11
*/
@Mapper
public interface GeoJSONMapper extends BaseMapper<GeoJSON> {
@Select({"select id, type, tag, properties, st_asgeojson(geometry) as geometry from t_geojson order by id asc"})
@ResultType(ArrayList.class)
List<GeoJSONVO> getData();
@Insert({"INSERT INTO t_geojson (type, properties, geometry, tag) VALUES(#{type}, #{properties}, ST_GeomFromText(#{geometry},4326), #{tag})"})
void insertData(GeoJSONVO geoJSONVO);
// role_name = #{roleName} ST_GeomFromGeoJSON
@Update({"UPDATE public.t_geojson SET type= #{type}, geometry= ST_GeomFromText(#{geometry}), tag= #{tag} where id= #{id}"})
void updateData(GeoJSONVO geoJSONVO);
}
2.3 前端项目准备
由于不擅长前端页面和样式的编写,前端按照最简单的html
、css
、JavaScript
三件套搭建,在地图API
上选择Openlayers
,按照ES5的写法进行编写。前后端交互使用ajax
进行数据交互。
javascript
var img = new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://map.geoq.cn/arcgis/rest/services/ChinaOnlineStreetWarm/MapServer/tile/{z}/{y}/{x}'
})
});
var map = new ol.Map({
layers: [img],
target: 'map',
view: new ol.View({
projection: 'EPSG:4326',
center: [117.276905,31.837032],
zoom: 8
})
});
var select = new ol.interaction.Select({
condition: ol.events.condition.singleClick,
});
var modify = new ol.interaction.Modify({
features: select.getFeatures()
});
// 添加交互工具
map.addInteraction(select);
map.addInteraction(modify);
为了实现数据的编辑在页面上数据按照GeoJSON
格式进行加载,使用Openlayers
中的ol.Map
方法提供的addInteraction
实现数据编辑。
为modify
绑定modifyend
事件:
javascript
modify.on('modifyend', function (e) {
var features = e.features;
if (features.getLength() > 0) {
var feature = features.item(0);
// 将绘制图层转为geojson
let featureGeoJson = JSON.parse(new ol.format.GeoJSON().writeFeature(feature));
$.ajax({
type:"POST",
url:"http://localhost:8056/updateData",
contentType: "application/json",
data: JSON.stringify({
"id": feature.getId(),
"type": "Feature",
"properties": featureGeoJson.properties,
"tag": feature.getProperties().name,
"geometry": featureGeoJson.geometry
}),
success:function (data) {
if(data.code == "200"){
alert("修改成功!");
}
console.log(data)
}
})
}
});
完整前端代码:
javascript
var img = new ol.layer.Tile({
source: new ol.source.XYZ({
url: 'https://map.geoq.cn/arcgis/rest/services/ChinaOnlineStreetWarm/MapServer/tile/{z}/{y}/{x}'
})
});
var map = new ol.Map({
layers: [img],
target: 'map',
view: new ol.View({
projection: 'EPSG:4326',
center: [117.276905,31.837032],
zoom: 8
})
});
function addFeature(){
// map.removeLayer(vectorLayer)
$.ajax({
type:"GET",
url:"http://localhost:8056/getGeoJSONData",
contentType: "application/json",
dataType:"json",
success:function (data) {
var json = JSON.stringify(data);
var vectorSource = new ol.source.Vector({
features: (new ol.format.GeoJSON()).readFeatures(data)
});
var vectorLayer = new ol.layer.Vector({
source: vectorSource,
style: new ol.style.Style({
image: new ol.style.Circle({
radius: 8,
fill: new ol.style.Fill({
color: 'blue'
})
}),
stroke: new ol.style.Stroke({
color: 'blue',
width: 5
}),
fill: new ol.style.Fill({
color: 'yellow'
})
})
});
map.addLayer(vectorLayer);
}
})
}
var select = new ol.interaction.Select({
condition: ol.events.condition.singleClick,
});
var modify = new ol.interaction.Modify({
features: select.getFeatures()
});
modify.on('modifyend', function (e) {
var features = e.features;
if (features.getLength() > 0) {
var feature = features.item(0);
// 将绘制图层转为geojson
let featureGeoJson = JSON.parse(new ol.format.GeoJSON().writeFeature(feature));
$.ajax({
type:"POST",
url:"http://localhost:8056/updateData",
contentType: "application/json",
data: JSON.stringify({
"id": feature.getId(),
"type": "Feature",
"properties": featureGeoJson.properties,
"tag": feature.getProperties().name,
"geometry": featureGeoJson.geometry
}),
success:function (data) {
if(data.code == "200"){
alert("修改成功!");
}
console.log(data)
}
})
}
});
// 添加交互工具
map.addInteraction(select);
map.addInteraction(modify);
3、实现效果
原始数据状态:
数据编辑状态:
后端代码不够丝滑只是实现了一个小功能,有什么问题请批评指正。