新闻详情

新闻详情

首页 / 资讯中心 / 详情

SpringBoot+Leaflet实战:行政区划地图掩膜与镂空遮罩实现

发布时间:2026/10/1 20:15:29来源:尧图网络
SpringBoot+Leaflet实战:行政区划地图掩膜与镂空遮罩实现
做政务大屏、WebGIS 可视化项目的时候“行政区划地图掩膜”这个需求我几乎每次都会遇到。客户不会跟你提“掩膜”这么专业的词他们只会说把山东这块区域突出显示其他地方压暗一点。听起来很简单但真正动手你就会发现这个遮罩并不是一个普通的半透明黑图层它是一块“带洞的遮罩”——洞的位置刚好是你需要突显的省份轮廓而且这个洞还要随着地图缩放、平移始终保持准确贴合。这篇文章就是我用 SpringBoot 提供行政区划数据、Leaflet 做前端渲染完整跑通掩膜效果后的一个实战记录适合正在做地图可视化、大屏项目或者想把 SpringBoot 和 Leaflet 前后端串起来的小伙伴参考。1. 掩膜效果到底是什么先想清楚再动手1.1 掩膜的本质与应用场景掩膜Mask这个词借自图像处理指用一块遮罩挡住不需要的区域只露出感兴趣的区域。放在地图场景里就是让全国地图整体压暗只有选中的某个行政区保持正常亮度甚至高亮视觉焦点一下就锁定了非常适合政务大屏里的重点区域展示、指挥调度平台里的受灾范围圈选、以及园区网格化管理。很多人一开始会想着“我直接在地图上盖一层半透明黑色 Div 不就行了”但这样目标区域也会跟着变暗达不到“突出”的目的。所以掩膜的真正难点在于遮罩要覆盖全图又要在目标区域镂空。镂空区域还必须跟随地图的拖动和缩放实时变化因为行政区边界是地理坐标而屏幕上是像素坐标两者之间的映射关系一直在变。1.2 三种实现方案对比我在动手之前对比过三条技术路线这里先分享出来免得你走弯路。第一种是用 Leaflet 自带的 L.geoJSON 叠加多边形遮罩层。思路是把全国画成一个覆盖视野的大矩形再把目标行政区的轮廓画上去两个图元一叠加视觉上就出现了镂空。这个方案实现最快但本质上是两个独立图层的叠放缩放过程中边界容易闪缝而且半透明区域叠加后颜色会不均匀严格来说不算真正的镂空。第二种是引入 Turf.js 做空间差集运算。用 Turf 把“全国矩形”和“目标行政区多边形”做一次 difference 计算得到真正的差集多边形再交给 Leaflet 渲染。这条路听起来很正规但 Turf 的差集对城市级、省级这种大范围面数据的计算比较耗时首次加载会有明显卡顿而且 Turf 库体积不小为了一个视觉效果引入一个重型空间计算库性价比偏低。第三种是自定义 Canvas 图层用 Canvas 的 Path2D 把矩形外框和目标行政区轮廓放进同一个路径里最后用 evenodd 填充规则一次绘制完成。这是我在实测后最终采用的方式原因很直接它不依赖额外库绘制完全可控拖拽缩放时重绘速度飞快而且 evenodd 是 Canvas 原生支持的填充规则镂空效果天然就是对的。1.3 为什么后端要选 SpringBoot 做数据出口至于后端用 SpringBoot并不是因为它有多花哨而是实际项目里行政区划数据大概率不能直接放在前端静态目录里。一方面这些 GeoJSON 数据往往要经过权限校验才能访问另一方面很多底图供应商或数据服务商不允许前端直接跨域调用。SpringBoot 在这里扮演的是一个数据代理层前端请求我们的接口后端去拿行政区划 GeoJSON再统一返回跨域、权限、缓存全都在后端收敛掉。前端开发只需要关注地图交互不用处理乱七八糟的数据源问题。2. 数据准备与 SpringBoot 数据服务落地2.1 行政区划数据从哪来DataV GeoAtlas行政区划边界数据是掩膜的地基边界数据不准后面全部白干。我用的数据源是阿里云 DataV 的 GeoAtlas 公共接口这个接口提供了全国、省、市、区县各级行政区的 GeoJSON 数据结构很规整更新也及时关键是省去了自己处理 GDB、SHP 转 GeoJSON 的麻烦。它的 URL 规则很有规律核心是行政区划代码 adcode层级示例 adcode请求地址全国100000https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json山东省370000https://geo.datav.aliyun.com/areas_v3/bound/370000_full.json济南市370100https://geo.datav.aliyun.com/areas_v3/bound/370100_full.json这里有个小知识加_full后缀的才是带完整 geometry 的边界数据不带后缀的一般只包含属性列表和中心点很适合做下钻菜单但画不了图。返回的 GeoJSON 里features 数组的每个元素都有 properties.adcode、properties.name、properties.center 这些字段center 的格式是 [经度, 纬度]这一点后面写前端代码时要特别留意。2.2 SpringBoot 工程搭建与依赖版本项目骨架我用的是 SpringBoot 3.x配合 JDK 17。如果你手头还在用 JDK 8建议老老实实选 SpringBoot 2.7.x不要硬上 3.x否则javax和jakarta包名差异会让你在启动阶段被各种 ClassNotFound 折磨。pom.xml 里最核心的就一个 web starter因为我们的需求很单纯把远程 GeoJSON 代理回来。不需要数据库不需要 MyBatis不需要安全框架加一堆用不到的依赖只会拖慢启动速度。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency /dependencies很多新人容易在这一步栽跟头所以我要强调一句SpringBoot 最省心的地方就是自动装配引入 starter-web 后内嵌 Tomcat、Jackson 序列化、基础 MVC 配置全部自动生效我们真正要写的代码其实很少。2.3 数据代理接口与 CORS 配置后端我设计了一个非常精简的 REST 接口GET /api/geo/{adcode}传入 adcode后端拼出 DataV 的完整地址去请求再把响应文本原样返回给前端。为什么原样返回字符串而不是解析成对象因为 Leaflet 前端本来就支持直接接收 GeoJSON 字符串或对象后端做 JSON 解析再序列化完全是多余开销字符串透传反而是性能最好的方式。Service public class GeoBoundaryService { private static final String DATA_SERVER https://geo.datav.aliyun.com/areas_v3/bound/; private static final MapString, String cache new ConcurrentHashMap(); public String getBoundary(String adcode) { return cache.computeIfAbsent(adcode, code - { String url DATA_SERVER code _full.json; try { HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NORMAL) .build(); HttpRequest request HttpRequest.newBuilder(URI.create(url)).GET().build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() ! 200) { throw new RuntimeException(remote data error: response.statusCode()); } return response.body(); } catch (Exception e) { throw new RuntimeException(fetch boundary failed: code, e); } }); } }这里我用的是 JDK 自带的 java.net.http.HttpClient没有额外引入 RestTemplate 或 OkHttp。对于“转发一个请求拿回字符串”这种场景它足够轻量连接超时也有明确控制。Controller 层同样简洁RestController RequestMapping(/api/geo) public class GeoBoundaryController { private final GeoBoundaryService geoBoundaryService; public GeoBoundaryController(GeoBoundaryService geoBoundaryService) { this.geoBoundaryService geoBoundaryService; } GetMapping(value /{adcode}, produces application/json;charsetUTF-8) public ResponseEntityString boundary(PathVariable String adcode) { return ResponseEntity.ok(geoBoundaryService.getBoundary(adcode)); } }注意produces application/json;charsetUTF-8这个细节不显式声明的话Spring 对 String 类型返回值默认可能走 StringHttpMessageConverter历史版本默认编码不是 UTF-8返回的中文名称很容易乱码。这一行能省掉你排查编码问题的大量时间。前后端分离开发时前端跑在 5173 端口后端跑在 8080 端口跨域是躲不掉的。我在项目里用一个全局配置类统一处理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }简单解释一下为什么这么配allowedOrigins 我这里开发环境直接放开不开启 allowCredentials因为浏览器端不需要携带 CookiemaxAge 设置 3600 秒让预检请求的结果可以缓存一小时减少 OPTIONS 请求的浪费。2.4 缓存与传输层优化DataV 的100000_full.json全国数据大概接近 1MB省级数据小一些也有几百 KB。每次进页面都让前端重新请求、后端重新从远程拉一遍既慢又没礼貌。我在 Service 里直接用 ConcurrentHashMap 做了一层内存缓存adcode 维度第一次请求拉远程之后全部走缓存。生产环境如果想更专业可以换成 Spring Cache 注解配合 Caffeine但 demo 阶段这个手动缓存完全够用。传输层还有一个容易被忽略的优化内嵌 Tomcat 的 GZIP 压缩。GeoJSON 是纯文本压缩率通常能达到 70% 以上。在 application.yml 里加上这段server: port: 8080 compression: enabled: true min-response-size: 1024 mime-types: application/json,text/html,text/plain前端浏览器请求时会自动带上 Accept-Encoding: gzipTomcat 压缩后响应体明显变小地图首次加载速度提升肉眼可见。这个配置对所有文本类接口都有效不是只服务于地图场景。3. Leaflet 掩膜图层实现全场硬核3.1 地图初始化与前端工程骨架前端核心就是 Leaflet。如果只是做技术验证不需要引入 Vue 或 React一个 HTML 加一个 app.js 就够了后面的逻辑都是地图 API 层面的东西迁移到任何工程化框架里只是换一层封装。我画了一个非常干净的基础页面全屏地图容器右上角放一个语言切换下拉框和一个重置按钮地图底图用 OpenStreetMap 的公开瓦片方便演示。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title行政区划地图掩膜实战/title link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / style html, body, #map { height: 100%; margin: 0; } .controls { position: absolute; top: 12px; left: 12px; z-index: 1000; background: rgba(255, 255, 255, 0.92); padding: 10px 14px; border-radius: 6px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); } .mask-canvas { position: absolute; top: 0; left: 0; pointer-events: none; z-index: 300; } .label-div { background: transparent; border: none; color: #333; font-size: 13px; text-shadow: 0 0 4px #fff; white-space: nowrap; } /style /head body div idmap/div div classcontrols select idlangSwitch option valuecn中文标注/option option valueen英文标注/option /select button idresetBtn重置视图/button /div script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script script srcapp.js/script /body /html这里有点必须提前说清楚.mask-canvas的pointer-events: none不是可有可无的。Canvas 遮罩是铺满整个地图容器的如果不把这个属性关掉鼠标点击全被 canvas 拦截下面的省界多边形永远接收不到 click 事件整个交互直接失灵。地图初始化和底图加载很直白const map L.map(map, { zoomControl: true }).setView([35.8, 104.0], 4); L.tileLayer(https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png, { maxZoom: 18 }).addTo(map);全国视野的中心我设成 [35.8, 104.0]这是中国几何中心附近缩放到 4 级刚好看到完整国界。实际项目里你换高德、天地图或者其他企业瓦片服务完全是同一套逻辑只是 tileLayer 的 URL 换掉。3.2 自定义 Canvas 掩膜图层evenodd 填充规则与实现现在进入正题掩膜图层。前面说过最终方案是自定义 Canvas 图层用 Path2D 同时放入外框矩形和目标行政区轮廓再用 evenodd 规则填充。先讲下 evenodd 到底是怎么回事。Canvas 的 fill 方法除了默认的 nonzero 规则外还支持 evenodd。它判断一个点是否在填充区域内的方式是“从这个点向外画一条射线统计射线与所有子路径的交点个数奇数则在填充区偶数则不在”。当一个覆盖整个屏幕的矩形路径和行政区的闭合轮廓放在同一个 Path2D 里时行政区内部的点会被矩形路径和行政区轮廓各穿过一次交点数为偶数因此不被填充行政区外部的点只被矩形路径穿过一次交点数为奇数被填充。一个恰到好处的洞就这样出来了。这个思路一旦理解代码就非常顺理成章。我继承 Leaflet 的 L.Layer 来写一个 MaskLayerconst MaskLayer L.Layer.extend({ onAdd: function (map) { this._map map; this._canvas L.DomUtil.create(canvas, mask-canvas); map.getPanes().overlayPane.appendChild(this._canvas); map.on(move zoom viewreset, this._redraw, this); this._redraw(); }, onRemove: function (map) { map.off(move zoom viewreset, this._redraw, this); L.DomUtil.remove(this._canvas); }, setTarget: function (feature) { this._target feature || null; this._redraw(); }, _redraw: function () { const map this._map; const canvas this._canvas; const size map.getSize(); const dpr window.devicePixelRatio || 1; // 处理高分屏避免边界发虚 canvas.width size.x * dpr; canvas.height size.y * dpr; canvas.style.width size.x px; canvas.style.height size.y px; const ctx canvas.getContext(2d); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, size.x, size.y); if (!this._target) return; const bounds map.getBounds(); const nw map.latLngToContainerPoint(bounds.getNorthWest()); const se map.latLngToContainerPoint(bounds.getSouthEast()); // 同一个 Path2D外框矩形 行政区划轮廓 const maskPath new Path2D(); maskPath.rect(nw.x, nw.y, se.x - nw.x, se.y - nw.y); const geometry this._target.geometry; const rings []; if (geometry.type Polygon) { rings.push(...geometry.coordinates); } else if (geometry.type MultiPolygon) { geometry.coordinates.forEach(poly rings.push(...poly)); } rings.forEach(ring { ring.forEach((coord, idx) { const pt map.latLngToContainerPoint([coord[1], coord[0]]); if (idx 0) { maskPath.moveTo(pt.x, pt.y); } else { maskPath.lineTo(pt.x, pt.y); } }); maskPath.closePath(); }); ctx.fillStyle rgba(0, 20, 45, 0.45); ctx.fill(maskPath, evenodd); } });这段代码有四个关键点缺一个效果就崩第一map.getPanes().overlayPane是 Leaflet 内部专门放覆盖物的容器canvas 加到这里层级最合适。第二latLngToContainerPoint是把经纬度转成当前屏幕像素坐标的核心 API每帧重绘都要重新计算因为用户拖一下地图、滚一下滚轮这个映射就变了。第三GeoJSON 的坐标是 [经度, 纬度]Leaflet 的 API 接受的是 [纬度, 经度]这里必须换位写反了边界会飞到海里去。第四外框矩形用视野边界来画比用经纬度硬编码一个全球矩形更稳因为不管用户拖到哪里、缩放到多少级矩形永远能覆盖整个画面。掩膜图层加进地图的方式就是一行const maskLayer new MaskLayer().addTo(map);没有选中任何区域时_target为空直接返回空白画布不影响正常地图浏览。3.3 目标区域高亮与子级边界联动掩膜只是第一步客户还要看到“全国变暗、全省变亮”的对比效果。我在掩膜图层之外又加了一个 L.geoJSON 高亮层用来给目标省填充亮色和描边。注意这个高亮层用的是 Leaflet 自带的矢量渲染不需要走 Canvas因为它只画一个省性能没有压力。点击省级边界时交互逻辑是这样一个完整链路点击省份 - 设置掩膜目标 - 高亮该省 - 动态请求该省的市级数据并渲染子级边界。let currentAdcode null; async function selectRegion(feature) { const adcode feature.properties.adcode; if (currentAdcode adcode) { resetView(); return; } currentAdcode adcode; maskLayer.setTarget(feature); highlightLayer.clearLayers(); highlightLayer.addData(feature); const resp await fetch(/api/geo/${adcode}); const childCollection await resp.json(); childrenLayer.clearLayers(); childrenLayer.addData(childCollection); childrenLayer.eachLayer(layer { if (layer.feature.properties.adcode adcode) { childrenLayer.removeLayer(layer); } }); map.flyTo([feature.properties.centroid[1], feature.properties.centroid[0]], 6); }这里有一个很典型的坑请求山东省的370000_full.json返回的 features 里不仅包含山东省自身还包含它下辖的 16 个地级市。如果你不把山东省自身这条数据过滤掉它的边界会被重复画两次线宽和颜色会叠加得特别脏。所以我每次把子级数据加入图层后会把和当前 adcode 相同的那一条移除掉。省级边界底图本身也是一个 L.geoJSON 图层加载全国数据后给每个省挂上 click 事件let boundaryLayer null; async function loadProvinces() { const resp await fetch(/api/geo/100000_full); const collection await resp.json(); boundaryLayer L.geoJSON(collection, { style: { color: #a0a0a0, weight: 1, fillColor: #ffffff, fillOpacity: 0.02 }, onEachFeature: (feature, layer) { layer.on(click, () selectRegion(feature)); } }).addTo(map); buildLabels(collection.features, cn); }高亮层的样式我单独控制让选中的省有一个明显的光晕感const highlightLayer L.geoJSON(null, { style: { color: #1890ff, weight: 2, fillColor: #1890ff, fillOpacity: 0.15 } }).addTo(map);配合掩膜的深色遮罩选中省份的辨识度非常高。你还可以在掩膜 Path2D 上对镂空边缘做一次 stroke用亮色勾出行政区的轮廓线——就是在ctx.fill(maskPath, evenodd)之后加一段ctx.stroke(maskPath)。不过要注意这里 stroke 的 maskPath 同时包含矩形外框和行政区轮廓描出来的边除了目标省边界还有一整个矩形框是否接受这视觉效果要看你的审美我一般在实现里会把矩形外框的描边去掉只对行政区轮廓单独构建一个 Path 再描边这样边缘更干净。3.4 中英文标注切换实战行政区划地图十有八九要支持中英文切换特别是政务大屏对外展示的时候。我的做法是基于 Leaflet 的 divIcon 做标注点而不是把文字画在掩膜 Canvas 上。原因很简单Canvas 文字一旦定死了想切换语言必须整帧重绘而且缩放时中文和英文长度不一样排版会乱用 divIcon 是 DOM 元素样式灵活切换语言只需要更换 HTML 内容Leaflet 会自动帮我们管理图层。先维护一份 adcode 到中英文名称的字典以常用省级行政区为例const labelDict { 100000: { cn: 中国, en: China }, 110000: { cn: 北京市, en: Beijing }, 120000: { cn: 天津市, en: Tianjin }, 370000: { cn: 山东省, en: Shandong }, 370100: { cn: 济南市, en: Jinan } };然后创建两个图层组一个放中文标注一个放英文标注const cnLayer L.layerGroup().addTo(map); const enLayer L.layerGroup(); function buildLabels(features, lang) { const layer lang cn ? cnLayer : enLayer; layer.clearLayers(); features.forEach(f { const props f.properties; const item labelDict[props.adcode]; const text item ? item[lang] : props.name; L.marker([props.center[1], props.center[0]], { icon: L.divIcon({ className: label-div, html: span${text}/span, iconSize: [80, 20] }) }).addTo(layer); }); }切换触发逻辑就是监听下拉框的值document.getElementById(langSwitch).addEventListener(change, e { const lang e.target.value; if (lang en) { map.removeLayer(cnLayer); enLayer.addTo(map); } else { map.removeLayer(enLayer); cnLayer.addTo(map); } });因为图层组内部维护了所有 marker整体移除和添加不会影响其他图层这个方案在工程上非常干净。实际项目里如果地名数量巨大不建议在前端写死字典更好的做法是把多语言名称表存在后端数据库前端遍历 properties.adcode 去接口取但核心原理一模一样。重置按钮的代码也顺手写一下document.getElementById(resetBtn).addEventListener(click, resetView); function resetView() { currentAdcode null; maskLayer.setTarget(null); highlightLayer.clearLayers(); childrenLayer.clearLayers(); map.flyTo([35.8, 104.0], 4); }4. 实操中遇到的坑与排查记录4.1 坐标偏移问题这是地图开发的老大难。DataV 提供的 GeoJSON 坐标是 WGS84 坐标系而 OpenStreetMap 瓦片使用的也是 WGS84 标准两人能对上所以我的 demo 里一切正常。但如果你把底图换成高德或者百度就要特别注意了高德用的是 GCJ-02 火星坐标系百度更是有自己的一套 BD-09行政区划边界叠加上去后会有明显的偏移放大到市级视角时偏差能到几百米。排查方法很直观在 Leaflet 里把边界图层和底图同时显示放大到最大级别看省界线是不是沿着道路、河流这些真实地物走。如果发现偏移两个解决办法一是在后端统一做坐标转换二是换一套和高德底图同样坐标系的边界数据。千万别在前端拿两个不同坐标系的数据硬叠。4.2 SpringBoot 中文乱码和跨域问题前文提到过produces application/json;charsetUTF-8这里强调下它多重要SpringBoot 返回 String 时会根据 StringHttpMessageConverter 处理不同的 Spring 版本默认字符集不一样一旦走了 ISO-8859-1Leaflget 不管前端怎么设置 UTF-8 meta地名照样是问号。我排查这个问题时一开始看前端代码看了半天后来用 curl 直接请求接口才发现是后端响应头里 Content-Type 没带 charset。跨域配置里也有个隐蔽问题如果你在 CORS 配置里同时设置了allowCredentials(true)和allowedOrigins(*)部分浏览器会直接拒绝请求因为白名单和携带凭证不能同时生效。我的项目里前端不需要用 Cookie所以干脆不开启 allowCredentials开发阶段用allowedOrigins(*)最省事。如果生产环境迫不得已要带凭证必须把 origin 明确写出来不能用星号。4.3 高分屏模糊与大数据量卡顿掩膜 Canvas 第一次在地图上画出来时我发现在 Retina 屏上边界锯齿特别明显这是因为 CSS 像素和设备物理像素不是一一对应的。解决方式就是前面代码里的 devicePixelRatio 适配把 Canvas 的实际宽高乘上 dpr再用ctx.setTransform(dpr, 0, 0, dpr, 0, 0)放大绘制坐标系。这一步不做界面上所有线条都是糊的严重影响质感。大数据量卡顿出现在两个地方一是后端每次返回的省级数据量大二是掩膜重绘时每次都要把所有坐标点从经纬度转成像素坐标。针对前者GZIP 压缩加上接口缓存基本能解决针对后者我发现无论如何都不要在缩放级别很低的时候把几千个点全部投到屏幕上可以做一个简单的抽稀相邻两个像素点之间的距离小于 1 或 2 个像素时就跳过当前点只保留关键转折点省界形状完全不受影响绘制耗能却大幅下降。注意最后闭合点一定要保留否则多边形会缺一个角。4.4 版本兼容与其他印象深刻的坑SpringBoot 3.x 要求 JDK 17 起步这个版本兼容问题劝退了不少照搬网上教程的新手。如果你打开项目发现编译时报 Unsupported class file major version不要怀疑是自己的代码问题去看看 JDK 版本是不是低于 17。同理Leaflet 我用的是 1.9.4如果你用了老版本 1.6 之类的部分 CSS 类名和事件行为有差异掩膜 Canvas 的 overlayPane 获取方式也可能不同。再说一个和 Leaflet 周边生态有关的冷门问题如果你在项目里用了地图旋转插件比如 leaflet-rotate掩膜 Canvas 很可能对不上底图。原因很简单旋转是通过 CSS transform 实现的底图转了一层而latLngToContainerPoint计算出来的坐标并没有同步感知这个旋转矩阵。我实测下来要么放弃 Canvas 方案改用 SVG要么给 Canvas 挂上和底图相同的旋转角度但后者要处理的边界情况很多。所以除非需求确实明确必须旋转否则我通常不建议在做掩膜功能的页面上启用地图旋转。后端部署这里也有个小提醒如果你用 IDEA 直接跑 SpringBoot 没问题但打成 jar 包部署后发现接口 404先检查是不是 context-path 或端口配置被 application.yml 里的环境变量覆盖了。实在要看 jar 里装的什么可以用反编译工具拆开确认下 resource 目录但正常开发流程里一般用不上。写在最后的一点体会这套基于 SpringBoot Leaflet 的行政区划掩膜方案我在多个可视化项目里验证过稳定性是没问题的。个人感受最深的一点是掩膜效果看起来像个小技巧但真正牵扯到的点比想象中多得多——数据源的组织方式、后端代理与缓存、坐标系的统一、Canvas 的渲染细节、高分屏适配、事件联动任何一个环节没处理好最终效果都会露馅。把 evenodd 这条核心思路吃透以后你会发现不只是省级掩膜街道级、园区级、网格级的掩膜本质上全是同一套逻辑换一个面数据、改一条 click 事件就完事了。希望这篇文章能帮你把这块硬骨头啃下来少走我走过的弯路。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

2023年上海中心城区洗车店创业成本与盈利模式分析 2026/10/1 21:00:14

2023年上海中心城区洗车店创业成本与盈利模式分析

开篇前言随着汽车保有量的持续增长,上海作为中国最大的城市之一,其洗车服务市场需求日益旺盛。然而,在这样一个竞争激烈的市场环境中,如何选择合适的运营模式以降低成本、提高盈利能力成为了创业者关注的核心问题。本文将从行业现…

阅读更多 →
降AI率工具推荐:软件工程硕士论文AIGC超标4.8元亲测知网达标完整方案2026 2026/10/1 21:00:14

降AI率工具推荐:软件工程硕士论文AIGC超标4.8元亲测知网达标完整方案2026

降AI率工具推荐:软件工程硕士论文AIGC超标4.8元亲测知网达标完整方案2026 直接给结论:嘎嘎降AI(www.aigcleaner.com),降AI率从74%降到4.9%一次过。 软件工程硕士论文降AI率工具怎么选、怎么用,这篇写清楚…

阅读更多 →
力扣139 单词拆分:动态规划状态转移与代码实现全拆解 2026/10/1 21:00:13

力扣139 单词拆分:动态规划状态转移与代码实现全拆解

平时刷力扣,很多人一看到字符串题就条件反射想用双指针或者回溯,但遇到"单词拆分"这种题,往往会卡在"我到底该先切哪一刀"的思路上出不来。这题在力扣100热题里排第86,题号是139,属于非常典型的动…

阅读更多 →
【回眸】私人定制旅游路线助手 2026/10/1 21:00:07

【回眸】私人定制旅游路线助手

做旅行规划最让人头疼的,往往不是“去哪里”,而是“怎么把想去的地方串起来”。很多人都有过这样的经历:出发前兴奋地在地图上标记了十几个打卡点,结果到了当地才发现,A 景点和 B 餐厅虽然直线距离不远,但中…

阅读更多 →
深入解析 Remote In Tech 目录中的 Akka 公司档案:从 frontmatter 到站点渲染的完整链路 2026/10/1 21:00:07

深入解析 Remote In Tech 目录中的 Akka 公司档案:从 frontmatter 到站点渲染的完整链路

数据集 【免费下载链接】remote-jobs Source for remoteintech.company — a community-maintained directory of remote-friendly tech companies 项目地址: https://gitcode.com/GitHub_Trending/re/remote-jobs 点击查看 免费下载 导读 本文以远程技术公司目录…

阅读更多 →
五步框架:从零构建高质量AI提示词工程实战指南 2026/10/1 21:00:06

五步框架:从零构建高质量AI提示词工程实战指南

1. 为什么大多数人写提示词都在做无用功我见过太多人把提示词当成“咒语”来用。打开对话框,敲一句“帮我写个方案”,结果不满意,就换一句“请帮我写一个高质量的方案”,还是不满意,再改成“你是一个资深专家&#xff…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉