在构建WebGIS的应用系统中,通常会遇到以下的建设需求。功能点如下:
- 实现影像地图的展示,可以放大、缩小和浏览地图。
- 地图的拖拽范围需要控制在合理的经纬度范围内。
- 在影像地图侧边实现某乡镇级行政区的信息展示,包括名称,级别;以及支持在影像地图上进行矢量数据的展示,同时在显示的边界内展示乡镇信息。
通过以上的信息分析,大致的技术点和关键技术其实在个人博客中已经有一定的涉及。比如关于Leafletjs的二维WebGIS系统开发、如何在LeafLet上叠加影像地图、Leaflet如何限制地图的拖动范围、空间矢量数据如何导入PostGIS数据库、MybatisPlus中操作Geometry字段信息、LeafLet中展示GeoJSON数据。
系列文章地址如下表所示:
本文将采用Leafletjs地图开发组件,围绕GeoJSON的可视化展示,以湖南省乡镇行政区划数据的查询,空间定位作为实践案例,完整讲述一个基础的WebGIS小功能,最后形成一个GeoJSON的可视化工具。
序号 博客地址 1 gis信息可视化之一Leaflet组件介绍 2 layerGroup在LeafLet中的实战 3 postgis空间数据导入及可视化 4 基于Mybatis-Plus实现Geometry字段在PostGis空间数据库中的使用 5 基于Leaflet的leaflet-sidebar侧边栏组件集成 6 Leaflet中如何限制地图的拖动范围 7 玩转Leaflet-带你吃透Control知识 8 LeafLet实战-扩展工具栏指南
在上面的前言部分,简单的对功能需求进行了分析。其实功能很简单,是最常见的WebGIS功能点。主要包括影像地图的展示、地图的放大和缩小,平移;影像底图图层和标签图层的叠加展示、湖南省乡镇行政区划数据的展示和空间定位,对GeoJSON数据进行地图定位,叠加自定义Marker对象,显示乡镇名称和自定义样式等功能。
业务架构比较简单,针对简单的业务需求设计简单的架构。主要包含以下三层:
数据层:数据层主要包含业务数据库,用于存储用户信息、权限数据等;矢量数据库主要包含湖南省乡镇行政区划矢量信息;影像底图就是瓦片和标签瓦片信息等;
服务层:服务层在数据层的基础上,主要提供相应的数据查询服务能力,包含空间数据查询服务、空间分析服务、用户管理服务、地图展示服务;通过服务层,将相关底层的调用封装起来,供上层的应用层进行调用。
应用层:应用层主要面向具体的使用用户。将直接调用服务层的服务组装成用户需要的功能。主要包含行政区划展示、空间数据定位展示、项目管理及用户管理功能。
同样基于简单的业务需求,以及简单的应用架构,这里采用简单的单体架构模式。在此基础上可以进行架构的迭代和升级,保证业务的扩展性。
前端技术栈:这里的前端依然采用最简单的es5架构,使用Jquery+Html5+css等经典原生开发模式。主要采用的技术如下:
序号 | 技术点 | 说明 |
1 | Leaflet.js | WebGIS 地图展示组件 |
2 | leaflet-sidebar.js | 基于Leaflet的侧边栏展示组件 |
3 | thymeleaf | 前端模板引擎 |
4 | bootstrap | 前端bootstrap组件库 |
5 | bootstrap-table | 基于bootstrap的表格组件库 |
6 | jquery | Dom操作和Ajax的操作库 |
后端技术栈:后端是经过改造的Ruoyi单体框架,主要改造点是完全的兼容PostgreSQL,很多原来MySQL的语法在迁移到PG后有很多的不兼容问题,在此基础上进行了升级和改造。
序号 | 技术点 | 说明 |
1 | Springboot | 基础技术框架 |
2 | Mybatis-plus | 操作数据库的ORM框架 |
3 | flywaydb | 自动管理数据组件 |
4 | postgis-jdbc | postgis数据库支持驱动 |
5 | spring-boot-admin | springboot监控组件 |
6 | shiro | shiro开源安全认证框架 |
空间数据库:空间数据库中不仅仅包含空间数据的存储,还有普通业务数据的存储。这里完全采用PostgreSQL和扩展PostGIS进行空间数据存储和分析需求。
在进行系统研发之前,需要提前准备湖南省的乡镇行政区划shp数据。这里使用的实例数据从互联网下载而成,仅供学习使用。
由于系统要实现将湖南省的所有乡镇信息全部导入到空间数据库PostGIS中,这里我们采用PostGIS自带的客户端工具进行导入的方式。具体操作方式略,有兴趣的朋友可以看之前写得博文或者自行查询搜索引擎查阅相关资料。
将数据导入空间数据库后,会在数据库中形成一张biz_hn_town的表,其中,biz_hn_town是在导入的时候自动把shp的文件名当成了表名。
可以查看一下这张表的物理结构,如下sql所示:
-- ----------------------------
-- Table structure for biz_hn_town
-- ----------------------------
DROP TABLE IF EXISTS "public"."biz_hn_town";
CREATE TABLE "public"."biz_hn_town" (
"gid" int4 NOT NULL DEFAULT nextval('biz_hn_town_gid_seq'::regclass),
"gml_id" varchar(80) COLLATE "pg_catalog"."default",
"name" varchar(80) COLLATE "pg_catalog"."default",
"layer" varchar(80) COLLATE "pg_catalog"."default",
"code" varchar(80) COLLATE "pg_catalog"."default",
"grade" int4,
"geom" "public"."geometry"
);
-- ----------------------------
-- Indexes structure for table biz_hn_town
-- ----------------------------
CREATE INDEX "biz_hn_town_geom_idx" ON "public"."biz_hn_town" USING gist (
"geom" "public"."gist_geometry_ops_2d"
);
-- ----------------------------
-- Primary Key structure for table biz_hn_town
-- ----------------------------
ALTER TABLE "public"."biz_hn_town" ADD CONSTRAINT "biz_hn_town_pkey" PRIMARY KEY ("gid");
通过查询语句,我们可以看到
select * from biz_hn_town;
package com.yelang.project.extend.map.controller;
import java.util.List;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.yelang.framework.web.controller.BaseController;
import com.yelang.framework.web.domain.AjaxResult;
import com.yelang.framework.web.page.TableDataInfo;
import com.yelang.project.extend.map.domain.HnTown;
import com.yelang.project.extend.map.service.IHnTownService;
/**
*地图Controller
* @author yelangking
* @date 2023-5-7
*/
@Controller
@RequestMapping("/extend/map")
public class MapController extends BaseController{
private String prefix = "extend/map";
@Autowired
private IHnTownService hnTownService;
@RequiresPermissions("extend:map:view")
@GetMapping()
public String map(){
return prefix + "/map";
}
@RequiresPermissions("extend:map:list")
@PostMapping("/list")
@ResponseBody
public TableDataInfo list(HnTown hnTown){
startPage();
List list = hnTownService.selectList(hnTown);
return getDataTable(list);
}
@RequiresPermissions("extend:map:geom")
@GetMapping("/geojson/{id}")
@ResponseBody
public AjaxResult editSave(@PathVariable("id") Long id){
HnTown hnTown = hnTownService.findGeoJsonById(id, null);
return AjaxResult.success().put("data", hnTown.getGeomJson());
}
}
mapper这里采用了自定义的sql脚本,并有mybatis执行引擎来进行执行。将空间字段geom采用st_asgeojson方法转换成geojson字符串,并由前端leaflet.js进行渲染。
package com.yelang.project.extend.map.mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yelang.project.extend.map.domain.HnTown;
public interface HnTownMapper extends BaseMapper{
static final String FIND_GEOJSON_SQL="";
@Select(FIND_GEOJSON_SQL)
HnTown findGeoJsonById(@Param("gid")Long gid,@Param("name")String name);
}
package com.yelang.project.extend.map.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.yelang.framework.handler.PgGeometryTypeHandler;
import lombok.*;
@TableName(value ="biz_hn_town",autoResultMap = true)
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class HnTown {
@TableId(value="gid")
private Long gId;
@TableField(value="gml_id")
private String gmlId;
private String name;
private String layer;
private String code;
private Integer grade;
@TableField(typeHandler = PgGeometryTypeHandler.class)
private String geom;
@TableField(exist=false)
private String geomJson;
}
$("#mapid").height($(window).height());//动态设置高度
L.CRS.CustomEPSG4326 = L.extend({}, L.CRS.Earth, {
code: 'EPSG:4326',
projection: L.Projection.LonLat,
transformation: new L.Transformation(1 / 180, 1, -1 / 180, 0.5),
scale: function (zoom) {
return 256 * Math.pow(2, zoom - 1);
}
});
//限制地图的拖动范围是正负90到正负180,这样才合理。
var maxBounds = L.latLngBounds(L.latLng(-90, -180), L.latLng(90, 180)); //构建视图限制范围 第一个参数是左上角经纬度 第二个参数是右下点经纬度
var mymap = L.map('mapid',{crs:L.CRS.CustomEPSG4326,maxBounds:maxBounds,attributionControl:false}).setView([29.052934, 104.0625], 5);
var showLayerGroup =L.featureGroup().addTo(mymap);
L.tileLayer('http://localhost:8086/data/basemap_nowater/1_10_tms/{z}/{x}/{y}.jpg', {minZoom:1,
maxZoom: 16,
id: 'baseMap-nowater',
tileSize: 256,
zoomOffset: -1
}).addTo(mymap);
//标签
L.tileLayer('http://localhost:8086/data/basemap_nowater/1-10label/{z}/{x}/{y}.png', {maxZoom: 10,minZoom:1,
id: 'mapbox/label',tileSize: 256,zoomOffset: -1
}).addTo(mymap);
var popup = L.popup();
function onMapClick(e) {
popup.setLatLng(e.latlng)
.setContent("当前坐标为:" + e.latlng.toString())
.openOn(mymap);
}
mymap.on('click', onMapClick);
function initSidebar(){//初始化sidebar页面
var sidebar = L.control.sidebar('sidebar', {position: 'right'}).addTo(mymap);
//默认sidebar打开,并展示一个tab页
sidebar.open();
$("#xz_info").addClass("active");
$("#home").addClass("active");
//初始化行政区划表格
initHnTownTable();
}
function initHnTownTable(){
var options = {
url: prefix + "/list",
createUrl: prefix + "/add",
updateUrl: prefix + "/edit/{id}",
modalName: "乡镇行政区划",
columns: [{
checkbox: true
},,
{
title: '操作',
align: 'center',
formatter: function(value, row, index) {
var actions = [];
actions.push('定位');
return actions.join('');
}
}]
};
$.table.init(options);
}
function previewTown(gid,name){
var myStyle = {color:"red",weight:8,"opacity":0.6};
$.ajax({
type:"get",
url:prefix + "/geojson/" + gid,
data:{},
dataType:"json",
cache:false,
processData:false,
success:function(result){
if(result.code == web_status.SUCCESS){
var areaLayer = L.geoJSON(JSON.parse(result.data),{style:myStyle}).addTo(mymap);
var content = "名称:"+name+"" +
"时间:2020-05-04" +
"面积:8.52 K㎡";
var myIcon = L.divIcon({
html: ""+content+"",
className: 'my-div-icon',
iconSize: 100
});
showLayerGroup.clearLayers();
showLayerGroup.addLayer(areaLayer);
mymap.fitBounds(areaLayer.getBounds());
//中心点位
L.marker(areaLayer.getBounds().getCenter(), { icon: myIcon}).addTo(showLayerGroup);
}
},
error:function(){
$.modal.alertWarning("获取空间信息失败");
}
});
}
在实际工 具的展示中可以看到,底图实现了自定义影像地图的加载,以及侧边栏表格的数据展示。在表格中展示了乡镇名称、级别以及定位操作按钮。点击定位按钮可以进行乡镇区划定位。
点击列表中的定位按钮,可以实现当前乡镇空间的自动定位,并在地图中进行高亮展示定位。同时在列表中实现按照乡镇名字进行检索的功能,具体如下图所示(以中寨镇为例):
通过这个简单的程序,集中演示了WebGIS乡镇行政区划数据可视化工具的基本功能,重点围绕WebGIS的相关技术,讲解如何开发可视化工具。基于该工具,基本满足我们的预期业务需求,实现了乡镇行政区划数据的展示及空间定位。对于掌握WebGIS的开发和实现空间数据可视化提供了良好的基础知识讲解。
以上就是本文的主要内容,本文将重点讲解如何采用Leafletjs地图开发组件,围绕GeoJSON的可视化展示,以湖南省乡镇行政区划数据的查询,空间定位作为实践案例,完整讲述一个基础的WebGIS小功能,最后形成一个GeoJSON的可视化工具。希望这个可视化工具实现的技术路径可以作为您开发WebGIS程序的一些参考。行文仓促,如有问题,欢迎批评指针,更加环境在文章的最后留下您的宝贵意见和评论。