什么是 mapbox

近期公司有一个在地图上展示大量数据的项目。需要可以在国内访问国外的地图,同时可以累加开发

最后我们找到了一个相当靠谱但是也比较复杂的地图库,就是 mapbox

就记录一下自己看过和用过的吧

mapbox 中国

mapbox gl js

mapbox 文档

mapbox 中国也有文档,但是不是最新的,不过可以参考

mapbox GL 应该怎么用

假设想搞出来这样一个东西,一个可以操作的地图上,有上亿的数据需要显示,这时候 mapbox gl 就非常合适

前端需要做的是根据mapbox gl js的文档完成地图上的一些交互功能,比如地图控件,地图层的控制,地图的缩放移动事件等等。后端需要准备一份数据,毕竟那么多的点点不是随机生成的呀,具体的细节我不太清楚,就只放一份文档在这里。地图的层数据,甚至地图的样式,都是一个链接。前端拿到这个链接之后就可以获取到数据,按照 mapbox 的约定将数据呈现地图里

美国人种分布

先把地图搞出来

上手最快的方法,就是看Examples

正确引入地图的 script 和 link 之后,和绝大多数地图一样,接管一个 html 元素,把地图 new 出来就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<link
href="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.css"
rel="stylesheet"
/>
<script src="https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js"></script>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
<div id="map"></div>
1
2
3
4
5
6
7
8
// 哦对了,还要记得设置accsessToken
mapboxgl.accessToken = "你的accessToken";
const map = new mapboxgl.Map({
container: "map", // container ID
style: "mapbox://styles/mapbox/streets-v11", // style URL
center: [-74.5, 40], // starting position [lng, lat]
zoom: 9, // starting zoom
});

mapbox GL 的一些概念

这关系到怎么去使用 mapbox 的功能

只说一些简单常用的

复杂的拎出来单独写写

Map,也就是 new mapboxgl.Map

主要的作用是初始化地图,得到一个地图的实例

传入参数,确定地图最初的样子

得到这个地图实例之后,就可以操作很多事情了

控件(Controls)

地图的控件,也就是地图的缩放按钮,导航按钮,比例尺之类的东西

这些都很简单啦

1
2
3
4
5
6
7
// 关键方法
// addControl 第一个参数是控件,第二个参数是位置
map.addControl(new mapboxgl.NavigationControl(), "top-left");
// getCanvas 返回地图的canvas元素
const canvas = map.getCanvas();
// getContainer 返回地图的容器
const container = map.getContainer();

地图的限制(Constraints)

主要用来获得一些地图的属性,或者对地图进行一些选项设置

列举几个常用的方法

1
2
3
4
5
6
7
8
9
10
map.getMinZoom();
map.setMinZoom();
map.getMaxZoom();
map.setMaxZoom();
// ----
// map.resize()
// Resize the map when the map container is shown
// after being initially hidden with CSS.
const mapDiv = document.getElementById("map");
if (mapDiv.style.visibility === true) map.resize();

地图的状态(Movement state)

地图是否正在移动,是否正在缩放,是否正在旋转

1
2
3
map.isMoving();
map.isZooming();
map.isRotating();

标记(Markers)

在地图上做一个点标记

1
2
3
4
5
6
const marker = new mapboxgl.Marker({
color: "#fff",
draggable: true, // 能否拖拽
})
.setLngLat([30.5, 50.5]) // 设置marker的位置
.addTo(map); // 把marker放进地图里

选项有好多,还可以设置图片啊偏移啊什么的

地图事件和事件的挂载卸载(events & working with events)

挂在和卸载地图的事件用的是map.on, map.off, map.once

第一个参数是事件名,比如 click,第二个参数是地图的层 id,类似于指定事件的 target,第三个参数是时间触发的函数(listener)

一些常用的事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 缩放结束事件 zoomend
// 根据缩放等级改变一个元素里的内容
map.on('zoomend''layer_id', (e) => {
const zoom = map.getZoom()
const countTip = document.querySelector('.count-tip span')
if(zoom < 4) {
countTip.textContent = '5000'
} else if(zoom >= 4 && zoom < 7) {
countTip.textContent = '1000'
} else if(zoom >= 7 && zoom < 10) {
countTip.textContent = '500'
} else if(zoom >= 10) {
countTip.textContent = '200'
}
})

生命周期(Lifecycle)

地图的生命周期函数,比如加载之后触发一个事件,出错了的事件…

看文档

常用的是 load

1
2
3
4
const map = new mapboxgl.Map({});
map.on("load", () => {
console.log("A load event occurred.");
});

还有 error

1
2
3
4
const map = new mapboxgl.Map({});
map.on("error", () => {
console.log("A error event occurred.");
});

图片(images)

给地图里面添加图标之类的东西,需要先loadImage然后addImage

然后再loadImage的 callback 中继续

loadImage 的第一个参数是图片的链接,或者 base64,第二个参数是回调

addImage 的第一个参数是给图片一个名字,第二个参数是图片,第三个参数是一些选项

The image as an HTMLImageElement , ImageData , ImageBitmap or object with width , height , and data properties with the same format as ImageData

1
2
3
4
5
6
7
8
9
10
11
12
13
// 需要在地图层中加载图片
map.loadImage("path/to/image.png", (error, image) => {
if (error) throw error;
map.addImage("image-name", image);
// map.addSource()
// map.addLayer()
});
// 另一种
const icon = new Image();
icon.src = "path/to/image.png";
icon.onload = () => {
map.addImage("icon-name", icon);
};

使用的时候

1
2
3
4
"layout": {
"icon-image": "college-icon",
// "icon-size": 0.6,
}

地图上的弹窗(popup)

地图上点击某个点打开一个泡泡框是常见的需求,地图提供了一个泡泡框的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const layer_id = "LayerId";
const popup = new mapboxgl.Popup({
closeButton: true,
closeOnClick: true,
maxWidth: "300px",
});
const changePointer = () => {
map.getCanvas().style.cursor = "pointer";
};
const openPopupCallback = (e) => {
// 事件参数里有很多东西,比如这个点的坐标,这个点的数据...
const coordinates = e.features[0].geometry.coordinates.slice();
// 泡泡框出现的方向
// https://docs.mapbox.com/mapbox-gl-js/example/popup-on-click/
while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
}
const data = e.features[0].properties.some_data;
let content = `<a>一段自定义的html,也可以插入${data},弹出框的内容,可以使用css修改样式</a>`;
popup.setLngLat(coordinates).setHTML(content).addTo(map);
};
const closePopupCallback = () => {
map.getCanvas().style.cursor = "";
};
map.on("mouseenter", layer_id, changePointer);
map.on("mouseenter", layer_id, openPopupCallback);
map.on("mouseleave", layer_id, closePopupCallback);

一些复杂的,或者更重要的

  • 数据源和地图层(source & layer)
  • 数据源的 type
  • 表达式(Expressions)

数据源和地图层

在地图上显示各种不同的信息,每个信息都是一个层(layer),地图是由多个层组成的。而层依赖于数据,没有数据源,就无法生成一个层。

数据源(Source)

https://docs.mapbox.com/mapbox-gl-js/api/sources/

https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/

数据源为地图层提供了数据,没有数据,层的显示毫无意义。

addSource 的第一个参数是这个数据源的 id,层通过这个 id,可以使用到数据源

Source 的 type 是 vector 时,url 的值就是后端给的。而 geojson 是一份静态资源

1
2
3
4
5
6
7
8
9
map.addSource("colleges", {
type: "geojson",
data: "/path/to/data.geojson",
// cluster: true,
// clusterMaxZoom: 10,
// clusterRadius: 50,
// type: 'vector',
// url: 'mapbox://username.sourcecode',
});

地图层(Layer)

地图层是依附于地图基底(style)上的,在使用map.setStyle()之后,地图层就会被清空。同时,地图层依赖于数据源。

数据源也可以作为一个选项卸载 addLayer 中。

数据源的 type 是vector时,source-layer是必选参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
map.addLayer({
id: "LayerId",
type: "circle",
// source: {
// type: 'vector',
// url: 'mapbox://username.sourcecode',
// },
minzoom: 4,
maxzoom: 7,
"source-layer": "source-layer-code",
paint: {
"circle-radius": {
base: 1,
stops: [
[4, 1],
[7, 2],
],
},
"circle-color": [
"match",
["get", "type_name"],
"w",
"#41b3eb",
"b",
"#f38b3c",
"o",
"#81d693",
"a",
"#3453a4",
"h",
"#cb3233",
"#f9e33e",
],
},
});

每个层都有一个 id,可以通过这个 id 对层进行控制,比如隐藏掉一个地图层

1
map.setLayerProperty("LayerId", "layer_option", "value");

数据源的 type

目前后端给到的 source 有两种,第一种是GeoJSON,另一种是Vector

在 mapbox 里,不同的 source 有不同的功能,比如 GeoJSON 可以聚合,而 Vector 就不可以

GeoJSON

顾名思义,GeoJSON 是一个关于地理数据的一个 JSON 格式

GeoJson 的文档标准

mapbox 里对关于 GeoJSONSource 的文档

在 mapbox 里,GeoJson 可以使用聚合(cluster)显示的功能,可以看这个例子

GeoJSON 它是可以是一个文件,也可以是 json 字符串,但是读取都在前端。所以无法避免被爬取。如果是比较小的数据,可以用这个格式

Vecter

Vector 是托管在 mapbox 服务器上的,是一个通过算法压缩过的数据格式,可以用来传输大量的位置点。比如这个美国人种分布的散点图例子

Vector 类型不支持聚合,但是可以通过 Layer 的minzoommaxzoom控制显示。

表达式(Expressions)

表达式是 Mapbox 的一个特色了,哇,这个用起来好复杂的,但是做内容匹配很方便

文档在这里

表达式的语法很简单,就是一个 JavaScript 数组,这个数组的每个元素都有特别的意义,数组会返回一个值。其本质是个 JavaScript 函数

1
2
3
4
5
6
7
8
9
// 这里以表达式的rgb方法为例,写一个类似的JavaScript函数
const rgb = (r, g, b) =>
`rgb(${r}, ${g}, ${b})`[
// r g b 是参数,`rgb(${r}, ${g}, ${b})`是返回值 rgb是方法的名字 r g b 三个参数都是数值型的
// 换成表达式这么写
("rgb", number, number, number)
];
// 这个数组会被转化成JavaScript并执行,最后返回一个rgb色值
// 第一个参数是函数名,从 length: 1 的元素开始,后面的元素都是函数的参数 数组最后会return计算结果

表达式可以嵌套,因为表达式返回的是一个值,这个值当然可以作为参数

1
2
3
4
5
6
7
8
// 还以rgb为例,做一个计算过的rgb
// rgb的三个参数是数值,所以嵌套表达式返回的结果应该也是数值
["rgb", 200, ["*", 100, 2], 200][
// 嵌套的第二层表达式的结果是 100 * 2 = 200
// 所以上面的嵌套表达式就是
("rgb", 200, 200, 200)
];
("rgb(200, 200, 200)");

部分表达式可以访问到数据源,匹配出合适的结果,下面的这个paint['circle-color']属性中,使用了 match 表达式,第二个参数从数据集中选择了数据源,之后第三个参数是匹配到的值,第四个参数是遇到这个属性显示的颜色。就好像switch case。而最后一个#f9e33e是一个默认值。就好像switch case default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
'paint': {
'circle-radius': 1.5,
'circle-color': [
'match',
['get', 'ethnicity'],
'w',
'#41b3eb',
'b',
'#f38b3c',
'o',
'#81d693',
'a',
'#3453a4',
'h',
'#cb3233',
'#f9e33e'
]
}

利用表达式,可以做很多奇妙的事情,比如不同的值显示不同的颜色(上面的)。甚至切割加工字符串,过滤掉某些值等等