谷歌地图浅尝试
发表于:2025-03-12 |

前言

本篇文章记录一下我最近搞的谷歌地图,因为公司要做外国项目,免费的天地图没法使用,加上谷歌地图的 UI 我还是比较喜欢的,而且每个月免费额度挺多的,就稍微研究了一下。

注册谷歌地图 API

这个我不多说,你需要翻墙和一张海外的卡。然后可以得到一个 key。

得到 URL

这里的??就是你的 key,后面的 maps 和 marker 是我要用到的插件,maps 用来绘图,marker 是标记。
此时你会得到一个 URL’https://maps.googleapis.com/maps/api/js?key=??&libraries=maps,marker‘;

异步加载地图

常规加载

我们一般情况下是直接在 html 文件中引入就可以了

1
2
3
4
5
<script
src="https://maps.googleapis.com/maps/api/js?key=??&libraries=maps,marker"
defer
async
></script>

异步加载

但是我想要能在页面中切换,所以我需要异步导入,这里以英语和中文为例,此时代码如下

1
2
3
4
5
6
7
8
9
const BASE_GOOLE_URL =
"https://maps.googleapis.com/maps/api/js?key=??&libraries=maps,marker";
let loadUrl = BASE_GOOLE_URL;
// 判断是否是英文
if (locale.value === "en_US") {
loadUrl += "&language=en";
} else if (locale.value === "zh_CN") {
loadUrl += "&language=zh-CN";
}

更多语言

如果需要其他的语言,可以参考:https://developers.google.cn/admin-sdk/directory/v1/languages?hl=en

异步加载方法

为了避免重复加载,加了个 id,有这个 id 了就不再加载

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
// 异步加载谷歌地图
export function loadGoogleScript(url) {
return new Promise((resolve, reject) => {
// 判断是否有id为googleMapScript的script标签,有的话就不再加载
const googleMapScript = document.getElementById("googleMapScript");
if (googleMapScript) {
// 判断language是否一致
if (googleMapScript.src === url) {
return resolve("loading success");
} else {
googleMapScript.remove();
}
}
const script = document.createElement("script");
script.src = url;
script.defer = true;
script.id = "googleMapScript";

script.onload = () => {
resolve("loading success");
};
script.onerror = (error) => {
console.error("Error loading lib from " + url, error);
reject(error);
};
document.head.appendChild(script);
});
}

初始化地图

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
let temp;
let googleMap;
const siteMapContainer = ref();
const initMap = async () => {
// 把center置空
center.lat = 0;
center.lng = 0;
const windowTemp = window;
// 检查Google Maps API是否加载完成
if (!windowTemp.google) {
NotifyPlugin.error({
title: "Maps API not loaded",
});
return;
}

// 初始化地图
temp = windowTemp.google;
googleMap = new temp.maps.Map(siteMapContainer.value, {
center: { lat: 30.2, lng: 120.4 }, // 设置中心为中国
zoom: 3, // 放大比例
mapId: "DEMO_MAP_ID", // 需要id才能使用marker
minZoom: 3, // 最小比例
maxZoom: 10, // 最大比例
zoomControl: true, // 比例控制器
streetViewControl: false, // 街道视角控制器
cameraControl: false, // 相机控制器
fullscreenControl: false, // 全屏按钮
mapTypeControl: false, // 地图类型
});
};
onMounted(() => {
initMap();
});
1
2
3
<template>
<div ref="siteMapContainer" id="siteMapContainer"></div>
</template>

这里的控件,你可以按照你的喜好调整,可以参考:https://developers.google.cn/maps/documentation/javascript/reference/control?hl=zh_cn

此时完整代码如下:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<script setup>
import { onMounted, ref } from "vue";

const locale = ref("zh_CN");

const BASE_GOOLE_URL =
"https://maps.googleapis.com/maps/api/js?key=??&libraries=maps,marker";

let loadUrl = BASE_GOOLE_URL;

// 判断是否是英文
if (locale.value === "en_US") {
loadUrl += "&language=en";
} else if (locale.value === "zh_CN") {
loadUrl += "&language=zh-CN";
}

// 异步加载谷歌地图
function loadGoogleScript(url) {
return new Promise((resolve, reject) => {
// 判断是否有id为googleMapScript的script标签,有的话就不再加载
const googleMapScript = document.getElementById("googleMapScript");
if (googleMapScript) {
// 判断language是否一致
if (googleMapScript.src === url) {
return resolve("loading success");
} else {
googleMapScript.remove();
}
}
const script = document.createElement("script");
script.src = url;
script.defer = true;
script.id = "googleMapScript";

script.onload = () => {
resolve("loading success");
};
script.onerror = (error) => {
console.error("Error loading lib from " + url, error);
reject(error);
};
document.head.appendChild(script);
});
}
let temp;
let googleMap;
let center = { lat: 0, lng: 0 };
const siteMapContainer = ref();
const initMap = async () => {
// 把center置空
center.lat = 0;
center.lng = 0;
const windowTemp = window;
// 检查Google Maps API是否加载完成
if (!windowTemp.google) {
NotifyPlugin.error({
title: "Maps API not loaded",
});
return;
}

// 初始化地图
temp = windowTemp.google;
googleMap = new temp.maps.Map(siteMapContainer.value, {
center: { lat: 30.2, lng: 120.4 },
zoom: 3, // 放大比例
mapId: "DEMO_MAP_ID", // 需要id才能使用marker
minZoom: 3, // 最小比例
maxZoom: 10, // 最大比例
zoomControl: true, // 比例控制器
streetViewControl: false, // 街道视角控制器
cameraControl: false, // 相机控制器
fullscreenControl: false, // 全屏按钮
mapTypeControl: false, // 地图类型
});
};
onMounted(async () => {
await loadGoogleScript(loadUrl);
initMap();
});
</script>

<template>
<div ref="siteMapContainer" id="siteMapContainer"></div>
</template>

<style scoped>
#siteMapContainer {
height: 100vh;
width: 100%;
display: block;
position: relative;
}
</style>

这里,我们已经可以将谷歌地图加载出来了
初始化地图

这时候我们已经加载完地图了,但是左下角右下角有一些 logo,我们可以自己设置 css 给它干掉

1
2
3
4
5
6
7
8
#siteMapContainer .gm-style-cc {
display: none;
}
#siteMapContainer {
a {
display: none !important;
}
}

去除无效logo

json 格式

我项目里面自然是发请求得到 JSON 的,这里我就不拿出接口了,给大家看看 json 的格式即可
json格式

添加 markers 和 line

因为我的线是多段的,所以我有一些特殊处理,怕大家看不懂,我先把这段处理代码贴上来,如果大家自己绘制不是多段的可以不用这样,我的轨迹是最新的在前面,所以为了得到真正的首尾节点,我需要 reverse 一下。

处理 markers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const markers = ref([]);
// 绘制标记点
const drawMarkers = (trackList) => {
// 添加mark
trackList.reverse().forEach((item, index) => {
const { lon, lat } = item;
if (index === 0 || index === trackList.length - 1) {
markers.value.push({
position: { lat: Number(lat), lng: Number(lon) },
...item,
});
}
});
};

绘制线

将我们的 trackList 传入,即可绘制线

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
// 绘制线
const drawLines = async (trackList) => {
// 添加轨迹线
const { Polyline } = await temp.maps.importLibrary("maps");
// 创建轨迹线
const path = trackList.map((item) => ({
lat: Number(item.lat),
lng: Number(item.lon),
}));
const polyline = new Polyline({
path,
geodesic: true,
strokeColor: "#FF0000", // 线颜色
strokeOpacity: 1.0, // 线透明度
strokeWeight: 2, // 线宽度
map: googleMap, // 地图
icons: [
{
icon: {
path: temp.maps.SymbolPath.CIRCLE,
scale: 0, // 设置为0以隐藏默认图标
},
offset: "0%",
},
{
icon: { path: temp.maps.SymbolPath.FORWARD_CLOSED_ARROW },
offset: "20%", // 箭头
},
{
icon: { path: temp.maps.SymbolPath.FORWARD_CLOSED_ARROW },
offset: "40%", // 箭头
},
{
icon: { path: temp.maps.SymbolPath.FORWARD_CLOSED_ARROW },
offset: "60%", // 箭头
},
{
icon: { path: temp.maps.SymbolPath.FORWARD_CLOSED_ARROW },
offset: "80%", // 箭头
},
],
});
return {
path,
polyline,
};
};

此时效果图如下
绘制轨迹

上面的 icons 我简单说一下,因为我想要带箭头的轨迹线,所以在 20,40,60,80 的地方加了箭头,又因为我后续要加图标沿着轨迹线运动,所以在 0%的地方预留了这个 icon 的位置

这里给大家贴一个网址,绘制谷歌地图自带的 svg 样式的:https://developers.google.cn/maps/documentation/javascript/examples/overlay-symbol-arrow?hl=en

调整中心点

谷歌地图自带 setCenter 方法,可以调整中心点

1
2
3
4
5
6
7
8
9
10
11
12
// 根据所有节点得到中心点
if (!isEmpty(allList)) {
allList.forEach((item: any) => {
center.lat += Number(item.lat);
center.lng += Number(item.lon);
});
// 调整中心点
googleMap.setCenter({
lat: center.lat / allList.length,
lng: center.lng / allList.length,
});
}

绘制标记

这里给大家介绍俩种方法,一种是自己的图片,一种是谷歌地图自带的标记,自带的就是自己设置样式,创建 img 标签传给谷歌地图,如果想要添加文字的话也可以这样操作,添加 class 后,自己定义样式。如果是谷歌自带的标记,就很简单了,配置一些参数即可,这里给大家贴一个谷歌地图自带标记配置的网址:https://developers.google.cn/maps/documentation/javascript/examples/advanced-markers-basic-style?hl=en

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
if (!isEmpty(markers.value)) {
const startImg = document.createElement("img");
// 使用本地地址
startImg.src = new URL("@/assets/track/start.png", import.meta.url).href;
startImg.style.width = "50px";
const endImg = document.createElement("img");
endImg.src = new URL("@/assets/track/end.png", import.meta.url).href;
endImg.style.width = "50px";
const curImg = document.createElement("img");
curImg.src = new URL("@/assets/track/cur.png", import.meta.url).href;
curImg.style.width = "40px";
// 起始港
const startPort = props.detailInfo[0].shipFrom;
// 终点港
const endPort = props.detailInfo[0].shipTo;
// 添加mark
markers.value.forEach((item, index) => {
if (index == 0) {
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
title: "Start",
content: startImg,
});
const _div = document.createElement("div");
_div.className = "text";
_div.textContent =
startPort +
`(` +
dayjs(Number(item.createdAt)).format("YYYY-MM-DD HH:mm:ss") +
")";
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
content: _div,
});
} else if (index == markers.value.length - 1) {
// 使用第一个轨迹的时间来做判断(这里的轨迹是顺序的)
const firstRoute = props.detailInfo[0].ctnLogs;
// 判断最后一个时间是否比当前时间大
const lastTime = firstRoute[firstRoute.length - 1].actTime;
const isLastTime =
lastTime && dayjs(lastTime).valueOf() > dayjs().valueOf();
const _div = document.createElement("div");
_div.className = "text";
if (isLastTime) {
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
title: "Current",
content: curImg,
});
_div.textContent = dayjs(Number(item.createdAt)).format(
"YYYY-MM-DD HH:mm:ss"
);
} else {
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
title: "End",
content: endImg,
});
_div.textContent =
endPort +
`(` +
dayjs(Number(item.createdAt)).format("YYYY-MM-DD HH:mm:ss") +
")";
}
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
content: _div,
});
} else {
const pinTextGlyph = new PinElement({
glyph: "T",
glyphColor: "white",
background: "#003cab",
});
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
title: "T/S",
content: pinTextGlyph.element,
});
const _div = document.createElement("div");
_div.className = "text";
_div.textContent = dayjs(Number(item.createdAt)).format(
"YYYY-MM-DD HH:mm:ss"
);
new AdvancedMarkerElement({
map: googleMap,
position: item.position,
content: _div,
});
}
});
}
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
#siteMapContainer {
a {
display: none !important;
}
.text {
background-color: #3567dfcc;
color: #fff;
font-size: 14px;
border-radius: 3px;
}
/* HTML marker styles */
.tag {
background-color: #4285f4;
border-radius: 8px;
color: #ffffff;
font-size: 14px;
padding: 10px 15px;
position: relative;
}

.tag::after {
content: '';
position: absolute;
left: 50%;
top: 100%;
transform: translate(-50%, 0);
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 8px solid #4285f4;
}
}

此时轨迹线和标记都打上了
效果图

添加沿轨迹运动的小船

然后我们添加沿轨迹运动的小船

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 创建动画
if (allPaths.length > 0) {
// 创建船舶图标
const shipIcon = document.createElement("img");
const shipImg = new URL("@/assets/track/ship.png", import.meta.url).href;
shipIcon.src = shipImg;
shipIcon.style.width = "32px";
shipIcon.style.height = "32px";
const { spherical } = await temp.maps.importLibrary("geometry");
// 判断初始方向
const startPoint = allPaths[0];
const nextPoint = allPaths[1];
const initialHeading = spherical.computeHeading(
new temp.maps.LatLng(startPoint.lat, startPoint.lng),
new temp.maps.LatLng(nextPoint.lat, nextPoint.lng)
);

// 如果初始航向是向左的(heading < -90 || heading > 90),则图片朝左,否则朝右
const initialRotation =
initialHeading < -90 || initialHeading > 90 ? 90 : -90;
shipIcon.style.transform = `rotate(${initialRotation}deg)`;

// 创建移动的船舶标记
const shipMarker = new AdvancedMarkerElement({
map: googleMap,
content: shipIcon,
});

// 动画函数
let start: number | null = null;
const duration = 8000; // 动画持续时间,可以根据需要调整
let animateNum = 0;
const startAnimateTime = Number(allList[0].createdAt);
const endAnimateTime = Number(allList[allList.length - 1].createdAt);
let animateIndex = 0;
const animate = (timestamp: number) => {
if (!start) start = timestamp;
const progress = (timestamp - start) / duration;

if (progress >= 1) {
cancelAnimationFrame(animateNum);
// 最后一个点
emits("showInfo", true);
shipIcon.style.transform = `rotate(0deg)`;
// 多段的最后根据最后一段调整视角
if (splitPaths.length > 1) {
const bounds = new temp.maps.LatLngBounds();
[...splitPaths[splitPaths.length - 1]].forEach((item) => {
bounds.extend(new temp.maps.LatLng(item.lat, item.lng));
});
googleMap.fitBounds(bounds);
}
// 最后一个点
shipMarker.position = allPaths[allPaths.length - 1];
return;
}
const currentTime =
startAnimateTime + progress * (endAnimateTime - startAnimateTime);
// 得到距离当前时间最近的点
const currentIndex = allList.findIndex((item: any, index: number) => {
if (index < animateIndex) {
return false;
}
return Number(item.createdAt) >= currentTime;
});
const currentPosition = allPaths[currentIndex];
if (currentPosition) {
shipMarker.position = currentPosition;

if (currentIndex < allPaths.length - 1) {
const nextPosition = allPaths[currentIndex + 1];
const heading = spherical.computeHeading(
new temp.maps.LatLng(currentPosition.lat, currentPosition.lng),
new temp.maps.LatLng(nextPosition.lat, nextPosition.lng)
);

// 根据航向调整图片朝向
const rotation =
heading < -90 || heading > 90 ? heading + 90 : heading - 90;
shipIcon.style.transform = `rotate(${rotation}deg)`;
}
}

animateNum = requestAnimationFrame(animate);
};

const bounds = new temp.maps.LatLngBounds();
allPaths.forEach((item) => {
bounds.extend(new temp.maps.LatLng(item.lat, item.lng));
});
googleMap.fitBounds(bounds);
// 监听地图的 idle 事件
temp.maps.event.addListenerOnce(googleMap, "idle", function () {
// 开始动画
animateNum = requestAnimationFrame(animate);
});
}

这里的 allPath 是多段轨迹合并的线段,splitPath 的多段轨迹分开的格式,相当于一个是[…a,…b],另外一个是[[a],[b]],a 和 b 都是上面轨迹格式的数组。

这里我写了个简单的算法,根据轨迹传回来的 createAt 的时间点控制小船的运动速度,停留时间等。

1
2
3
4
allPaths.forEach((item) => {
bounds.extend(new temp.maps.LatLng(item.lat, item.lng));
});
googleMap.fitBounds(bounds);

这段代码就是根据这段轨迹线来自动调整地图的比例和中心点位置,这个点谷歌地图倒是让人挺省事的。剩下的都是我自己写的动画帧代码了,最终效果如下。

添加运动节点展示

最后在左侧加上css即可,这里代码就不展示了,注意一个点,zIndex要大一些,不然会显示不了,我通过上面的动画监听得到了轨迹动画结束的时候,这时候传一个showInfo给父组件展示左侧的样式,基本逻辑就是这样。

效果图

结语

本篇文章就到这里了,简单记录了一下我用谷歌地图的代码,更多内容敬请期待,债见。

下一篇:
【项目配置】vite-plugin-singlefile