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

原始数据状态:

数据编辑状态:

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

相关推荐
GIS工具-gistools20211 天前
地图资源下载工具失效下载链接重新分享
gis
GIS数据转换器3 天前
跨界融合,GIS如何赋能游戏商业——以《黑神话:悟空》为例
大数据·数据库·人工智能·游戏·gis·智慧城市
GIS数据转换器4 天前
时空大数据平台:激活新质生产力的智慧引擎
大数据·人工智能·物联网·3d·gis
纸飞机的旅行12 天前
Cesium坐标系
gis·webgis
丷丩14 天前
3. GIS后端工程师岗位职责、技术要求和常见面试题
面试·gis
丷丩15 天前
11. GIS三维建模工程师岗位职责、技术要求和常见面试题
面试·gis·三维建模
GIS工具-gistools202115 天前
Web 地图服务 简介
gis
丷丩17 天前
在Postgresql中计算工单的对应的GPS轨迹距离
数据库·postgresql·gis·gps轨迹
mahuifa20 天前
C++(Qt)-GIS开发-QGraphicsView显示在线瓦片地图
c++·qt·gis·瓦片地图
mahuifa20 天前
C++(Qt)-GIS开发-QGraphicsView显示瓦片地图简单示例2
c++·qt·gis·瓦片地图·bing地图