Postgresql PostGIS OpenLayers实现ShapeFile文件Web端编辑

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');

插入的数据包括POINTLINESTRINGPOLYGONMULTIPOLYGON四种空间数据类型。

2.2 后端项目准备

在后端使用Springbootmybatis-plus简单搭建了后端工程。同时引入了jtsgeotools对数据格式进行转换。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;  
}

为了实现数据格式的转换和数据类型转换,参考前人的代码编写DataConvertGeometryTypeHandler两个类。

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 前端项目准备

由于不擅长前端页面和样式的编写,前端按照最简单的htmlcssJavaScript三件套搭建,在地图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、实现效果

原始数据状态:

数据编辑状态:

后端代码不够丝滑只是实现了一个小功能,有什么问题请批评指正。

相关推荐
GISBox2 天前
PostGIS数据通过GISBox发布WFS/WMS全攻略
数据库·postgresql·wms·gis·postgis·矢量·gisbox
NULIWEIMENXIANG2 天前
ArcPy 程序调用 QGIS 进程实现几何拓扑检查
python·arcgis·gis
我才是银古3 天前
为什么要做 GeoPipeAgent
gis·ai平台
夜郎king5 天前
耒阳童车产业园POI实证分析——基于高德地图,还原“百亿园区”真实面貌
大数据·人工智能·gis·空间分析
ct9785 天前
Cesium的时间与时钟系统
gis·webgl·cesium
奔跑的呱呱牛8 天前
GeoJSON 在大数据场景下为什么不够用?替代方案分析
java·大数据·servlet·gis·geojson
奔跑的呱呱牛9 天前
GeoJSON vs TopoJSON:不仅是体积差异,而是数据模型的差异
gis·geojson·topojson
GISBox9 天前
技术干货:3DTiles转OSGB的适用场景及标准操作流程
gis·数据修复·3dtiles·osgb·gisbox·切片转换·反切
qq_2837200511 天前
Cesium实战(三):加载天地图(影像图,注记图)避坑指南
json·gis·cesium
GISBox11 天前
OSGB与3DTiles格式转换技术指南:从原理到实践
gis·cesium·倾斜摄影·3dtiles·osgb·gisbox·切片转换