SpringBoot联合PostGIS实现在线地图行政区划离线存储的实践探索

目录

前言

一、行政区划物理表设计

1、物理表字段设计

2、表结构脚本

二、行政区划在线API

1、请求参数简介

2、响应参数简介

三、Java后台数据获取及空间存储

1、API服务调用

2、递归获取数据

四、成果展示

1、PostGIS数据查询

2、WebGIS可视化

五、总结


前言

在当今数字化时代,地理信息数据的应用场景日益丰富,从城市规划到物流配送,从旅游出行到环境监测,地图数据都扮演着不可或缺的角色。然而,在线地图服务虽然便捷,但在一些特殊场景下,如网络不稳定地区、数据安全要求较高的内部系统等,离线存储地图数据的需求应运而生。行政区划数据作为地图数据的重要组成部分,包含了国家、省、市、区等各级行政区域的边界和属性信息,对于地理信息系统的分析和应用至关重要。因此,如何高效地将在线地图的行政区划数据进行离线存储,并在本地系统中进行管理和查询,成为了一个值得研究的课题。本文将深入探讨如何结合强大的 SpringBoot 框架和专业的地理空间数据库 PostGIS,以高德地图开放平台为例,实现在线地图行政区划数据的离线存储,以满足多样化的业务需求,提升系统的灵活性和可靠性。

通过本次实践探索,我们不仅能够掌握 SpringBoot 和 PostGIS 在地理空间数据处理方面的应用,还能深入了解在线地图数据离线存储的关键技术和实现方法。这将为我们在地理信息系统开发、大数据处理、移动应用开发等领域提供宝贵的经验和参考。此外,该实践成果还可以推广应用于其他类型的地理空间数据离线存储场景,具有广泛的应用前景和实际价值。在接下来的内容中,我们将详细阐述数据获取与预处理的过程、数据库设计与建模的方法、SpringBoot 后端开发的具体实现以及前端展示与交互的设计思路,希望本文的分享能够帮助更多对地理空间数据离线存储感兴趣的开发者和研究人员,共同推动地理信息技术的发展和应用。

一、行政区划物理表设计

本节将对行政区划的物理表设计和表结构脚本进行详细的介绍。物理表很重要,是我们后续内容的开展和继续的基础。

1、物理表字段设计

行政区划信息表的物理模型如下图所示:

为了有效减少物理表的数量,与一些按照不同层级设计不同的表的方法不一样,这里我们增加了一个parent_id字段,以此来维护上下级的关系,达到一张表管理不限层级的数据。

2、表结构脚本

为方便大家学习,这里直接分享我的行政区划物理表数据设计,仅供参考。这里的字段信息大家可以根据自己的需要进行灵活调整。如果有字段信息没有包含进来,可以新增。实例表结构如下,大家可以按需使用:

sql 复制代码
CREATE TABLE "public"."biz_region_info" (
  "pk_id" int8 NOT NULL,
  "parent_id" int8 DEFAULT 0,
  "city_code" varchar(6) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
  "adcode" varchar(6) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
  "name" varchar(50) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
  "polyline_geom" "public"."geometry",
  "center" varchar(30) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
  "level" varchar(10) COLLATE "pg_catalog"."default" DEFAULT ''::character varying,
  "lon" numeric,
  "lat" numeric,
  "center_geom" "public"."geometry",
  CONSTRAINT "pk_biz_region_info" PRIMARY KEY ("pk_id")
);
COMMENT ON COLUMN "public"."biz_region_info"."pk_id" IS '主键';
COMMENT ON COLUMN "public"."biz_region_info"."parent_id" IS '上级主键,最上级默认0';
COMMENT ON COLUMN "public"."biz_region_info"."city_code" IS '城市编码';
COMMENT ON COLUMN "public"."biz_region_info"."adcode" IS '区域编码';
COMMENT ON COLUMN "public"."biz_region_info"."name" IS '行政区名称';
COMMENT ON COLUMN "public"."biz_region_info"."polyline_geom" IS '行政区边界坐标点';
COMMENT ON COLUMN "public"."biz_region_info"."center" IS '区域中心点';
COMMENT ON COLUMN "public"."biz_region_info"."level" IS '行政区划级别';
COMMENT ON COLUMN "public"."biz_region_info"."lon" IS '中心点经度';
COMMENT ON COLUMN "public"."biz_region_info"."lat" IS '中心点纬度';
COMMENT ON COLUMN "public"."biz_region_info"."center_geom" IS '中心点位置';
COMMENT ON TABLE "public"."biz_region_info" IS '行政区划信息表';

需要注意的是,这里没有增加空间索引和其他的物理索引。在实际业务中,我们会经常进行行政区划内的数据检索,因此建议大家在使用时,对行政区边界坐标点字段设计一个空间索引。

二、行政区划在线API

本节将重点介绍高德地图的行政区划检索在线API,主要包括请求参数和响应参数。这是后续我们使用在线地图的基础知识。行政区域查询是一类简单的 HTTP 接口,根据用户输入的搜索条件可以帮助用户快速的查找特定的行政区域信息。例如:中国>山东省>济南市>历下区>舜华路街道(国>省>市>区>街道)。

|--------------------------------------------------------|------|
| URL | 请求方式 |
| https://restapi.amap.com/v3/config/district?parameters | GET |

parameters 代表的参数包括必填参数和可选参数。所有参数均使用和号字符(&)进行分隔。下面的列表枚举了这些参数及其使用规则。

1、请求参数简介

|-------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------|------|
| 参数名 | 含义 | 规则说明 | 是否必须 | 缺省值 |
| key | 请求服务权限标识 | 用户在高德地图官网 申请 Web 服务 API 类型 KEY | 必填 | 无 |
| keywords | 查询关键字 | 规则:只支持单个关键词语搜索关键词支持:行政区名称、citycode、adcode 例如,在 subdistrict=2,搜索省份(例如山东),能够显示市(例如济南),区(例如历下区) adcode 信息可参考 城市编码表 获取 | 可选 | 无 |
| subdistrict | 子级行政区 | 规则:设置显示下级行政区级数(行政区级别包括:国家、省/直辖市、市、区/县、乡镇/街道多级数据) 可选值:0、1、2、3等数字,并以此类推 0:不返回下级行政区; 1:返回下一级行政区; 2:返回下两级行政区; 3:返回下三级行政区; 需要在此特殊说明,目前部分城市和省直辖县因为没有区县的概念,故在市级下方直接显示街道。 例如:广东-东莞、海南-文昌市 | 可选 | 1 |
| page | 需要第几页数据 | 最外层的 districts 最多会返回20个数据,若超过限制,请用 page 请求下一页数据。 例如:page=2;page=3。默认:page=1 | 可选 | 1 |
| offset | 最外层返回数据个数 | | 可选 | 20 |
| extensions | 返回结果控制 | 此项控制行政区信息中返回行政区边界坐标点; 可选值:base、all; base:不返回行政区边界坐标点; all:只返回当前查询 district 的边界值,不返回子节点的边界值; 目前不能返回乡镇/街道级别的边界值 | 可选 | base |
| filter | 根据区划过滤 | 按照指定行政区划进行过滤,填入后则只返回该省/直辖市信息 需填入 adcode,为了保证数据的正确,强烈建议填入此参数 | 可选 | |
| callback | 回调函数 | callback 值是用户定义的函数名称,此参数只在 output=JSON 时有效 | 可选 | |
| output | 返回数据格式类型 | 可选值:JSON,XML | 可选 | JSON |

2、响应参数简介

行政区域查询的响应结果的格式由请求参数output指定。

|---|---|-----------|------------------------|---------------------------------------------------------------------------------------|
| 名称 ||| 含义 | 规则说明 |
| status ||| 返回结果状态值 | 值为0或1,0表示失败;1表示成功 |
| info ||| 返回状态说明 | 返回状态说明,status 为0时,info 返回错误原因,否则返回"OK"。 |
| infocode ||| 状态码 | 返回状态说明,10000代表正确,详情参阅 info 状态表 |
| suggestion ||| 建议结果列表 | |
| | keywords || 建议关键字列表 | |
| | cities || 建议城市列表 | |
| districts ||| 行政区列表 | |
| | district || 行政区信息 | |
| | | citycode | 城市编码 | |
| | | adcode | 区域编码 | 街道没有独有的 adcode,均继承父类(区县)的 adcode |
| | | name | 行政区名称 | |
| | | polyline | 行政区边界坐标点 | 当一个行政区范围,由完全分隔两块或者多块的地块组 成,每块地的 polyline 坐标串以 | 分隔 。 如北京 的 朝阳区 |
| | | center | 区域中心点 | 乡镇级别返回的center是边界线上的形点,其他行政级别返回的center不一定是中心点,若政府机构位于面内,则返回政府坐标,政府不在面内,则返回繁华点坐标。 |
| | | level | 行政区划级别 | country:国家 province:省份(直辖市会在province显示) city:市(直辖市会在province显示) district:区县 street:街道 |
| | | districts | 下级行政区列表,包含 district 元素 | |

三、Java后台数据获取及空间存储

SpringBoot 是一个开源的 Java 基于 Spring 框架的快速开发平台,它简化了基于 Spring 框架的应用开发,通过一系列的默认配置和约定,让开发者能够快速搭建出稳定、高效的系统。PostGIS 是一个开源对象关系型空间数据库扩展,它允许用户在 PostgreSQL 数据库中存储和操作地理空间数据,提供了强大的空间数据类型和几何函数。通过将 SpringBoot 与 PostGIS 结合,我们可以充分利用 SpringBoot 的开发优势和 PostGIS 的地理空间处理能力,构建出一个高效、稳定的在线地图行政区划离线存储系统。在实践过程中,我们将面临数据格式转换、性能优化以及数据更新与同步等挑战,但这些挑战也将为我们提供宝贵的经验和学习机会。

1、API服务调用

为了简化调用,我们使用UniHttp进行在线服务的集成,接口定义如下:

java 复制代码
package org.yelang.pcwater.amap.service;
import com.burukeyou.uniapi.http.annotation.HttpApi;
import com.burukeyou.uniapi.http.annotation.param.QueryPar;
import com.burukeyou.uniapi.http.annotation.request.GetHttpInterface;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
/**
 * - 高德地图开放平台API接口,负责与高德交互
 * 
 * @author 夜郎king
 *
 */
@HttpApi(url = "https://restapi.amap.com")
public interface AmapService {
	/**
	 * - 行政区域查询 API 服务
	 * 
	 * @param key         请求服务权限标识,必填
	 * @param keywords    查询关键字
	 * @param subdistrict 子级行政区
	 * @param extensions  此项控制行政区信息中返回行政区边界坐标点; 可选值:base、all;
	 *                    -base:不返回行政区边界坐标点;all:只返回当前查询 district
	 *                    -的边界值,不返回子节点的边界值;目前不能返回乡镇/街道级别的边界值
	 * @return
	 */
	@GetHttpInterface("/v3/config/district")
	public HttpResponse<String> district(@QueryPar("key") String key, @QueryPar("keywords") String keywords,
			@QueryPar("subdistrict") int subdistrict, @QueryPar("extensions") String extensions);
}

下面针对返回的数据进行反序列化,将字符串转换为对象,后续可以直接进行数据入库。对应我们定义两个JavaBean实体类,一个是行政区划响应类,代码如下:

java 复制代码
package org.yelang.pcwater.amap.dto.amap.district;

import java.util.List;
import org.yelang.pcwater.amap.dto.amap.CommonDTO;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString(callSuper=true)//callSuper=true表示输出父类属性
@EqualsAndHashCode(callSuper=true)
public class DistrictRespDTO extends CommonDTO{
	private static final long serialVersionUID = 3562870512223434443L;
	private List<DistrictDTO> districts;//行政区列表
}

还有一个最重要的行政区划信息类,在这里类中包含了一个List的集合,就是用来保存当前行政区划的下级单位,代码如下:

java 复制代码
package org.yelang.pcwater.amap.dto.amap.district;
import java.io.Serializable;
import java.util.List;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
/**
 * - 行政区信息数据传输对象
 * @author 夜郎king
 *
 */
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class DistrictDTO implements Serializable{
	private static final long serialVersionUID = -6086240995930742268L;
	@SerializedName("citycode")
	private String cityCode;//城市编码
	private String adcode;//区域编码
	private String name;//行政区名称
	private String polyline;//行政区边界坐标点
	private String center;//区域中心点
	private String level;//行政区划级别
	private List<DistrictDTO> districts;//下级行政区列表,包含 district 元素
}

2、递归获取数据

下面以深圳市为例,详细讲解三级行政区划的数据获取及离线存储。在前面的接口说明中,已经了解了一次可以返回多个下层数据的信息。大家可以根据自己的需要来调整业务,但是不可避免的需要对不同层级的数据进行统一处理。这里演示一种简单的使用递归的方式来进行数据的获取。

java 复制代码
package org.yelang.pcwater.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.yelang.pcwater.amap.dto.amap.district.DistrictDTO;
import org.yelang.pcwater.amap.dto.amap.district.DistrictRespDTO;
import org.yelang.pcwater.amap.service.AmapService;
import org.yelang.pcwater.domain.RegionInfo;
import org.yelang.pcwater.mapper.RegionInfoMapper;
import org.yelang.pcwater.service.IRegionInfoService;
import org.yelang.pcwater.utils.PolygonStringToWKTConverter;
import org.yelang.pcwater.utils.StringUtils;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.burukeyou.uniapi.http.core.response.HttpResponse;
import com.google.gson.Gson;

@Service
public class RegionInfoServiceImpl extends ServiceImpl<RegionInfoMapper, RegionInfo> implements IRegionInfoService{
	// 高德地图的key,需要替换成自己申请的key
    private static final String DEFAULT_AMAP_KEY = "yourkey";
	private static final String EXTENSIONS = "all";
	@Autowired
	private AmapService amapService;
	
	@Override
	public void syncDistrict() throws InterruptedException {
		Gson gson = new Gson();
		int subdistrict = 3;// 返回下三级行政区
		HttpResponse<String> result = amapService.district(DEFAULT_AMAP_KEY, "深圳市", subdistrict, EXTENSIONS);
		if (StringUtils.isEmpty(result.getBodyResult())) {
			return;
		}
		DistrictRespDTO districtRespDTO = gson.fromJson(result.getBodyResult(), DistrictRespDTO.class);
		List<RegionInfo> regionList = new ArrayList<RegionInfo>();
		this.extractDistrict(districtRespDTO.getDistricts(), regionList, 0L);
		
		if(StringUtils.isEmpty(regionList)) {
			return ;
		}
		for (RegionInfo regionInfo : regionList) {
			String polylineGeom = regionInfo.getPolylineGeom();
			String level = regionInfo.getLevel();
			//市、区等行政区划如果第一轮没有采集需要重新获取行政区划边界
			if(StringUtils.isEmpty(polylineGeom) && (level.equalsIgnoreCase("city") || level.equalsIgnoreCase("district"))) {
				//System.out.println("单独处理边界" + regionInfo.getAdcode()+ regionInfo.getName());
				//这里只需要返回当前层级即可
				HttpResponse<String> _tempResp = amapService.district(DEFAULT_AMAP_KEY, regionInfo.getAdcode(), 0, EXTENSIONS);
				String amapPolyline = this.getPolyline(_tempResp.getBodyResult());
				if(StringUtils.isNotEmpty(amapPolyline)) {
					String polylineGeomWkt = PolygonStringToWKTConverter.convertToWKT(amapPolyline);
					regionInfo.setPolylineGeom("SRID=" + 4326 +";" + polylineGeomWkt);//拼接srid,实现动态写入);
				}
				Thread.sleep(100);
			}
		}
		this.saveBatch(regionList, 50);
	}
	
	/**
	 * - 行政区划信息抽取
	 * @param dataList
	 * @param regionList
	 * @param parentId
	 */
	public void extractDistrict(List<DistrictDTO> dataList, List<RegionInfo> regionList, Long parentId) {
		if (StringUtils.isNotEmpty(dataList)) {
			for (DistrictDTO dto : dataList) {
				Long pkId = IdWorker.getId();
				String center = dto.getCenter();
				String polyline = dto.getPolyline();
				String level = dto.getLevel();
				double lon = Double.parseDouble(center.split(",")[0]);
				double lat = Double.parseDouble(center.split(",")[1]);
				
				String centerGeomWkt = PolygonStringToWKTConverter.point(lon, lat);
				RegionInfo region= new RegionInfo(pkId, parentId, dto.getCityCode(), dto.getAdcode(), dto.getName(), center, level, lon, lat, "SRID=" + 4326 +";" + centerGeomWkt);
				
				if(StringUtils.isNotEmpty(polyline)) {
					String polylineGeomWkt = PolygonStringToWKTConverter.convertToWKT(polyline);
					region.setPolylineGeom("SRID=" + 4326 +";" + polylineGeomWkt);//拼接srid,实现动态写入);
				}
				regionList.add(region);
				if (StringUtils.isNotEmpty(dto.getDistricts())) {
					extractDistrict(dto.getDistricts(), regionList, pkId);
				}
			}
		}
	}
	
	/**
	 * - 从响应文本中获取边界
	 * @param responseResult 响应文本
	 * @return
	 */
	public String getPolyline(String responseResult) {
		String result = "";
		if (StringUtils.isEmpty(responseResult)) {
			return result;
		}
		DistrictRespDTO districtRespDTO = new Gson().fromJson(responseResult, DistrictRespDTO.class);
		if(null != districtRespDTO && StringUtils.isNotEmpty(districtRespDTO.getDistricts())) {
			result = districtRespDTO.getDistricts().get(0).getPolyline();
		}
		return result;
	}
}

使用以上代码就可以实现将行政区划代码离线存储保存到空间数据库中。大家可以使用Junit的方式进行集成,也可以做成API的方式进行调用。需要说明的是,在使用高德地图时,返回的数据坐标是高德的坐标系,在设置SRID时,大家可以使用4326,但是在程序中需要进行标注,尤其是在别的底图上展示时,对坐标进行处理,处理偏移的问题。具体的处理方法,这里不进行赘述。之前的博客中已经做过介绍。

四、成果展示

本节将对深圳的三级行政区划信息进行展示,包括直接在PostGIS中进行查询和WebGIS可视化。以此验证我们的数据采集是否成功。

1、PostGIS数据查询

在我们的程序中调用数据库的方法后,可以实现将深圳市的三级行政区划数据进行PostGIS存储,下面来验证一下是否成功,使用以下SQL语句查询,

sql 复制代码
select * from biz_region_info;

在Navicat客户端中执行以上sql后,在返回结果中可以看到以下内容:

如果能看到输出,说明数据已经成功进行了采集。

2、WebGIS可视化

根据实际的需要,我们可以将上述的行政区划数据进行空间可视化。这里我们使用Leaflet对深圳市的三级区划进行展示,这里仅演示实际的效果,具体Leaflet的集成内容,之前有系列博客讲解。

深圳市行政区划

坪山行政区划

五、总结

以上就是本文的主要内容,本文将深入探讨如何结合强大的 SpringBoot 框架和专业的地理空间数据库 PostGIS,以高德地图开放平台为例,实现在线地图行政区划数据的离线存储,以满足多样化的业务需求,提升系统的灵活性和可靠性。通过本次实践探索,我们不仅能够掌握 SpringBoot 和 PostGIS 在地理空间数据处理方面的应用,还能深入了解在线地图数据离线存储的关键技术和实现方法。这将为我们在地理信息系统开发、大数据处理、移动应用开发等领域提供宝贵的经验和参考。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。