一、Mapbox简介
Mapbox 是一家提供定制地图服务的公司,它允许开发者和设计师通过其平台创建和部署个性化的地图。Mapbox 提供了一整套地图工具和API,使得用户可以轻松地在网站、移动应用和各种设备上集成地图服务。
以下是Mapbox的一些主要特点:
- 定制化:Mapbox 允许用户根据自己的品牌和设计需求定制地图样式,包括颜色、图标、字体等。
- 数据驱动:Mapbox 支持使用各种数据源,包括开放街道地图(OpenStreetMap)数据,以及其他商业和私有数据。
- 实时更新:Mapbox 提供的地图服务可以实时更新,确保地图信息的准确性和最新性。
- 多平台支持:Mapbox 的API和服务支持多种平台和语言,包括Web、iOS、Android等。
- 交互式地图:Mapbox 支持创建交互式地图,用户可以添加图层、标记、路径、热力图等。
- 位置服务:Mapbox 提供了一套完整的位置服务,包括地理编码、逆地理编码、方向和路由规划等。
- 地图分析:Mapbox 提供地图分析工具,帮助用户理解用户行为和地图数据。
- 社区和生态系统:Mapbox 拥有一个活跃的开发者社区,提供了大量的教程、文档和论坛支持。
- 安全性:Mapbox 提供了安全措施,如访问控制和数据加密,以保护用户数据的安全。
- 可扩展性:Mapbox 的服务设计为可扩展的,可以支持从小规模到大规模的地图应用。
- 企业解决方案:Mapbox 为企业提供定制化的解决方案,满足特定业务需求。
- 集成第三方服务:Mapbox 可以与许多第三方服务和API集成,如天气、交通、社交网络等。
- 地图编辑工具:Mapbox 提供了地图编辑工具,允许用户直接在地图上添加或修改数据。
- 地图SDK:Mapbox 提供了软件开发工具包(SDK),使得开发者可以快速地在自己的应用中集成地图功能。
Mapbox 的服务通常是基于订阅模式的,用户根据自己的使用量和需求选择合适的订阅计划。Mapbox 的服务广泛应用于交通、物流、房地产、旅游、城市规划等多个领域。通过Mapbox,用户可以创建出既美观又功能强大的地图应用。
二、Mapbox添加地图、各数据图层和功能的思路
2.1、添加天地图底图
mapbox导入天地图比较复杂,如下代码所示,配置一个配置项,然后在初始化的时候放到设置底图的位置即可。
// 将天地图作为底图 const vecUrl = // "http://t0.tianditu.gov.cn/vec_w/wmts?tk=yourtoken"; "http://t0.tianditu.gov.cn/vec_w/wmts?tk=yourtoken"; const cvaUrl = // "http://t0.tianditu.gov.cn/cva_w/wmts?tk=yourtoken"; "http://t0.tianditu.gov.cn/cva_w/wmts?tk=yourtoken"; //实例化source对象 var tdtVec = { //类型为栅格瓦片 type: "raster", tiles: [ //请求地址 vecUrl + "&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles", ], crossOrigin: "anonymous", //分辨率 tileSize: 256, }; var tdtCva = { type: "raster", tiles: [ cvaUrl + "&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles", ], tileSize: 256, }; var style = { //设置版本号,一定要设置 version: 8, //添加来源 sources: { tdtVec: tdtVec, tdtCva: tdtCva, }, layers: [ { //图层id,要保证唯一性 id: "tdtVec", //图层类型 type: "raster", //数据源 source: "tdtVec", //图层最小缩放级数 minzoom: 0, //图层最大缩放级数 maxzoom: 17, }, { id: "tdtCva", type: "raster", source: "tdtCva", minzoom: 0, maxzoom: 17, }, ], glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", }; export function loadMap(box) { map = new mapboxgl.Map({ container: box, // 导入设置好的天地图配置 style: style, preserveDrawingBuffer: true, center: [114, 30], zoom: 4, }); }
2.2、Mapbox添加行政区矢量图层
这个很简单,主要是需要行政区边界的geojson,这个一般是用shpfile文件转化为geojson,可以通过这个在线网站实现:mapshaper
代码实现,先完成geojson数据源添加,然后添加一个矢量边界图层就可以了。
export function loadMap(box) { map = new mapboxgl.Map({ container: box, // style: 'mapbox://styles/mapbox/streets-v11', // style: 'mapbox://styles/examples/cjgioozof002u2sr5k7t14dim', style: style, preserveDrawingBuffer: true, center: [114, 30], zoom: 4, }); } export function addGeoJson() { map.on("style.load", () => { // 加载 GeoJSON 数据源 map.addSource("geojsonSource", { type: "geojson", data: CityData, }); // 添加图层来显示行政区划的边界 map.addLayer({ id: "lineLayer", type: "line", source: "geojsonSource", paint: { "line-color": "#FFF", "line-width": 1.5, }, }); }); }
2.3、Mapbox添加分级设色图层
分级设色本质是为了直观的体现不同的等级或者不同的数值:
- 体现不同的等级:根据数值的极差分档,类似于arcgis的重分类,不同档位设置不同的颜色
- 体现不同的数值:根据数值的极差用颜色渐变平滑的展示数值的区别,一目了然可任意两两比较。
这里展示的是体现不同的数值的写法:
export function loadMap(box) { map = new mapboxgl.Map({ container: box, // style: 'mapbox://styles/mapbox/streets-v11', // style: 'mapbox://styles/examples/cjgioozof002u2sr5k7t14dim', style: style, preserveDrawingBuffer: true, center: [114, 30], zoom: 4, }); } // 根据value值上色 export function paintMap() { // 添加图层来上色 map.addLayer({ id: "geojsonLayer", type: "fill", // 根据你的数据类型设置合适的图层类型,比如 'fill'、'circle'、'line' 等 source: "geojsonSource", paint: { "fill-color": [ "match", //在geojson中获取name属性 ["get", "name"], //将geojson中的name属性与cityValueData进行匹配,得到正确的综合得分,并根据colorRanges的情况上色 ...rankingFormatted.reduce((acc, data) => { return [...acc, data.cityName, getColor2(data.score)]; }, []), "#000000", // 默认颜色 ], "fill-opacity": 1, // 填充透明度 }, }); } // 设置颜色范围 // const colorRange = ['#ADD8E6','#00008B']; const colorRange = ["#DBEEF6", "#36869A"]; const colorRangeMin = ["#E2F0D9", "#385723"]; const innovation = ["#fff4f8", "#dc7d61"]; const operation = ["#e0e5e9", "#73a9d7"]; const green = ["#e9f0e8", "#80c67d"]; const share = ["#fff8eb", "#f6bb81"]; const open = ["#eaebff", "#b67ebd"]; // 创建颜色插值函数(综合得分) const colorInterpolate = chroma.scale(colorRange).domain([37, 90]); // 创建颜色插值函数(单指标得分) const colorInterpolateMin = chroma.scale(colorRangeMin).domain([1.5, 20]); const colorInnovation = chroma.scale(innovation).domain([4.1, 17.4]); const colorOperation = chroma.scale(operation).domain([9.4, 18.7]); const colorGreen = chroma.scale(green).domain([9.2, 18]); const colorOpen = chroma.scale(open).domain([1.6, 20]); const colorShare = chroma.scale(share).domain([7.5, 20]); // 根据城市值获取对应颜色 function getColor2(value, type) { if (value > 20) { return colorInterpolate(value).hex(); } else { switch (type) { case 0: return colorInnovation(value).hex(); case 1: return colorOperation(value).hex(); case 2: return colorGreen(value).hex(); case 3: return colorShare(value).hex(); case 4: return colorOpen(value).hex(); default: break; } // return colorInterpolateMin(value).hex(); } }
2.4、Mapbox添加文本标记图层
底图嘛,只有矢量边界不够直观,底图信息又会被颜色图层盖住,所以需要在最上方添加文本注记图层,当然也可以添加一些别的文本内容,标记等都可以。
这里有一个额外引入的数据源,是一个点shpfile转化的geojson,这个点是用来规定显示文本注记的位置的,也可以直接在原先面数据源的基础上使用文本注记,那么文本注记会直接显示在每一个闭合曲线(拓扑展现就是一个面)上显示。
export function loadMap(box) { map = new mapboxgl.Map({ container: box, // style: 'mapbox://styles/mapbox/streets-v11', // style: 'mapbox://styles/examples/cjgioozof002u2sr5k7t14dim', style: style, preserveDrawingBuffer: true, center: [114, 30], zoom: 4, }); } export function addGeoJson() { map.on("style.load", () => { map.addSource("pointGeojsonSource", { type: "geojson", data: cityPoint, }); // 添加symbol图层以显示文本 map.addLayer({ id: "pointLabel", type: "symbol", source: "pointGeojsonSource", layout: { "text-field": ["get", "name"], // 使用get表达式来获取"title"属性 "text-size": 14, "text-variable-anchor": ["top", "bottom", "left", "right"], "text-radial-offset": 0.5, "text-justify": "auto", }, paint: { "text-color": "#000", // 文本颜色 "text-halo-color": "#FFF", // 文本描边颜色 "text-halo-width": 1, // 文本描边宽度 }, }); }); }
2.5、Mapbox自定义鼠标悬浮框
鼠标悬浮框主要是增加地图图层的互动效果,用于展示更多的信息。
// 在鼠标移动到地图上显示信息 let popup = null; function showPopup(e, value) { const features = e.features; if (features.length > 0) { map.getCanvas().style.cursor = "pointer"; const cityName = features[0].properties.name; // 城市名称 const cityValue = getCityValue(cityName, value); // 获取对应的值 if (popup) { popup.remove(); } // 在页面上显示浮动信息 popup = new mapboxgl.Popup() .setLngLat(e.lngLat) .setHTML(`<strong>${cityName}</strong><br>评分为: ${cityValue}`) .addTo(map); } else { map.getCanvas().style.cursor = ""; } } // 在鼠标移出时移除浮动信息 function removePopup() { if (popup) { popup.remove(); } map.getCanvas().style.cursor = ""; } // 绑定地图交互事件,鼠标悬浮和移出事件 function bindMapInteractions(value) { map.off("mousemove", "geojsonLayer", showPopup); map.on("mousemove", "geojsonLayer", (e) => showPopup(e, value)); map.off("mouseout", "geojsonLayer", removePopup); map.on("mouseout", "geojsonLayer", removePopup); }
三、最终效果及代码展示
3.1、最终效果
包括天地图底图,行政区矢量边界图层,分层设色图层,文本标记图层,还有鼠标悬浮框(不包括图例)在内的综合效果。
编辑
3.2、完整代码展示:
3.2.1、map.vue(map所在的组件):
<template> <div class="mapboxBorder"> <div id="mapbox" style="width: 100%; height: 100%; border-radius: 18px" ></div> </div> </template> <script setup> import { map, loadMap, addGeoJson, updateMap } from "@/utils/mapbox.js"; import { onMounted, ref, watch } from "vue"; const initMapbox = () => { loadMap("mapbox"); map.setCenter([119.14, 31.22]); //修改地图中心点 map.setZoom(6.4); //设置缩放级别 }; onMounted(() => { //挂载mapbox initMapbox(); //添加矢量图层 addGeoJson(); }); // ...map组件中的其他事件内容 </script>
3.2.2、mapbox.js:
import mapboxgl from "mapbox-gl"; import "mapbox-gl/dist/mapbox-gl.css"; // import MapboxLanguage from "@mapbox/mapbox-gl-language"; import CityData from "@/assets/json/standardCityBoundary.json"; import ranking from "@/assets/json/scoreDetail.json"; import { scoreFormat } from "@/utils/format.ts"; import cityPoint from "@/assets/json/cityCenterPoint.json"; const rankingFormatted = scoreFormat(ranking); mapboxgl.accessToken = "yourtoken"; //去mapbox官⽹申请 const colorRanges = [ { min: 0, max: 45, color: "#E31A1C" }, { min: 45, max: 55, color: "#FEB24C" }, { min: 55, max: 70, color: "#FFEDA0" }, { min: 70, max: 100, color: "#90EE90" }, ]; const colorRangesSingle = [ { min: 0, max: 5, color: "#E31A1C" }, { min: 5, max: 10, color: "#FEB24C" }, { min: 10, max: 15, color: "#FFEDA0" }, { min: 15, max: 20, color: "#90EE90" }, ]; // 将天地图作为底图 const vecUrl = // "http://t0.tianditu.gov.cn/vec_w/wmts?tk=yourtoken"; "http://t0.tianditu.gov.cn/vec_w/wmts?tk=yourtoken"; const cvaUrl = // "http://t0.tianditu.gov.cn/cva_w/wmts?tk=yourtoken"; "http://t0.tianditu.gov.cn/cva_w/wmts?tk=yourtoken"; //实例化source对象 var tdtVec = { //类型为栅格瓦片 type: "raster", tiles: [ //请求地址 vecUrl + "&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles", ], crossOrigin: "anonymous", //分辨率 tileSize: 256, }; var tdtCva = { type: "raster", tiles: [ cvaUrl + "&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=tiles", ], tileSize: 256, }; var style = { //设置版本号,一定要设置 version: 8, //添加来源 sources: { tdtVec: tdtVec, tdtCva: tdtCva, }, layers: [ { //图层id,要保证唯一性 id: "tdtVec", //图层类型 type: "raster", //数据源 source: "tdtVec", //图层最小缩放级数 minzoom: 0, //图层最大缩放级数 maxzoom: 17, }, { id: "tdtCva", type: "raster", source: "tdtCva", minzoom: 0, maxzoom: 17, }, ], glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", }; export let map = null; // 导出 map 对象 export function loadMap(box) { map = new mapboxgl.Map({ container: box, // style: 'mapbox://styles/mapbox/streets-v11', // style: 'mapbox://styles/examples/cjgioozof002u2sr5k7t14dim', style: style, preserveDrawingBuffer: true, center: [114, 30], zoom: 4, }); // 设置汉化 // map.addControl(new MapboxLanguage({ // defaultLanguage: 'zh-Hans' // })); } export function addGeoJson() { map.on("style.load", () => { // 加载 GeoJSON 数据源 map.addSource("geojsonSource", { type: "geojson", data: CityData, }); map.addSource("pointGeojsonSource", { // 注意:这里使用的是不同的ID type: "geojson", data: cityPoint, }); //初始化上色 paintMap(); //绑定地图事件 bindMapInteractions(); // 添加图层来显示行政区划的边界 map.addLayer({ id: "lineLayer", type: "line", source: "geojsonSource", paint: { "line-color": "#FFF", "line-width": 1.5, }, }); // 添加symbol图层以显示文本 map.addLayer({ id: "pointLabel", type: "symbol", source: "pointGeojsonSource", layout: { "text-field": ["get", "name"], // 使用get表达式来获取"title"属性 "text-size": 14, "text-variable-anchor": ["top", "bottom", "left", "right"], "text-radial-offset": 0.5, "text-justify": "auto", }, paint: { "text-color": "#000", // 文本颜色 "text-halo-color": "#FFF", // 文本描边颜色 "text-halo-width": 1, // 文本描边宽度 }, }); // 添加地级市名称的文本图层 // map.addLayer({ // id: "cityNameLayer", // type: "symbol", // source: "geojsonSource", // layout: { // "text-field": [ // "match", // ["get", "is_island"], // "true", // "", // 如果是群岛城市,不显示名字 // ["get", "name"], // 如果不是群岛城市,显示名字 // ], // "text-size": 14, // "text-variable-anchor": ["top", "bottom", "left", "right"], // "text-radial-offset": 0.5, // "text-justify": "auto", // "text-allow-overlap": false, // 不允许文本标签重叠 // }, // paint: { // "text-color": "#000", // 文本颜色 // "text-halo-color": "#FFF", // 文本描边颜色 // "text-halo-width": 1, // 文本描边宽度 // }, // }); }); } // 根据value值上色 export function paintMap() { // 添加图层来上色 map.addLayer({ id: "geojsonLayer", type: "fill", // 根据你的数据类型设置合适的图层类型,比如 'fill'、'circle'、'line' 等 source: "geojsonSource", paint: { "fill-color": [ "match", //在geojson中获取name属性 ["get", "name"], //将geojson中的name属性与cityValueData进行匹配,得到正确的综合得分,并根据colorRanges的情况上色 ...rankingFormatted.reduce((acc, data) => { return [...acc, data.cityName, getColor2(data.score)]; }, []), "#000000", // 默认颜色 ], "fill-opacity": 1, // 填充透明度 }, }); } // 切换数据更新地图上色 export function updateMap(value) { // 根据新的 value 更新绘制属性 let propertiesSelect = ""; switch (parseInt(value)) { case 0: propertiesSelect = "创新发展"; break; case 1: propertiesSelect = "协调发展"; break; case 2: propertiesSelect = "绿色发展"; break; case 3: propertiesSelect = "开放发展"; break; case 4: propertiesSelect = "共享发展"; break; case 5: propertiesSelect = "score"; break; default: break; } map.setPaintProperty("geojsonLayer", "fill-color", [ "match", ["get", "name"], ...rankingFormatted.reduce((acc, data) => { return [ ...acc, data.cityName, getColor2(data[propertiesSelect], parseInt(value)), ]; }, []), "#000000", // 默认颜色 ]); bindMapInteractions(value); } // 设置颜色范围 // const colorRange = ['#ADD8E6','#00008B']; const colorRange = ["#DBEEF6", "#36869A"]; const colorRangeMin = ["#E2F0D9", "#385723"]; const innovation = ["#fff4f8", "#dc7d61"]; const operation = ["#e0e5e9", "#73a9d7"]; const green = ["#e9f0e8", "#80c67d"]; const share = ["#fff8eb", "#f6bb81"]; const open = ["#eaebff", "#b67ebd"]; // 创建颜色插值函数(综合得分) const colorInterpolate = chroma.scale(colorRange).domain([37, 90]); // 创建颜色插值函数(单指标得分) const colorInterpolateMin = chroma.scale(colorRangeMin).domain([1.5, 20]); const colorInnovation = chroma.scale(innovation).domain([4.1, 17.4]); const colorOperation = chroma.scale(operation).domain([9.4, 18.7]); const colorGreen = chroma.scale(green).domain([9.2, 18]); const colorOpen = chroma.scale(open).domain([1.6, 20]); const colorShare = chroma.scale(share).domain([7.5, 20]); // 根据城市值获取对应颜色 function getColor2(value, type) { if (value > 20) { return colorInterpolate(value).hex(); } else { switch (type) { case 0: return colorInnovation(value).hex(); case 1: return colorOperation(value).hex(); case 2: return colorGreen(value).hex(); case 3: return colorShare(value).hex(); case 4: return colorOpen(value).hex(); default: break; } // return colorInterpolateMin(value).hex(); } } // 悬浮地图上时,获取该城市的值 function getCityValue(cityName, value) { const cityData = rankingFormatted.find((data) => data.cityName === cityName); switch (parseInt(value)) { case 0: return cityData ? cityData["创新发展"] : "N/A"; case 1: return cityData ? cityData["协调发展"] : "N/A"; case 2: return cityData ? cityData["绿色发展"] : "N/A"; case 3: return cityData ? cityData["开放发展"] : "N/A"; case 4: return cityData ? cityData["共享发展"] : "N/A"; case 5: return cityData ? cityData.score : "N/A"; default: return cityData ? cityData.score : "N/A"; } } // 在鼠标移动到地图上显示信息 let popup = null; function showPopup(e, value) { const features = e.features; if (features.length > 0) { map.getCanvas().style.cursor = "pointer"; const cityName = features[0].properties.name; // 城市名称 const cityValue = getCityValue(cityName, value); // 获取对应的值 if (popup) { popup.remove(); } // 在页面上显示浮动信息 popup = new mapboxgl.Popup() .setLngLat(e.lngLat) .setHTML(`<strong>${cityName}</strong><br>评分为: ${cityValue}`) .addTo(map); } else { map.getCanvas().style.cursor = ""; } } // 在鼠标移出时移除浮动信息 function removePopup() { if (popup) { popup.remove(); } map.getCanvas().style.cursor = ""; } // 绑定地图交互事件,鼠标悬浮和移出事件 function bindMapInteractions(value) { map.off("mousemove", "geojsonLayer", showPopup); map.on("mousemove", "geojsonLayer", (e) => showPopup(e, value)); map.off("mouseout", "geojsonLayer", removePopup); map.on("mouseout", "geojsonLayer", removePopup); }
四、总结
Mapbox的中国分部好像在2021年左右就退出中国了,官方文档的汉化工作也戛然而止,相关的社区建设也相当欠缺,内容比较混乱,最离谱的是mapbox官方底图库中的中国地图基本都是错的,天地图引入又麻烦......
恰好我最近有一个基础的mapbox应用需求,就做了一些整理和探索,分享给大家。
更多前端好文:各种前端问题的技巧和解决方案
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~