0%

初识Mapbox

突如其来

2020年2月初,发现公司使用谷歌地图功能的网页和后台功能出现异常。排查过后发现问题在于网络咱解决不了。所以暂时屏蔽了相关功能,着手更换。

听闻业内企业换成了mapbox,决定一试,本文回顾了这个过程。

了解mapbox

mapbox成立于2010年,是一个提供地图相关服务的平台。从他的开发文档中提供的变更记录中我们能够发现mapbox最近正在快速的迭代产品。通常这表明了两个信息:

  1. 发展势头不错。
  2. 产品尚未完善。

技术开发更关心第二条,这次的经历也印证了这个推断。

选择关注的内容

mapbox提供了线上的服务,在此基础上还提供了针对不同平台的开发SDK。

因为我们是java后端的web网站,前端代码修改选择看javascript的在这里,java后端在这里,在Github上不止有这些。

前端js的选择

个人理解目前js部分正在做切换,官方在醒目的位置推荐的是mapbox-gl这个项目,但是在另外一处你能找到另外一个版本,使用到了mapbox.js和Leaflet,在booking的地图中我们也找到了Leaflet的印记。最开始我们因为没有找到后面这个版本(搜索引擎也没搜到),所以径直选用了mapbox-gl。

后端java的选择

如果你是maven项目,添加以下的依赖信息即可:

1
2
3
4
5
<dependency>
<groupId>com.mapbox.mapboxsdk</groupId>
<artifactId>mapbox-sdk-services</artifactId>
<version>4.0.0</version>
</dependency>

项目在上方的github地址中都能找到。

实现细节

地图初始化

在指定的html节点内容创建地图对象,设置了地图的外观、中心点,缩放度量,同时将地图上的地名文字设置为中文,添加必要的操作控件。

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
   let initMap=function(mapContainerId){
// 1.清除html节点容器内的内容
$('#'+mapContainerId).html("");
// 2.构造map对象
mapboxgl.accessToken =mapboxToken.token;
const map = new mapboxgl.Map({
container: mapContainerId,
style: "mapbox://styles/mapbox/streets-v9",
center: [16.367817,48.215319],
zoom: 8
});
//3.设置地图语言为中文
// 1)尚未做设置的情况方可进行设置
if(mapboxgl.getRTLTextPluginStatus()=="unavailable"){
// 英文标注转换为中文
mapboxgl.setRTLTextPlugin(
"https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.1.0/mapbox-gl-rtl-text.js"
);
}
// 2)设置语言
var language = new MapboxLanguage({
defaultLanguage: "zh"
});
map.addControl(language);
//4.添加界面控件
// 1)全屏
map.addControl(new mapboxgl.FullscreenControl());
// 2)地图导航
var nav = new mapboxgl.NavigationControl();
map.addControl(nav);
return map;
}

这里 map的style决定了地图的的贴图外观,还有如下的值备选:

  • mapbox://styles/mapbox/streets-v11
  • mapbox://styles/mapbox/outdoors-v11
  • mapbox://styles/mapbox/light-v10
  • mapbox://styles/mapbox/dark-v10
  • mapbox://styles/mapbox/satellite-v9
  • mapbox://styles/mapbox/satellite-streets-v11
  • mapbox://styles/mapbox/navigation-preview-day-v4
  • mapbox://styles/mapbox/navigation-preview-night-v4
  • mapbox://styles/mapbox/navigation-guidance-day-v4
  • mapbox://styles/mapbox/navigation-guidance-night-v4

根据经纬度在地图上标点并显示

提供标点的坐标和名称信息,在地图上创建对应的标记点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let putCityMarkerOnMap=function(element,map){
var el = document.createElement('div');
el.className = 'cMarker_city';
let m = new mapboxgl.Marker(el, {
offset: [0, -20]
})
.setLngLat(element.center)
.setPopup(new mapboxgl.Popup({
offset: 25
}) // add popups
.setHTML('<div class="cMarkerPop">' + element.text + '</div>'))
.addTo(map);
// 存储创建的标记点
city_marker_list.push(m);
};

上面代码中创建了自定义外观的标记点,这段代码需要和一个名称为cMarker_city的class class配合才可以生效。如果不使用自定的标记点,直接使用new mapboxgl.Marker()就可以了。

另外,标记点上附加了一个点击弹出框。

在最后把创建的标记点存入了一个容器,这样做的目的是为了在需要的时候清除地图上的所有标记点。类似下面这样:

1
2
3
4
   while(city_marker_list.length>0){
let m=city_marker_list.pop();
m.remove();
}

到此,你可以做到标记点的摆放和清除了,然而很可能你看不到他们。因为很可能他们不在地图的视野范围内。当然你可以调用map.setCenter([-74, 38]);设置当前地图的中心点,但是实际情况可能存在多个标记点,而且你还要需要比较恰当的把他们都在地图视野内的显示完全。那么试试下面的方法吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   /**
* 让地图能完整显示所有的坐标点
*/
let setBounds=function(arr,map){
let first = new mapboxgl.LngLat(arr[0].lng, arr[0].lat);
let bounds = new mapboxgl.LngLatBounds(first, first);
if(arr.length>1){
for(var index=1;index<arr.length-1;index++){
let newPoint=new mapboxgl.LngLat(arr[index].lng, arr[index].lat);
bounds.extend(newPoint);
}
}
map.fitBounds(bounds, {
padding: 100// 控制要显示的点距离地图视野有100像素
});
}

这个方法中map.fitBounds是关键,之前都是参数准备。

根据地名显示行程路线

提供的是数个城市名称,需要在地图上根据他们的顺序绘制导航路线。

这个操作需要需要请求导航线路,需要用到另外的js类库:mapbox-sdk-js,这个类库封装了一系列的请求,你可以通过它来设置参数,发送请求看得到结果。详细的列表在这里

初始化服务客户端

1
const  mapboxClient = mapboxSdk({ accessToken: mapboxToken.token });

mapboxSdk就是mapbox-sdk-js。这里传入了账号对应的token字符串,接下来可以根据名称直接获取对应的服务。

PS,如果是引用单个服务的应的js文件,每个服务也可以单独构建。譬如:

1
2
3
4
5
6
7
8
9
10
11
12
13
const mbxGeocoding = require('@mapbox/mapbox-sdk/services/geocoding');
import {
token
} from './Token';

var mapboxGeocodingClient = mbxGeocoding({
accessToken: token
});
mapboxGeocodingClient
.forwardGeocode(
....
)
....

地址转换为坐标

同上一个例子类似,调用geocoding.reverseGeocode方法可以得到地址对应的坐标。

第一步:检索城市的坐标

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
let searchCities=function(cities, callBack) {
cleanCityMarkers();
let promiseArr = cities.map(item => {
return new Promise((resolve, reject) => {
mapboxClient.geocoding
.forwardGeocode({
query: item.enName,
countries: [item.countryCode],
autocomplete: false,
limit: 1,
language:['zh-Hans','en// 指定返回的地址名称,包括中文和英文,影响结果中feature.text的值
})
.send()
.then(function(response) {
if (
response &&
response.body &&
response.body.features &&
response.body.features.length
) {
var feature = response.body.features[0];
resolve(
{ center:feature.center,
text:feature.text
});
} else {
resolve(null);
}
});
});
});
Promise.all(promiseArr).then(function(pCenters) {
callBack(pCenters);
});
};

这里的参数queryStr类似:London,GB这样的格式。如果有多个地点需要查询,这个接口也支持,但是需要对你的账号付费才行,价格参看这里。免费使用的例子就如上面代码一样。上面的例子中,执行完所有的查询后执行传入的回调函数,当然也可以返回Promise。

访问mapbox的此类服务,你需要在sdk文档服务参数两份文档之间来回查看。目前sdk并未支持接口所有的参数。

第二步:在地图上绘制行程路线

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
let getDirections=function(centers,map){
let cityCenters = centers.map(item => {
return {
coordinates: item.center,
}
});
mapboxClient.directions.getDirections({
// profile: 'driving-traffic',// 这个选项只支持最多三个点
profile: 'driving',
waypoints: cityCenters,
geometries: "geojson",
})
.send()
.then(response => {
var directions = response.body;
var data = directions.routes[0];
var route = data.geometry.coordinates;
var geojson = {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': route
}
};
// if the route already exists on the map, we'll reset it using setData
if (map.getSource('route')) {
map.getSource('route').setData(geojson);
}
// otherwise, we'll make a new request
else {
map.addLayer({
'id': 'route',
'type': 'line',
'source': {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {},
'geometry': {
'type': 'LineString',
'coordinates': geojson
}
}
},
'layout': {
'line-join': 'round',
'line-cap': 'round'
},
'paint': {
'line-color': 'blue',
'line-width': 5,
'line-opacity': 0.75
}
});
}
if (map.getSource('route')) {
map.getSource('route').setData(geojson);
var coordinates = geojson.geometry.coordinates;
var bounds = coordinates.reduce(function(bounds, coord) {
/* reduce语法:array1.reduce(callbackfn[, initialValue]) callbackfn语法:function callbackfn(previousValue, currentValue, currentIndex, array1),这里整个语句的含义是以坐标0为初始值,边界逐渐扩展边界到最后一个坐标 */
return bounds.extend(coord);
/* extend (obj):包含给定的经纬度或者经纬度边界来扩展区域边界 */
}, new mapboxgl.LngLatBounds(coordinates[0], coordinates[0])); /*new LngLatBounds(sw: [LngLatLike], ne: [LngLatLike]):创建LngLatBounds的构造器,LngLatBounds对象表示一个地理上有界限的区域,使用西南和东北的点的经纬坐标表示 */
map.fitBounds(bounds, {
/*fitBounds(bounds,[options],[eventData]):移动缩放地图来将某个可视化区域包含在指定的地理边界内部,最终也会使用最高的zoomlevel来显示可视化区域试图 */
padding: 100
});
}
});
}

这个方法请求了多点间的导航信息,在得到返回值(一系列点)后,尝试在地图上新加了一层,用得到的数据在这层上绘制了导航路径。最后控制地图缩放定位到正好完全显示导航路径的为位置。

检索内容,获得静态地图图片

这个功能在java服务端实现,需要介绍下这边的sdk情况。总体来说这个sdk和js那边一样,都是要组合一些参数,请求一个mapbox服务得到结果。sdk底层使用了okhttp3retrofit2,两者结合把网络请求操作简化了。代码风格类似如下:

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
36
37
38
39
public static String getStaticImageURL(List<String> citiesList) throws IOException  {
if(citiesList==null||citiesList.size()==0) {
return Const.MAPBOX_DEFAULT_STATIC_IMAGE_URL;
}
// 存储附加在地图上的标注点
List<StaticMarkerAnnotation> markerList=new ArrayList<StaticMarkerAnnotation>();
for(int i=0;i<citiesList.size();i++) {
String queryStr=citiesList.get(i);
queryStr=queryStr.replaceAll("\\[|\\]", "");
MapboxGeocoding client = MapboxGeocoding.builder()
.accessToken(Const.MAPBOX_API_KEY)
.query(queryStr)
.baseUrl("https://api.mapbox.com/geocoding/v5/")
.build();
// 同步请求数据,另外有异步请求数据方法enqueue
Response<GeocodingResponse> response=client.executeCall();
List<CarmenFeature> results = response.body().features();
if (results.size() > 0) {
Point firstResultPoint = results.get(0).center();
StaticMarkerAnnotation marker=StaticMarkerAnnotation.builder()
.lnglat(firstResultPoint)
.label((i+1)+"")
.color(255, 0, 0)
.build();
markerList.add(marker);
} else {
}
}

MapboxStaticMap staticImage = MapboxStaticMap.builder()
.accessToken(Const.MAPBOX_API_KEY)
.styleId("streets-v9")
.cameraAuto(true)// 做了这个设置,自动达到最佳效果,地图中心点和缩放度量都失效
.width(653) // Image width
.height(837) // Image height
.staticMarkerAnnotations(markerList)
.build();
return staticImage.url().toString();
}

以上代码分为两部分,第一部分同步请求城市的坐标,根据返回结果生成一组标记点信息,第二部分生成一个请求,这个请求中附带了上述生成的标记点信息,最后这个请求没有被发送,而是转换成为了一个url,供程序的其他部分使用。

这里需要特别注意的是对于免费用户,生成图片的次数受到限制,参见这里

总结

使用mapbox构建自己的网站或者应用,更像是购买零件攒一台机器,可能你想要的零件都有,但你得耐心寻找,另外零件也应该在快速更新。