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、实现效果

原始数据状态:

数据编辑状态:

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

相关推荐
葱明撅腚6 小时前
利用Python挖掘城市数据
python·算法·gis·聚类
ct9782 天前
Cesium高级特效与着色器开发全指南
前端·gis·cesium·着色器
葱明撅腚4 天前
shapely空间数据分析
python·pandas·gis·shapely
极海拾贝5 天前
秒加在线底图!天地图、高德地图、星图地球、吉林一号底图一次配齐,收藏这篇就够了!
arcgis·gis·geoscene
ct9785 天前
Cesium 矩阵系统详解
前端·线性代数·矩阵·gis·webgl
两点王爷5 天前
KML文件格式和支持添加的内容
gis
水静川流6 天前
GIS工具、POI数据、DEM数据、NDVI数据等地学大数据
arcgis·gis·poi·dem·地学大数据
GIS遥遥8 天前
2026年地信测绘遥感(3S)专业升学、就业、考证、竞赛专属日历
gis·gis开发·测绘·地图可视化
酬勤-人间道8 天前
XPlote3DGenie 2.1.1.0:实用 3D 数据处理工具,百度网盘可直接安装
c++·3d·gis·编程·计算机软件·岩土