SpringBoot+WebGIS:全国风景区按省展示最佳实践

2026-06-22阅读 0热度 0
国风

前言

旅行这件事,说到底是心灵的一次放空。每一次出发,都像翻开一本新书,你不知道下一页会遇见怎样的风景,但总能在某个拐角处,被一缕阳光、一阵山风击中,然后突然想明白一些事。路上看过的山川湖海,遇见的有趣灵魂,最后都悄悄沉淀下来,变成心里最踏实的底气。身体和灵魂,总有一个要在路上——这话听多了,但真到了路上,才知道那种“在路上”的感觉有多上瘾。

不知道你是否还记得自己上一次旅行是什么时候?是和谁一起?最难忘的风景又是哪里?除了风景,还有什么让你念念不忘?既然选择了出发,那就去那些风景好的地方吧,去认真的景区里走走,把心放空,也把能量蓄满。然后带着这些能量,继续扎进生活里——这才是旅行的意义。

不管你是旅游爱好者,还是文旅行业的分析达人,用WebGIS来一场时空之旅,在地图上把旅行的足迹标出来,都是一件挺酷的事。这篇文章就以全国的旅游资源为样本,按省份展示,把不同省份的风景区信息标注在地图上。顺便也能看看,全国范围里,这些风景区的空间分布到底长什么样。

文章会先介绍一下数据库里风景区的基本情况,然后说说怎么用SpringBoot搭一个风景区查询的后台,接着讲前端怎么基于Leaflet做可视化,最后拿几个实际省份的案例来看看效果。喜欢旅游的朋友,别急着关页面,咱们一起看看图,也算云旅行了。

一、全国风景区信息介绍

这一节先说说核心数据——全国风景区信息。其实之前在另一篇文章里已经详细讲过全国A级风景区数据入库的过程,用的是Ja va和PostGIS那一套。不过为了让大家读起来不觉得突兀,这里重新把表结构拿出来说一下,也方便熟悉空间表的设计思路。

1、全国范围内数据分布

数据入库的细节就不重复了,直接看表结构:

对应的建表脚本如下:

CREATE TABLE "public"."biz_scenic_spot" (
  "id" int8 NOT NULL,
  "name" varchar(255) COLLATE "pg_catalog"."default",
  "level" varchar(4) COLLATE "pg_catalog"."default",
  "province" varchar(255) COLLATE "pg_catalog"."default",
  "city" varchar(255) COLLATE "pg_catalog"."default",
  "area" varchar(255) COLLATE "pg_catalog"."default",
  "address" varchar(255) COLLATE "pg_catalog"."default",
  "evaluation_time" varchar(255) COLLATE "pg_catalog"."default",
  "publish_time" varchar(255) COLLATE "pg_catalog"."default",
  "lng_gcj02" varchar(30) COLLATE "pg_catalog"."default",
  "lat_gcj02" varchar(30) COLLATE "pg_catalog"."default",
  "lng_bd09" varchar(30) COLLATE "pg_catalog"."default",
  "lat_bd09" varchar(30) COLLATE "pg_catalog"."default",
  "lng_wgs84" varchar(30) COLLATE "pg_catalog"."default",
  "lat_wgs84" varchar(30) COLLATE "pg_catalog"."default",
  "geom" "public"."geometry",
  "publish_link" varchar(255) COLLATE "pg_catalog"."default",
  CONSTRAINT "pk_biz_scenic_spot" PRIMARY KEY ("id")
);
CREATE INDEX "idx_biz_scenic_spot_geom" ON "public"."biz_scenic_spot" USING gist ("geom" "public"."gist_geometry_ops_2d");
COMMENT ON COLUMN "public"."biz_scenic_spot"."id" IS '主键';
COMMENT ON COLUMN "public"."biz_scenic_spot"."name" IS '景区名称';
COMMENT ON COLUMN "public"."biz_scenic_spot"."level" IS '景区级别';
COMMENT ON COLUMN "public"."biz_scenic_spot"."province" IS '所属省份';
COMMENT ON COLUMN "public"."biz_scenic_spot"."city" IS '所属城市';
COMMENT ON COLUMN "public"."biz_scenic_spot"."area" IS '所属区县';
COMMENT ON COLUMN "public"."biz_scenic_spot"."address" IS '地址';
COMMENT ON COLUMN "public"."biz_scenic_spot"."evaluation_time" IS '评定时间';
COMMENT ON COLUMN "public"."biz_scenic_spot"."publish_time" IS '发布时间';
COMMENT ON COLUMN "public"."biz_scenic_spot"."lng_gcj02" IS 'lng_GCJ02';
COMMENT ON COLUMN "public"."biz_scenic_spot"."lat_gcj02" IS 'lat_GCJ02';
COMMENT ON COLUMN "public"."biz_scenic_spot"."lng_bd09" IS 'lng_BD09';
COMMENT ON COLUMN "public"."biz_scenic_spot"."lat_bd09" IS 'lat_BD09';
COMMENT ON COLUMN "public"."biz_scenic_spot"."lng_wgs84" IS 'lng_WGS84';
COMMENT ON COLUMN "public"."biz_scenic_spot"."lat_wgs84" IS 'lat_WGS84';
COMMENT ON COLUMN "public"."biz_scenic_spot"."publish_link" IS '发布链接';
COMMENT ON TABLE "public"."biz_scenic_spot" IS '全国风景区信息表';

数据库里一共有14847条风景区记录。按级别分组统计一下,结果如下(其中3条没有具体级别,估计是数据异常):

序号景区级别数量
15A336
24A4486
33A7866
42A2066
5A93

2、全国风景区分布

从数据库层面先看看全国的风景区分布长什么样。直接用省份字段分组(没有关联外部的省份信息表,用的是风景区表里的省份字段):

select count(1) num,province from biz_scenic_spot group by province order by num desc;

结果如下:

序号省份景区数量
1浙江1322
2山东1205
3四川895
4广西662
5安徽605
6江苏600
7广东597
8河南580
9湖北576
10新疆574
11贵州570
12云南561
13辽宁558
14湖南550
16陕西527
17河北490
18福建463
19甘肃442
20内蒙古428
21江西421
22黑龙江409
23吉林275
24重庆272
25山西268
26北京216
27青海163
28西藏148
29宁夏147
30上海139
31天津100
32海南84

结果一目了然:浙江、山东、四川稳居前三,旅游资源相当丰富。后三位则是海南、天津、上海。当然,这只是表格里的数字,要让数据真正有空间感,还得靠地图说话。接下来就用WebGIS把这些分布落到地图上。

3、PostGIS空间关联查询

为了让景区信息具有空间分布特征,我们把它和省级行政区划表(biz_province)做空间关联。用st_contains(geom, geom)来判定某个省份范围内包含了哪些景点。举个例子,查湖南省(code=430000)的风景区:

SELECT T.*, st_asgeojson(T.geom) AS geomJson 
FROM biz_province P, biz_scenic_spot T 
WHERE P.code = '430000' AND st_contains(P.geom, T.geom);

二、后台查询的设计与实现

后台服务这块,还是用熟悉的SpringBoot + Mybatis-Plus组合,空间数据库选PostGIS。整体按MVC模式分层来设计。

1、Model和Mapper层

Model层包括实体类和视图对象,Mapper层就是数据访问层。基于Mybatis-Plus,操作起来很顺手。实体类代码:

package com.yelang.project.extend.scenicspot.domain;

import ja va.io.Serializable;
import org.apache.ibatis.type.BlobByteObjectArrayTypeHandler;
import org.apache.ibatis.type.BlobInputStreamTypeHandler;
import org.apache.ibatis.type.BlobTypeHandler;
import org.apache.ibatis.type.ByteArrayTypeHandler;
import org.apache.ibatis.type.ByteObjectArrayTypeHandler;
import org.apache.ibatis.type.ByteTypeHandler;
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.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@TableName(value = "biz_scenic_spot", autoResultMap = true)
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
@ToString
public class ScenicSpot implements Serializable {
    private static final long serialVersionUID = 1830004907219610805L;
    @TableId
    private Long id;
    private String name;
    private String level;
    private String province;
    private String city;
    private String area;
    private String address;
    @TableField(value="evaluation_time")
    private String evaluationTime;
    @TableField(value="publish_time")
    private String publishTime;
    @TableField(value="publish_link")
    private String publishLink;
    @TableField(value="lng_GCJ02")
    private String lngGCJ02;
    @TableField(value="lat_GCJ02")
    private String latGCJ02;
    @TableField(value="lng_BD09")
    private String lngBD09;
    @TableField(value="lat_BD09")
    private String latBD09;
    @TableField(value="lng_WGS84")
    private String lngWGS84;
    @TableField(value="lat_WGS84")
    private String latWGS84;
    @TableField(typeHandler = PgGeometryTypeHandler.class)
    private String geom;
    @TableField(exist=false)
    private String geomJson;
    @TableField(exist=false)
    private byte[] tile;

    public ScenicSpot(String name, String level, String province, String city, String area, String address,
                      String evaluationTime, String publishTime, String publishLink,
                      String lngGCJ02, String latGCJ02, String lngBD09, String latBD09,
                      String lngWGS84, String latWGS84, String geom) {
        super();
        this.name = name;
        this.level = level;
        this.province = province;
        this.city = city;
        this.area = area;
        this.address = address;
        this.evaluationTime = evaluationTime;
        this.publishTime = publishTime;
        this.publishLink = publishLink;
        this.lngGCJ02 = lngGCJ02;
        this.latGCJ02 = latGCJ02;
        this.lngBD09 = lngBD09;
        this.latBD09 = latBD09;
        this.lngWGS84 = lngWGS84;
        this.latWGS84 = latWGS84;
        this.geom = geom;
    }
}

Mapper层主要提供按省级行政区划代码查询风景区列表的方法,关键代码:

package com.yelang.project.extend.scenicspot.mapper;

import ja va.util.List;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.type.JdbcType;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yelang.project.extend.scenicspot.domain.ScenicSpot;
import com.yelang.project.extend.scenicspot.domain.TileBox;

public interface ScenicSpotMapper extends BaseMapper {
    static final String FIND_GEOJSON_SQL = "<script>"
        + "select st_asgeojson(geom) as geomJson from biz_scenic_spot "
        + "where id = #{id} "
        + "and name like concat('%', #{name}, '%')"
        + "</script>";

    @Select(FIND_GEOJSON_SQL)
    ScenicSpot findGeoJsonById(@Param("id") Long id, @Param("name") String name);

    static final String FIND_LISTBY_PCODE = "<script>"
        + " select t.*, st_asgeojson(t.geom) as geomJson from biz_province p, biz_scenic_spot t "
        + " where p.code = #{code} and st_contains(p.geom, t.geom) "
        + "</script>";

    @Select(FIND_LISTBY_PCODE)
    List findListByPcode(@Param("code") String code);
}

2、业务层和控制层设计

业务层比较简单,就是搭桥。关键代码如下:

package com.yelang.project.extend.scenicspot.service.impl;

@Service
public class ScenicSpotServiceImpl extends ServiceImpl implements IScenicSpotService {
    @Override
    public List findListByPcode(String code) {
        return this.baseMapper.findListByPcode(code);
    }
}
package com.yelang.project.extend.scenicspot.controller;

import ja va.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;

@Controller
@RequestMapping("/sspot/province")
public class ScenicSpotProvinceController extends BaseController {
    private String prefix = "scenicspot/province";
    @Autowired
    private IProvinceService provinceService;
    @Autowired
    private IScenicSpotService sspotService;

    @RequiresPermissions("sspot:province:view")
    @GetMapping()
    public String map() {
        return prefix + "/map";
    }

    @RequiresPermissions("sspot:province:list")
    @PostMapping("/list")
    @ResponseBody
    public TableDataInfo list(Province province) {
        startPage();
        List list = provinceService.selectList(province);
        return getDataTable(list);
    }

    @RequiresPermissions("sspot:province:geom")
    @GetMapping("/geojson/{id}")
    @ResponseBody
    public AjaxResult getGeojson(@PathVariable("id") Long id) {
        Province province = provinceService.findGeoJsonById(id, null);
        return AjaxResult.success().put("data", province.getGeomJson());
    }

    @RequiresPermissions("sspot:province:sspotlist")
    @GetMapping("/datalist/{code}")
    @ResponseBody
    public AjaxResult sspotList(@PathVariable("code") String code) {
        List list = sspotService.findListByPcode(code);
        AjaxResult ar = AjaxResult.success();
        ar.put("data", list);
        return ar;
    }
}

控制层提供了四个接口:页面跳转、省份列表、省份GeoJSON、省份下辖风景区列表。后台就这么搭起来了,服务端的工作基本搞定。

三、WebGIS可视化

前端可视化这块,用Leaflet框架,配合Canvas文本标记方法。地图定义和列表展示不是重点,直接跳过,说核心。

1、省份范围可视化

界面里先展示省份列表,点击某个省份的操作按钮,获取该省份的code,然后拉取对应的省级行政区划GeoJSON,用Leaflet展示到地图上。关键代码:

function showProvince(id) {
    var myStyle = {color:"white", weight:5, "opacity":1, fillOpacity: 0};
    $.ajax({
        type: "get",
        url: prefix + "/geojson/" + id,
        data: {},
        async: false,
        dataType: "json",
        cache: false,
        processData: false,
        success: function(result) {
            if (undefined != provinceAreaLayer) {
                provinceAreaLayer.removeFrom(mymap);
            }
            if (result.code == web_status.SUCCESS) {
                var geojson = JSON.parse(result.data);
                provinceAreaLayer = L.geoJSON(geojson, {style: myStyle}).addTo(mymap);
                mymap.setView(provinceAreaLayer.getBounds().getCenter(), 7);
            }
        },
        error: function() {
            $.modal.alertWarning("获取空间信息失败");
        }
    });
}

2、省级风景区可视化展示

通过code关联两个表,做空间叠加分析,把省份范围内的风景区全部标出来。核心代码:

function showScenicSpot(code) {
    $.ajax({
        type: "get",
        url: prefix + "/datalist/" + code,
        dataType: "json",
        cache: false,
        processData: false,
        success: function(result) {
            if (result.code == web_status.SUCCESS) {
                var strokeStyleSet = "#23168d";
                var lat, lng, cityInfo;
                for (var i = 0; i < result.data.length; i++) {
                    var dataInfo = result.data[i];
                    var geomObj = JSON.parse(dataInfo.geomJson);
                    if (i == 0) {
                        lat = geomObj.coordinates[1];
                        lng = geomObj.coordinates[0];
                        continue;
                    }
                    var radiusSize = 5;
                    switch(dataInfo.level) {
                        case '5A': strokeStyleSet = "#c50808"; radiusSize += 7; break;
                        case '4A': strokeStyleSet = "#c37322"; radiusSize += 5; break;
                        case '3A': strokeStyleSet = "#6f8d16"; radiusSize += 3; break;
                        case '2A': strokeStyleSet = "#168d40"; radiusSize += 1; break;
                        default: strokeStyleSet = "#23168d";
                    }
                    var content = "名称:" + dataInfo.name 
                        + "
级别:" + dataInfo.level; content += "
所属行政区划:" + dataInfo.province + "/" + dataInfo.city + "/" + dataInfo.area; content += "
评定时间:" + dataInfo.evaluationTime; var latlng = new L.latLng(geomObj.coordinates[1], geomObj.coordinates[0]); let marker = L.circleMarker(latlng, { radius: radiusSize, color: strokeStyleSet, labelStyle: { offsetX: 0, offsetY: 30, text: dataInfo.name, rotation: 0, zIndex: radiusSize, minZoom: 5, strokeStyle: strokeStyleSet } }).addTo(showLayerGroup); marker.bindPopup(content); } mymap.addLayer(showLayerGroup); } }, error: function() { $.modal.alertWarning("获取信息失败"); } }); }

3、成果展示

代码跑通之后,来看看几个省份的实际效果。选了山西、辽宁、江苏、安徽、新疆五个省份的风景区分布图,直观感受一下WebGIS带来的空间视角。

山西省风景区分布图山西省风景区分布图

辽宁省风景区分布图辽宁省风景区分布图

江苏省风景区分布图江苏省风景区分布图

安徽省风景区分布图安徽省风景区分布图

新疆维吾尔自治区风景区分布示意图新疆维吾尔自治区风景区分布示意图

总结

从数据入库到后台服务,再到前端可视化,整个流程走下来,算是一次比较完整的WebGIS实践。通过PostGIS的空间查询能力,把全国风景区数据按省份切分,再用Leaflet落到地图上,确实能很直观地看到旅游资源在空间上的分布规律。希望这个思路能给想做类似项目的朋友一些参考。

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策