突如其来 2020年2月初,发现公司使用谷歌地图功能的网页和后台功能出现异常。排查过后发现问题在于网络咱解决不了。所以暂时屏蔽了相关功能,着手更换。
听闻业内企业换成了mapbox,决定一试,本文回顾了这个过程。
了解mapbox mapbox成立于2010年,是一个提供地图相关服务的平台。从他的开发文档中提供的变更记录中我们能够发现mapbox最近正在快速的迭代产品。通常这表明了两个信息:
发展势头不错。
产品尚未完善。
技术开发更关心第二条,这次的经历也印证了这个推断。
选择关注的内容 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底层使用了okhttp3 和retrofit2 ,两者结合把网络请求操作简化了。代码风格类似如下:
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构建自己的网站或者应用,更像是购买零件攒一台机器,可能你想要的零件都有,但你得耐心寻找,另外零件也应该在快速更新。