最近因为一些契机,需要做一个网页版的地图相关的开发,本篇博客对开发过程进行简单记录,以备日后查阅。
1.背景介绍
我们的核心需求是给定一些曾经去过的地方,网页需要在地图上展示对应的标记,类似于在一幅现实地图上“插旗”,如下所示。
之所以有这个需求,是因为近些年去了一些地方,有时候自己都想不起来到底去过哪里,所以想做一个备忘录。虽然一开始想在高德地图、Google地图之类的APP中通过收藏地图点的形式实现这个需求,但关键的问题是无法分享给别人或者分享的知识文字列表形式的地图点而非可视化地图。例如高德地图中,即使你创建了个收藏列表,但分享给别人后,必须只能在手机端用高德APP打开,不够便捷。
他分享的本质是一个超长的网页链接,无法记忆,也不够优雅。而在Google地图中,创建收藏列表后分享链接,也不能完整以地图方式显示标记点,如下。
虽然分享链接短了很多,但还是无法记忆,而且在博主电脑浏览器中无法打开。
因此,(1)完全可控、随意修改;(2)良好图形化展示;(3)能够方便分享给别人,这样的需求就逐渐明确了。
2.需求分析与功能设计
在明确了需求以后,就需要根据需求匹配相应技术。对于需求一,最好的解决办法就是自己开发。对于需求二,则可以通过基于各类在线地图API实现。对于需求三,最好的方案是行程在线网页,并匹配短小、有意义的网址(而不是需要安装的移动端APP等)。因此最终选择采用JavaScript、基于百度地图API进行在线网页开发,并在本博客域名下新增字段构成网址(zhaoxuhui.top/world)。
更具体而言,用户在代码中以结构化形式给出数据,代码根据规则读取解析这些数据,并将其转化为可以在地图上显示的标记。完成显示后,进一步实现交互功能。
基础功能包括:地图显示、用户数据读取与解析、标记显示
高级功能包括:标记交互、地图样式切换、数据统计、数据导出等
需要说明的是,这个需求本身并不难,并且其实在很多年前本科的时候就已经有过相关技术积累,可参见这个和这个博客。但之前都是基于Android的移动端开发,并没有相对深入研究JavaScript API。因此,本篇博客进行相关学习。
3.地图API基础介绍
百度地图开发者账户和API Key的申请此处略过,如有需要可以直接去看官网指引。
3.1 地图创建与显示
根据百度地图API官方文档,基础地图的创建、加载与显示主要通过JS代码完成。创建地图必须的代码如下:
// 创建Map实例
var map = new BMapGL.Map('container');
// 初始化地图并设置中心点坐标和地图级别
map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 12);
需要指定地图的中心点和缩放级数。根据示例中心的实际测试,缩放级数可以为浮点数,范围从3到21,数字越大缩放越大。完整的html文件内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>地图展示</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<style>
body,
html,
#container {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
font-family: "微软雅黑";
}
</style>
<script src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=您的密钥"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
<script>
// 创建Map实例
var map = new BMapGL.Map('container');
// 初始化地图并设置中心点坐标和地图级别
map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 12);
</script>
这里,地图是通过div
元素进行显示的,id
为container
,该元素的样式在前面的style
标签中进行了定义。运行后效果如下所示。
除了地图中心点和缩放级别,还可以根据需求,通过JS给地图设置不同的属性和样式,常见的代码如下:
// 鼠标滚轮缩放(默认不开启)
map.enableScrollWheelZoom(true);
// 设置地图类型为卫星图
map.setMapType(BMAP_SATELLITE_MAP);
// 设置地图类型为矢量图(默认)
map.setMapType(BMAP_NORMAL_MAP);
// 设置地图类型为三维地球
map.setMapType(BMAP_EARTH_MAP);
// 设置地图3D视角
map.setHeading(64.5);
map.setTilt(73);
// 自定义3D视角地图颜色
map.setDisplayOptions({skyColors: ['rgba(186, 0, 255, 0)','rgba(186, 0, 255, 0.2)']});
// 不显示底图地物POI文字(默认开启)
map.setDisplayOptions({poiText: false});
// 不显示底图地物POI图标(默认开启)
map.setDisplayOptions({poiIcon: false});
// 不显示绘制的地图覆盖物(默认显示)
map.setDisplayOptions({overlay: false});
// 不显示绘制的叠加图层(默认显示)
map.setDisplayOptions({layer: false});
// 不显示绘制的3D建筑物(默认显示)
map.setDisplayOptions({building: false});
// 不显示地图路网(默认显示,只对卫星图和地球模式有效)
map.setDisplayOptions({street: false});
// 添加比例尺控件
var scaleCtrl = new BMapGL.ScaleControl();
map.addControl(scaleCtrl);
// 添加缩放控件
var zoomCtrl = new BMapGL.ZoomControl();
map.addControl(zoomCtrl);
// 添加3D控件
var navi3DCtrl = new BMapGL.NavigationControl3D();
map.addControl(navi3DCtrl);
// 创建定位控件
var locationControl = new BMapGL.LocationControl({
// 控件的停靠位置(可选,默认左上角)
anchor: BMAP_ANCHOR_TOP_RIGHT,
// 控件基于停靠位置的偏移量(可选)
offset: new BMapGL.Size(20, 20)
});
// 将控件添加到地图上
map.addControl(locationControl);
// 创建城市选择控件
var cityControl = new BMapGL.CityListControl({
// 控件的停靠位置(可选,默认左上角)
anchor: BMAP_ANCHOR_TOP_LEFT,
// 控件基于停靠位置的偏移量(可选)
offset: new BMapGL.Size(10, 5)
});
// 将控件添加到地图上
map.addControl(cityControl);
以上便可以实现地图的基本创建与相关自定义。
3.2 地图标记
在百度地图JS API中,满足我们需求的地图覆盖物是点标记(Marker)。
3.2.1 创建与显示
参考官方文档,其创建与显示仍通过JS代码完成,代码如下:
// 构造地图点对象
var point = new BMapGL.Point(116.404, 39.925);
// 创建点标记
var marker = new BMapGL.Marker(point);
// 在地图上添加点标记
map.addOverlay(marker);
首先是构造了一个地图点对象,然后基于该对象创建Marker对象,最后将其添加到地图中。完整代码如下。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>地图展示</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<style>
body,
html,
#container {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
font-family: "微软雅黑";
}
</style>
<script src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=您的密钥"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
<script>
// 创建Map实例
var map = new BMapGL.Map('container');
// 初始化地图并设置中心点坐标和地图级别
map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 12);
// 构造地图点对象
var point = new BMapGL.Point(116.404, 39.925);
// 创建点标记
var marker = new BMapGL.Marker(point);
// 在地图上添加点标记
map.addOverlay(marker);
</script>
运行效果如下所示。
3.2.2 点击事件
对于地图上显示的Maker,我们自然不止希望能看,还希望可以点击它,然后给我们一些反馈。这些可以通过给Maker添加监听事件和对应的回调函数实现。一个基础的点击事件如下:
// 构造地图点对象
var point = new BMapGL.Point(116.404, 39.925);
// 信息窗口基本属性
var opts = {
width: 200,
height: 100,
title: '故宫博物院'
};
// 创建信息窗口
var infoWindow = new BMapGL.InfoWindow('地址:北京市东城区王府井大街88号乐天银泰百货八层', opts);
// 点标记添加点击事件
marker.addEventListener('click', function () {
// 回调函数:在point的位置开启信息窗口
map.openInfoWindow(infoWindow, point);
});
上述代码中,给Maker添加了信息窗口,完整代码如下。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>地图展示</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<style>
body,
html,
#container {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
font-family: "微软雅黑";
}
</style>
<script src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=您的密钥"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
<script>
// 创建Map实例
var map = new BMapGL.Map('container');
// 初始化地图并设置中心点坐标和地图级别
map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 12);
// 构造地图点对象
var point = new BMapGL.Point(116.404, 39.925);
// 创建点标记
var marker = new BMapGL.Marker(point);
// 在地图上添加点标记
map.addOverlay(marker);
// 信息窗口基本属性
var opts = {
width: 200,
height: 100,
title: '故宫博物院'
};
// 创建信息窗口
var infoWindow = new BMapGL.InfoWindow('地址:北京市东城区王府井大街88号乐天银泰百货八层', opts);
// 点标记添加点击事件
marker.addEventListener('click', function () {
// 回调函数:在point的位置开启信息窗口
map.openInfoWindow(infoWindow, point);
});
</script>
运行结果如下。
3.2.3 自定义图标与拖拽
Marker除了默认的红色图标,也支持自定义图标,核心代码如下:
// 构造地图点对象
var pt = new BMapGL.Point(116.417, 39.909);
// 创建图标对象
var myIcon = new BMapGL.Icon("/jsdemo/img/car.png", new BMapGL.Size(52, 26));
// 使用图标创建Marker
var marker = new BMapGL.Marker(pt, {icon: myIcon});
// 将Marker添加到地图
map.addOverlay(marker);
运行效果如下。
此外,Marker也支持实时拖拽,只需要修改它的
enableDragging
即可,如下。
var marker = new BMapGL.Marker(point, {
enableDragging: true
});
3.3 地图上下文菜单
向地图添加上下文菜单的方式也比较简单,核心代码如下:
var menu = new BMapGL.ContextMenu();
var txtMenuItem = [
{
text: '设置为卫星图',
callback: function (){map.setMapType(BMAP_SATELLITE_MAP);}
}, {
text: '设置为矢量图',
callback: function (){map.setMapType(BMAP_NORMAL_MAP);}
}
];
for (var i = 0; i < txtMenuItem.length; i++) {
menu.addItem(new BMapGL.MenuItem(txtMenuItem[i].text, txtMenuItem[i].callback, 100));
}
map.addContextMenu(menu);
上述代码中首先创建了上下文菜单对象,然后构造了菜单内容的列表,并为每一个项目指定了名称和回调函数。然后通过一个for循环将这些项目赋给菜单对象,最后调用相应函数实现菜单添加。完整的html代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>地图展示</title>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<style>
body,
html,
#container {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
font-family: "微软雅黑";
}
</style>
<script src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=您的密钥"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
<script>
// 创建Map实例
var map = new BMapGL.Map('container');
// 初始化地图并设置中心点坐标和地图级别
map.centerAndZoom(new BMapGL.Point(116.404, 39.915), 12);
var menu = new BMapGL.ContextMenu();
var txtMenuItem = [
{
text: '设置为卫星图',
callback: function (){map.setMapType(BMAP_SATELLITE_MAP);}
}, {
text: '设置为矢量图',
callback: function (){map.setMapType(BMAP_NORMAL_MAP);}
}
];
for (var i = 0; i < txtMenuItem.length; i++) {
menu.addItem(new BMapGL.MenuItem(txtMenuItem[i].text, txtMenuItem[i].callback, 100));
}
map.addContextMenu(menu);
</script>
运行效果如下。
4.关键代码开发与说明
有了上述代码基础,我们就可以进入正式开发了。
4.1 数据输入与解析
旅行数据可以通过远程方式获取也可以直接写到html文件中。因为本需求中不涉及隐私等相关问题,所以采用比较简单的办法,直接写到代码中。我们规定数据的结构是一条记录由一个list储存,list中分别存放目的地经度(浮点型)、目的地纬度(浮点型)、目的地国家(字符串)、目的地城市(字符串)以及访问时间(多个时间用英文逗号隔开),示例如下:
[100.501751, 13.756322, "泰国", "曼谷", "2017年7月, 2023年8月"]
多个目的地通过list进行存储。添加数据的相关代码写到loadTrajectory()
函数中便于管理。
4.2 创建基础地图并显示
对于地图,要求总体简单,相关代码如下:
var map = new BMapGL.Map("container", {
showVectorStreetLayer: true,
showVectorLine: false,
});
map.enableScrollWheelZoom(true);
map.setMapType(BMAP_NORMAL_MAP);
默认类型地图、显示道路、支持滚轮缩放即可。
4.3 添加地图Marker
基于给定的数据,向地图添加Marker也比较简单,如下。
var myIcon = new BMapGL.Icon("/world/assets/images/flag.png", new BMapGL.Size(32, 32));
for (var i = 0; i < pls.length; i++) {
var point = new BMapGL.Point(pls[i][0], pls[i][1]);
var marker = new BMapGL.Marker(point, {icon: myIcon});
map.addOverlay(marker);
mean_lat = mean_lat + pls[i][1];
mean_lon = mean_lon + pls[i][0];
}
mean_lat = mean_lat / pls.length;
mean_lon = mean_lon / pls.length;
map.centerAndZoom(new BMapGL.Point(mean_lon, mean_lat), 3);
首先构造了自定义的图标,然后依次遍历数据,进行Marker添加。同时在这个过程中,会动态计算这些Marker的中心点,之后以此作为整个地图的中心点。
4.4 核心功能实现
为了使用更加舒适,设计了“设为卫星图”、“设为矢量图”、“设为3D图”、“信息统计”、“随机漫游”和“记录导出”这几个功能,下面简单介绍。
4.4.1 设置地图功能
这些功能就是将地图设为对应的类型,比较简单。在代码中通过各函数实现,如下。
function setMapSatellite(){
map.setMapType(BMAP_SATELLITE_MAP);
}
function setMapNormal(){
map.setMapType(BMAP_NORMAL_MAP);
}
function setMap3D(){
map.setMapType(BMAP_EARTH_MAP);
}
4.4.2 信息统计功能
基于给定的数据进行一些基础的信息统计,在代码中通过infoStat()
函数实现,如下:
function infoStat(){
var nn_pos = -90;
var sn_pos = 90;
var en_pos = 0;
var wn_pos = 360;
var nn_idx = 0;
var sn_idx = 0;
var en_idx = 0;
var wn_idx = 0;
var str_names = "";
var str_cts = "";
var str_nat = "";
var t_num = 3;
var t_list = [];
var tmp_nums = [];
var num_cts = [];
var num_cts2 = [];
var num_nats = [];
var num_nats2 = [];
var tmp_countries = [];
for (var i = 0; i < pls.length; i++) {
var tmp_lon = pls[i][0] + 180;
var tmp_lat = pls[i][1];
if(tmp_lon > en_pos){
en_pos = tmp_lon;
en_idx = i;
}
if(tmp_lon < wn_pos){
wn_pos = tmp_lon;
wn_idx = i;
}
if(tmp_lat > nn_pos){
nn_pos = tmp_lat;
nn_idx = i;
}
if(tmp_lat < sn_pos){
sn_pos = tmp_lat;
sn_idx = i;
}
var tmp_visit_city_num = pls[i][4].split(",").length;
num_cts.push(tmp_visit_city_num);
num_cts2.push(tmp_visit_city_num);
tmp_countries.push(pls[i][2]);
}
num_cts.sort(function(a,b){return a-b;});
tmp_nums = Array.from(new Set(num_cts));
for(var i = 1; i <= t_num; i++){
t_list.push(tmp_nums[tmp_nums.length-i]);
}
for(var i = 0; i <= t_list.length; i++){
var cur_num = t_list[i];
for(var j = 0; j < num_cts2.length; j++){
if(cur_num == num_cts2[j]){
str_cts = str_cts + pls[j][2] + pls[j][3] + "(" + cur_num.toString() + "次), ";
}
}
}
str_cts = str_cts.slice(0, str_cts.length-2);
var unique_countries = Array.from(new Set(tmp_countries));
for (var i = 0; i < unique_countries.length; i++) {
var tmp_country = unique_countries[i];
str_names = str_names + tmp_country + ", ";
var tmp_num = 0;
for (var j = 0; j < pls.length; j++) {
if(tmp_country == pls[j][2]){
tmp_num = tmp_num + 1;
}
}
num_nats.push(tmp_num);
num_nats2.push(tmp_num);
}
str_names = str_names.slice(0, str_names.length-2);
num_nats.sort(function(a,b){return a-b;});
tmp_nums = Array.from(new Set(num_nats));
t_list = [];
for(var i = 1; i <= t_num; i++){
t_list.push(tmp_nums[tmp_nums.length-i]);
}
for(var i = 0; i <= t_list.length; i++){
var cur_num = t_list[i];
for(var j = 0; j < num_nats2.length; j++){
if(cur_num == num_nats2[j]){
str_nat = str_nat + unique_countries[j] + "(" + cur_num.toString() + "次), ";
}
}
}
str_nat = str_nat.slice(0, str_nat.length-2);
var str_line1 = "🔓️已解锁"+unique_countries.length.toString()+"个国家/地区的"+pls.length.toString()+"座城市:\n"+str_names+"\n";
var str_line2 = "🏝️到访城市最多国家(TOP3):\n"+str_nat+"\n";
var str_line3 = "🏡到访最多城市(TOP3):\n"+str_cts+"\n";
var range_hon = (Math.abs(wn_pos-180)+en_pos-180);
var range_ver = (Math.abs(sn_pos)+nn_pos);
var hon_pct = range_hon / 360;
var ver_pct = range_ver / 180;
var explore_pct = (range_hon * range_ver) / (360*180);
var str_line4 = "🌏地球探索度:"+explore_pct.toFixed(2)+" / 1\n";
str_line4 = str_line4+"东西跨度:"+range_hon.toFixed(4)+"° / 360° (约"+hon_pct.toFixed(2)+"个地球)\n";
str_line4 = str_line4+"南北跨度:"+range_ver.toFixed(4)+"° / 180° (约"+ver_pct.toFixed(2)+"个地球)\n";
var str_line5 = "🧭️探索范围:\n";
str_line5 = str_line5 + "️最东:"+pls[en_idx][2]+pls[en_idx][3]+" (东经"+(en_pos-180).toFixed(4).toString()+")"+"\n";
str_line5 = str_line5 + "最西:"+pls[wn_idx][2]+pls[wn_idx][3]+" (西经"+Math.abs(wn_pos-180).toFixed(4).toString()+")"+"\n";
str_line5 = str_line5 + "最南:"+pls[sn_idx][2]+pls[sn_idx][3]+" (南纬"+Math.abs(sn_pos).toFixed(4).toString()+")"+"\n";
str_line5 = str_line5 + "最北:"+pls[nn_idx][2]+pls[nn_idx][3]+" (北纬"+(nn_pos).toFixed(4).toString()+")"+"\n";
alert(str_line1+"\n"+str_line2+"\n"+str_line3+"\n"+str_line4+"\n"+str_line5);
}
核心就是通过遍历各记录从而计算最值或均值等操作,统计包括到访城市、国家、次数、范围等信息。稍微需要注意的是包括JS中去除重复元素、按需求排序等。最后,通过浏览器的Alert功能弹出提示框显示内容。
4.4.3 数据导出功能
作为尽可能开放的功能实现,面对未来可能的各种潜在需求,我们支持直接导出原始数据,从而可以方便用户进行分析或二次开发。在代码中通过outputInfo()
函数实现,如下:
function outputInfo(){
var str_info = "序号, 国家/地区, 城市, 经度, 纬度, 说明\n";
for (var i = 0; i < pls.length; i++) {
var tmp_parts = pls[i][4].split(",")
var tmp_desp = "";
if(tmp_parts.length==1){
tmp_desp = pls[i][4]
}else{
for(var j = 0; j < tmp_parts.length; j++) {
tmp_desp = tmp_desp + tmp_parts[j]
}
}
str_info = str_info + (i+1).toString() + ", " + pls[i][2]+", "+pls[i][3]+", "+pls[i][0].toFixed(4).toString()+", "+pls[i][1].toFixed(4).toString()+", "+tmp_desp+"\n";
}
var textFileAsBlob = new Blob([str_info], {type: 'text/plain'});
var downloadLink = document.createElement('a');
downloadLink.download = 'Records.csv';
downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
downloadLink.click();
}
简单来说,我们通过通用的csv文件格式对原始数据进行了格式化,然后通过JS构造了相应文件。当运行该函数后,浏览器就会自动弹出下载界面。
4.4.4 随机探索功能
为了增加浏览乐趣,也增加了随机探索功能。所谓随机探索是指,程序随机从给定的访问记录中选择一个,并且将其居中,在地图上展示详细信息。在代码中通过explore()
函数实现,如下:
function explore(){
var tmp_idx = (Math.random()*pls.length).toFixed(0);
if(tmp_idx == pls.length){
tmp_idx = pls.length - 1;
}
var tmp_str_loc ="经度:"+pls[tmp_idx][0].toFixed(4).toString()+", 纬度:"+pls[tmp_idx][1].toFixed(4).toString();
map.openInfoWindow(new BMapGL.InfoWindow(tmp_str_loc+"<br>时间:"+pls[tmp_idx][4],{title:pls[tmp_idx][2]+pls[tmp_idx][3]}),
new BMapGL.Point(pls[tmp_idx][0],pls[tmp_idx][1]));
map.centerAndZoom(new BMapGL.Point(pls[tmp_idx][0],pls[tmp_idx][1]),8);
}
其核心是随机数的构造以及map.centerAndZoom()
将选择的地点设置为地图中心点的能力。
4.5 构造控制菜单
为了方便不同用户使用,我们提供两种控制方式,一种是PC端,在地图上点击右键即可进行控制,另一种则是移动端,无法右键点击,则通过点击右下角的常驻按钮弹出菜单进行控制。根据上文,我们目前提供给功能包括:设为卫星图、设为矢量图、设为3D图、信息统计、随机漫游、记录导出,这6个功能,对应菜单也有6项。
4.5.1 右键上下文菜单
在3.3部分已经介绍过百度地图API中上下文菜单的添加,实际代码如下:
var menu = new BMapGL.ContextMenu();
var txtMenuItem = [
{
text: '设置为卫星图',
callback: setMapSatellite
}, {
text: '设置为矢量图',
callback: setMapNormal
}, {
text: '设置为3D图',
callback: setMap3D
}, {
text: '信息统计',
callback: infoStat
}, {
text: '随机漫游',
callback: explore
}, {
text: '记录导出',
callback: outputInfo
}
];
for (var i = 0; i < txtMenuItem.length; i++) {
menu.addItem(new BMapGL.MenuItem(txtMenuItem[i].text, txtMenuItem[i].callback, 100));
}
map.addContextMenu(menu);
在每一项中,callback
属性都设置成了前面提到过的对应回调函数。最后,通过menu.addItem()
将项目添加到菜单中,并通过map.addContextMenu()
将菜单添加到地图。
4.5.2 常驻菜单
对于常驻菜单,我们首先需要在HTML中构造出它的样子,如下:
<ul id="menuBackground" class="btn-wrap" style="z-index: 99;visibility: hidden">
<li id="btn_map_sat" class="btn" onclick="setMapSatellite()">卫星图</li>
<li id="btn_map_norm" class="btn" onclick="setMapNormal()">矢量图</li>
<li id="btn_map_3d" class="btn" onclick="setMap3D()">3D图</li>
<li id="btn_stat" class="btn" onclick="infoStat()">统计</li>
<li id="btn_explore" class="btn" onclick="explore()">漫游</li>
<li id="btn_output" class="btn" onclick="outputInfo()">导出</li>
<li id="btn_root" class="btn-root" onclick="showMenu()" style="visibility:visible">💡</li>
</ul>
这里我们采用ul
无序列表实现基础的框架,然后li
表示每一项。对于每一项分别设置他们的文字内容和对应的点击函数。对于每一项的按钮,他们的类别都是btn
,对于整个菜单,类型是btn-wrap
。在<style>
标签中,我们采用CSS进行了样式定义,如下:
ul li {
list-style: none;
}
.btn-wrap {
z-index: 999;
position: fixed;
width: 65px;
bottom: 5px;
right: 10px;
padding: 5px;
border-radius: 5px;
background-color: rgba(265, 265, 265, 0.5);
box-shadow: 0 2px 6px 0 rgba(27, 142, 236, 0.5);
}
.btn {
width: 55px;
height: 30px;
float: left;
background-color: #fff;
color: rgba(27, 142, 236, 1);
font-size: 14px;
border:1px solid rgba(27, 142, 236, 1);
border-radius: 5px;
margin: 0 5px 5px;
text-align: center;
line-height: 30px;
}
.btn:hover {
background-color: rgba(27, 142, 236, 0.8);
color: #fff;
cursor: pointer;
}
.btn-root {
width: 55px;
height: 30px;
float: left;
background-color: rgb(255, 246, 215);
color: rgba(27, 142, 236, 1);
font-size: 14px;
border:1px solid rgba(27, 142, 236, 1);
border-radius: 5px;
margin: 0 5px 5px;
text-align: center;
line-height: 30px;
box-shadow: 0 2px 6px 0 rgba(27, 142, 236, 0.5);
}
.btn-root:hover {
background-color: rgba(250, 231, 161);
color: #fff;
cursor: pointer;
}
这样便实现了一个和4.5.1部分上下文菜单功能相同、可以点击的控制菜单了。但这里有个美学上和用户交互上的问题。作为一个干净整洁的地图展示应用,如果有一堆按钮一直出现在屏幕右下角在某些场合下也会比较“破坏气氛”。我们希望有一个控制菜单显示的按钮在右下角,默认菜单不显示,点击一下按钮菜单显示,再点击一下菜单隐藏。为了实现这个功能,在代码中以showMenu()
函数实现,如下:
function showMenu(){
if(flag_vis==1){
var element_bk = document.getElementById("menuBackground");
element_bk.style.visibility = 'hidden';
var element_btn_root = document.getElementById("btn_root");
element_btn_root.style.visibility = 'visible';
element_btn_root.style.boxShadow = '0 2px 6px 0 rgba(27, 142, 236, 0.5)';
flag_vis = 0;
}else{
var element_bk = document.getElementById("menuBackground");
element_bk.style.visibility = 'visible';
var element_btn_root = document.getElementById("btn_root");
element_btn_root.style.boxShadow = 'none';
flag_vis = 1;
}
}
总体而言,我们的思路是通过一个全局变量flag_vis
控制菜单,而想要控制的对象又是通过JS的document.getElementById()
获得。通过将对应属性设置为visible
或none/hidden
实现菜单的显示或隐藏。
4.6 地图Marker添加监听
以上,我们完成了地图的基本创建、显示以及控制。最后,我们需要对各个Marker添加点击事件,实现详细信息的展示。有了3.2.2部分的基础,会相对简单一些,代码如下:
map.addEventListener("click", function(e) {
if (e.overlay && e.overlay instanceof BMapGL.Marker) {
const clickedMarker = e.overlay;
var cur_lon = clickedMarker.getPosition().lng;
var cur_lat = clickedMarker.getPosition().lat;
var clicked_idx = 0;
for (var i = 0; i < pls.length; i++) {
var tmp_lon = pls[i][0];
var tmp_lat = pls[i][1];
if(Math.abs(tmp_lon - cur_lon) < 0.015 && Math.abs(tmp_lat - cur_lat) < 0.015){
clicked_idx = i;
break;
}
}
var str_loc ="经度:" + cur_lon.toFixed(4).toString() + ", 纬度:" + cur_lat.toFixed(4).toString();
map.openInfoWindow(new BMapGL.InfoWindow(str_loc + "<br>时间:" + pls[clicked_idx][4], {title: pls[clicked_idx][2]+pls[clicked_idx][3]}),
new BMapGL.Point(cur_lon, cur_lat));
}
});
这里的一个重点是,每个Marker需要显示的内容是不同的,并不能每个Marker都显示相同的内容。所以我们需要动态的获取点击状态,然后根据状态判断显示什么。具体来说我们可以通过点击事件的种类和位置进行判断。这里e
就是包含了点击事件的返回值。我们分别获得他的位置和类型,再将其与各个Marker的位置进行比较,即可以判断是哪个Marker被点击了,进而显示相应内容。
4.7 完整代码展示
在完成各个核心功能的介绍后,下面展示了完整的Html网页代码:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>🗺Adventure Map - Where I Have Been To</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
<meta http-equiv="content-language" content="zh-cn">
<style type="text/css">
body,
html,
#container {
overflow: hidden;
width: 100%;
height: 100%;
margin: 0;
font-family: "微软雅黑";
}
ul li {
list-style: none;
}
.btn-wrap {
z-index: 999;
position: fixed;
width: 65px;
bottom: 5px;
right: 10px;
padding: 5px;
border-radius: 5px;
background-color: rgba(265, 265, 265, 0.5);
box-shadow: 0 2px 6px 0 rgba(27, 142, 236, 0.5);
}
.btn {
width: 55px;
height: 30px;
float: left;
background-color: #fff;
color: rgba(27, 142, 236, 1);
font-size: 14px;
border:1px solid rgba(27, 142, 236, 1);
border-radius: 5px;
margin: 0 5px 5px;
text-align: center;
line-height: 30px;
}
.btn:hover {
background-color: rgba(27, 142, 236, 0.8);
color: #fff;
cursor: pointer;
}
.btn-root {
width: 55px;
height: 30px;
float: left;
background-color: rgb(255, 246, 215);
color: rgba(27, 142, 236, 1);
font-size: 14px;
border:1px solid rgba(27, 142, 236, 1);
border-radius: 5px;
margin: 0 5px 5px;
text-align: center;
line-height: 30px;
box-shadow: 0 2px 6px 0 rgba(27, 142, 236, 0.5);
}
.btn-root:hover {
background-color: rgba(250, 231, 161);
color: #fff;
cursor: pointer;
}
</style>
<script src="//api.map.baidu.com/api?type=webgl&v=1.0&ak=yourkey"></script>
</head>
<body>
<div id="container"></div>
<ul id="menuBackground" class="btn-wrap" style="z-index: 99;visibility: hidden">
<li id="btn_map_sat" class="btn" onclick="setMapSatellite()">卫星图</li>
<li id="btn_map_norm" class="btn" onclick="setMapNormal()">矢量图</li>
<li id="btn_map_3d" class="btn" onclick="setMap3D()">3D图</li>
<li id="btn_stat" class="btn" onclick="infoStat()">统计</li>
<li id="btn_explore" class="btn" onclick="explore()">漫游</li>
<li id="btn_output" class="btn" onclick="outputInfo()">导出</li>
<li id="btn_root" class="btn-root" onclick="showMenu()" style="visibility: visible">💡</li>
</ul>
</body>
</html>
<script>
// 全局变量
var flag_vis = 0;
// 经度 纬度 地名 描述(时间)
var pls = [];
var mean_lat = 0.0;
var mean_lon = 0.0;
function loadTrajectory(){
pls.push([100.501751, 13.756322, "泰国", "曼谷", "2017年7月, 2023年8月"]);
pls.push([100.882244, 12.923159, "泰国", "芭提雅", "2017年7月"]);
pls.push([82.895857, 54.983751, "俄罗斯", "新西伯利亚", "2017年9月"]);
pls.push([135.502158, 34.693599, "日本", "大阪", "2018年2月, 2025年5月"]);
pls.push([135.768128, 35.011533, "日本", "京都", "2018年2月"]);
pls.push([139.766934, 35.682413, "日本", "东京", "2018年2月, 2019年6月, 2025年5月"]);
pls.push([113.543563, 22.198081, "中国", "澳门", "2018年5月"]);
pls.push([103.807156, 1.348317, "新加坡", "新加坡", "2018年8月, 2025年4月"]);
pls.push([115.185296, -8.418173, "印度尼西亚", "巴厘岛", "2018年8月"]);
pls.push([101.683895, 3.130960, "马来西亚", "吉隆坡", "2018年8月, 2023年8月"]);
pls.push([114.169369, 22.319221, "中国", "香港", "2018年9月, 2019年6月"]);
pls.push([141.354392, 43.060516, "日本", "札幌", "2019年2月"]);
pls.push([140.993829, 43.189764, "日本", "小樽", "2019年2月"]);
pls.push([142.363837, 43.770506, "日本", "旭川", "2019年2月"]);
pls.push([143.353148, 44.355344, "日本", "纹别", "2019年2月"]);
pls.push([126.630739, 37.474687, "韩国", "仁川", "2019年2月"]);
pls.push([139.546128, 35.318778, "日本", "镰仓", "2019年6月"]);
pls.push([79.861011, 6.926778, "斯里兰卡", "科伦坡", "2019年7月"]);
pls.push([73.5090693, 4.175160, "马尔代夫", "马累", "2019年7月"]);
pls.push([102.632541, 17.975771, "老挝", "万象", "2023年5月"]);
pls.push([116.076644, 5.983179, "马来西亚", "亚庇", "2023年8月, 2025年4月"]);
pls.push([31.2356326, 30.044029, "埃及", "开罗", "2023年8月"]);
pls.push([33.8112233, 27.255908, "埃及", "赫尔格达", "2023年8月"]);
pls.push([32.6393817, 25.686891, "埃及", "卢克索", "2023年8月"]);
pls.push([126.518976, 33.503221, "韩国", "济州岛", "2023年10月"]);
pls.push([2.351362, 48.857539, "法国", "巴黎", "2024年2月, 2024年6月, 2024年7月, 2025年2月"]);
pls.push([2.129729, 48.802315, "法国", "凡尔赛", "2024年2月"]);
pls.push([2.701560, 48.404639, "法国", "枫丹白露", "2024年2月"]);
pls.push([-1.511466, 48.636157, "法国", "勒蒙-圣米歇尔", "2024年2月"]);
pls.push([8.541666, 47.376872, "瑞士", "苏黎世", "2024年2月"]);
pls.push([8.309269, 47.050113, "瑞士", "卢塞恩", "2024年2月"]);
pls.push([7.863192, 46.686322, "瑞士", "因特拉肯", "2024年2月"]);
pls.push([8.041323, 46.624082, "瑞士", "格林德尔瓦尔德", "2024年2月"]);
pls.push([8.407103, 46.819451, "瑞士", "恩格尔贝格", "2024年2月"]);
pls.push([51.529835, 25.283296, "卡塔尔", "多哈", "2024年2月"]);
pls.push([1.528824, 49.076658, "法国", "吉维尼", "2024年6月"]);
pls.push([-21.940789, 64.146976, "冰岛", "雷克雅未克", "2024年6月"]);
pls.push([50.587274, 26.223222, "巴林", "麦纳麦", "2024年7月"]);
pls.push([23.727433, 37.983798, "希腊", "雅典", "2024年7月"]);
pls.push([-3.703301, 40.416602, "西班牙", "马德里", "2024年7月"]);
pls.push([5.369004, 43.302522, "法国", "马赛", "2024年7月"]);
pls.push([5.984179, 43.836076, "法国", "瓦朗索勒", "2024年7月"]);
pls.push([25.461440, 36.393072, "希腊", "圣托里尼", "2024年7月"]);
pls.push([-0.127600, 51.507189, "英国", "伦敦", "2025年2月"]);
pls.push([10.752224, 59.913837, "挪威", "奥斯陆", "2025年2月"]);
pls.push([18.955307, 69.649200, "挪威", "特罗姆瑟", "2025年2月"]);
pls.push([15.625646, 78.225284, "斯瓦尔巴群岛", "朗伊尔城", "2025年2月"]);
pls.push([12.568332, 55.676073, "丹麦", "哥本哈根", "2025年2月"]);
pls.push([118.611544, 4.479346, "马来西亚", "仙本那", "2025年4月"]);
pls.push([135.804715, 34.685025, "日本", "奈良", "2025年5月"]);
pls.push([136.906530, 35.181422, "日本", "名古屋", "2025年5月"]);
pls.push([138.382650, 34.975517, "日本", "静冈", "2025年5月"]);
pls.push([138.807215, 35.487832, "日本", "富士吉田市", "2025年5月"]);
pls.push([139.559610, 35.682977, "日本", "三鹰市", "2025年5月"]);
}
function showMenu(){
if(flag_vis==1){
var element_bk = document.getElementById("menuBackground");
element_bk.style.visibility = 'hidden';
var element_btn_root = document.getElementById("btn_root");
element_btn_root.style.visibility = 'visible';
element_btn_root.style.boxShadow = '0 2px 6px 0 rgba(27, 142, 236, 0.5)';
flag_vis = 0;
}else{
var element_bk = document.getElementById("menuBackground");
element_bk.style.visibility = 'visible';
var element_btn_root = document.getElementById("btn_root");
element_btn_root.style.boxShadow = 'none';
flag_vis = 1;
}
}
function outputInfo(){
var str_info = "序号, 国家/地区, 城市, 经度, 纬度, 说明\n";
for (var i = 0; i < pls.length; i++) {
var tmp_parts = pls[i][4].split(",")
var tmp_desp = "";
if(tmp_parts.length==1){
tmp_desp = pls[i][4]
}else{
for(var j = 0; j < tmp_parts.length; j++) {
tmp_desp = tmp_desp + tmp_parts[j]
}
}
str_info = str_info + (i+1).toString() + ", " + pls[i][2]+", "+pls[i][3]+", "+pls[i][0].toFixed(4).toString()+", "+pls[i][1].toFixed(4).toString()+", "+tmp_desp+"\n";
}
var textFileAsBlob = new Blob([str_info], {type: 'text/plain'});
var downloadLink = document.createElement('a');
downloadLink.download = 'Records.csv';
downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
downloadLink.click();
}
function setMapSatellite(){
map.setMapType(BMAP_SATELLITE_MAP);
}
function setMapNormal(){
map.setMapType(BMAP_NORMAL_MAP);
}
function setMap3D(){
map.setMapType(BMAP_EARTH_MAP);
}
function infoStat(){
var nn_pos = -90;
var sn_pos = 90;
var en_pos = 0;
var wn_pos = 360;
var nn_idx = 0;
var sn_idx = 0;
var en_idx = 0;
var wn_idx = 0;
var str_names = "";
var str_cts = "";
var str_nat = "";
var t_num = 3;
var t_list = [];
var tmp_nums = [];
var num_cts = [];
var num_cts2 = [];
var num_nats = [];
var num_nats2 = [];
var tmp_countries = [];
for (var i = 0; i < pls.length; i++) {
var tmp_lon = pls[i][0] + 180;
var tmp_lat = pls[i][1];
if(tmp_lon > en_pos){
en_pos = tmp_lon;
en_idx = i;
}
if(tmp_lon < wn_pos){
wn_pos = tmp_lon;
wn_idx = i;
}
if(tmp_lat > nn_pos){
nn_pos = tmp_lat;
nn_idx = i;
}
if(tmp_lat < sn_pos){
sn_pos = tmp_lat;
sn_idx = i;
}
var tmp_visit_city_num = pls[i][4].split(",").length;
num_cts.push(tmp_visit_city_num);
num_cts2.push(tmp_visit_city_num);
tmp_countries.push(pls[i][2]);
}
num_cts.sort(function(a,b){return a-b;});
tmp_nums = Array.from(new Set(num_cts));
for(var i = 1; i <= t_num; i++){
t_list.push(tmp_nums[tmp_nums.length-i]);
}
for(var i = 0; i <= t_list.length; i++){
var cur_num = t_list[i];
for(var j = 0; j < num_cts2.length; j++){
if(cur_num == num_cts2[j]){
str_cts = str_cts + pls[j][2] + pls[j][3] + "(" + cur_num.toString() + "次), ";
}
}
}
str_cts = str_cts.slice(0, str_cts.length-2);
var unique_countries = Array.from(new Set(tmp_countries));
for (var i = 0; i < unique_countries.length; i++) {
var tmp_country = unique_countries[i];
str_names = str_names + tmp_country + ", ";
var tmp_num = 0;
for (var j = 0; j < pls.length; j++) {
if(tmp_country == pls[j][2]){
tmp_num = tmp_num + 1;
}
}
num_nats.push(tmp_num);
num_nats2.push(tmp_num);
}
str_names = str_names.slice(0, str_names.length-2);
num_nats.sort(function(a,b){return a-b;});
tmp_nums = Array.from(new Set(num_nats));
t_list = [];
for(var i = 1; i <= t_num; i++){
t_list.push(tmp_nums[tmp_nums.length-i]);
}
for(var i = 0; i <= t_list.length; i++){
var cur_num = t_list[i];
for(var j = 0; j < num_nats2.length; j++){
if(cur_num == num_nats2[j]){
str_nat = str_nat + unique_countries[j] + "(" + cur_num.toString() + "次), ";
}
}
}
str_nat = str_nat.slice(0, str_nat.length-2);
var str_line1 = "🔓️已解锁"+unique_countries.length.toString()+"个国家/地区的"+pls.length.toString()+"座城市:\n"+str_names+"\n";
var str_line2 = "🏝️到访城市最多国家(TOP3):\n"+str_nat+"\n";
var str_line3 = "🏡到访最多城市(TOP3):\n"+str_cts+"\n";
var range_hon = (Math.abs(wn_pos-180)+en_pos-180);
var range_ver = (Math.abs(sn_pos)+nn_pos);
var hon_pct = range_hon / 360;
var ver_pct = range_ver / 180;
var explore_pct = (range_hon * range_ver) / (360*180);
var str_line4 = "🌏地球探索度:"+explore_pct.toFixed(2)+" / 1\n";
str_line4 = str_line4+"东西跨度:"+range_hon.toFixed(4)+"° / 360° (约"+hon_pct.toFixed(2)+"个地球)\n";
str_line4 = str_line4+"南北跨度:"+range_ver.toFixed(4)+"° / 180° (约"+ver_pct.toFixed(2)+"个地球)\n";
var str_line5 = "🧭️探索范围:\n";
str_line5 = str_line5 + "️最东:"+pls[en_idx][2]+pls[en_idx][3]+" (东经"+(en_pos-180).toFixed(4).toString()+")"+"\n";
str_line5 = str_line5 + "最西:"+pls[wn_idx][2]+pls[wn_idx][3]+" (西经"+Math.abs(wn_pos-180).toFixed(4).toString()+")"+"\n";
str_line5 = str_line5 + "最南:"+pls[sn_idx][2]+pls[sn_idx][3]+" (南纬"+Math.abs(sn_pos).toFixed(4).toString()+")"+"\n";
str_line5 = str_line5 + "最北:"+pls[nn_idx][2]+pls[nn_idx][3]+" (北纬"+(nn_pos).toFixed(4).toString()+")"+"\n";
alert(str_line1+"\n"+str_line2+"\n"+str_line3+"\n"+str_line4+"\n"+str_line5);
}
function explore(){
var tmp_idx = (Math.random()*pls.length).toFixed(0);
if(tmp_idx == pls.length){
tmp_idx = pls.length - 1;
}
var tmp_str_loc ="经度:"+pls[tmp_idx][0].toFixed(4).toString()+", 纬度:"+pls[tmp_idx][1].toFixed(4).toString();
map.openInfoWindow(new BMapGL.InfoWindow(tmp_str_loc+"<br>时间:"+pls[tmp_idx][4],{title:pls[tmp_idx][2]+pls[tmp_idx][3]}),
new BMapGL.Point(pls[tmp_idx][0],pls[tmp_idx][1]));
map.centerAndZoom(new BMapGL.Point(pls[tmp_idx][0],pls[tmp_idx][1]),8);
}
// step1
loadTrajectory();
// step2
var map = new BMapGL.Map("container", {
showVectorStreetLayer: true,
showVectorLine: false,
});
map.enableScrollWheelZoom(true);
map.setMapType(BMAP_NORMAL_MAP);
var myIcon = new BMapGL.Icon("/world/assets/images/flag.png", new BMapGL.Size(32, 32));
// step3
for (var i = 0; i < pls.length; i++) {
var point = new BMapGL.Point(pls[i][0], pls[i][1]);
var marker = new BMapGL.Marker(point, {icon: myIcon});
map.addOverlay(marker);
mean_lat = mean_lat + pls[i][1];
mean_lon = mean_lon + pls[i][0];
}
mean_lat = mean_lat / pls.length;
mean_lon = mean_lon / pls.length;
map.centerAndZoom(new BMapGL.Point(mean_lon, mean_lat), 3);
// step4
var menu = new BMapGL.ContextMenu();
var txtMenuItem = [
{
text: '设置为卫星图',
callback: setMapSatellite
}, {
text: '设置为矢量图',
callback: setMapNormal
}, {
text: '设置为3D图',
callback: setMap3D
}, {
text: '信息统计',
callback: infoStat
}, {
text: '随机漫游',
callback: explore
}, {
text: '记录导出',
callback: outputInfo
}
];
for (var i = 0; i < txtMenuItem.length; i++) {
menu.addItem(new BMapGL.MenuItem(txtMenuItem[i].text, txtMenuItem[i].callback, 100));
}
map.addContextMenu(menu);
// step5
map.addEventListener("click", function(e) {
if (e.overlay && e.overlay instanceof BMapGL.Marker) {
const clickedMarker = e.overlay;
var cur_lon = clickedMarker.getPosition().lng;
var cur_lat = clickedMarker.getPosition().lat;
var clicked_idx = 0;
for (var i = 0; i < pls.length; i++) {
var tmp_lon = pls[i][0];
var tmp_lat = pls[i][1];
if(Math.abs(tmp_lon - cur_lon) < 0.015 && Math.abs(tmp_lat - cur_lat) < 0.015){
clicked_idx = i;
break;
}
}
var str_loc ="经度:" + cur_lon.toFixed(4).toString() + ", 纬度:" + cur_lat.toFixed(4).toString();
map.openInfoWindow(new BMapGL.InfoWindow(str_loc + "<br>时间:" + pls[clicked_idx][4], {title: pls[clicked_idx][2]+pls[clicked_idx][3]}),
new BMapGL.Point(cur_lon, cur_lat));
}
});
</script>
5.小结与效果展示
完成的网页动态展示如下:
可以看到,常规的切换地图、显示信息等功能都可以正常运行。以上便是本次网页开发的记录,理论上也可以扩展为其它基于地图的应用,比如飞机航线可视化、高铁线路可视化等等。
6.参考资料
- [1] https://lbsyun.baidu.com/
- [2] https://lbsyun.baidu.com/index.php?title=open/jsdemo
- [3] https://lbsyun.baidu.com/jsdemo.htm#aCreateMap
- [4] https://lbsyun.baidu.com/jsdemo.htm#bSetGetMapZoom
本文作者原创,未经许可不得转载,谢谢配合