前言

很多年前写过一篇 Google Map 谷歌地图, 这篇算是翻新版本.

Google Map Registration

Google Maps Platform 是整个 Google Map 的大本营. 里面有许许多多的 Services. Embed 和 JavaScript API 只是其中的两个.

首先, Services 不是免费的, 像 Maps JavaScript API 1000 个 request 要 7 块钱美金, 但 Google 每个月有 free USD 200 让你花. 所以可以算是间接免费.

但是申请账户需要绑定银行卡哦.

下图是它的 Documentation

这篇的内容, 大部分都是来自 JS API 和 Embed API 文档.

申请的部分我就不 step by step 讲了. 大概有几个东西需要弄:

1. 申请 Google Cloud (Map 属于 Cloud 的一部分)

2. 绑卡

3. Create New Project

4. Enable Services (Maps Embed / JavaScript API)

5. 做 API Key (API Key 要设定只有在指定的 Domain 内能用哦 (这个叫 Key restrictions), 因为 API Key 是放在 HTML 公开的, 任何人都可以直接拿到.

Google Maps Embed API

Embed Map 就是把 Map 嵌套进网站里. 效果:

它是通过 iframe 完成的. 左上角是一个 location info. 中间有一个 drop point 就是目标地点 (通常就是公司的地址)

要制作 iframe 不需要什么 coding. 用 Generating an iframe 就可以了.

输入查询, 比如公司名字 > 点击 Looks good!

然后输入 API Key

API 在 Cloud 里面

接着 copy iframe 的 code, 放到要 display 的页面区域就可以了.

Options

想配置 config 可以通过 parameters. 参考: Embedding a map

上面我们使用了 iframe generator, 但其实它就是替我们生成 parameters 而已.

下面这个是手写的 iframe src

https://www.google.com/maps/embed/v1/place?q=place_id:ChIJcXt2BFgZ2jERY1RQlGLGTzU&zoom=10&key=your-key

通常可能需要配置的变量是: version, place_id (或者直接用 q, q stand for query), zoom, key, language. 参考: place mode

局限

Embed API 虽然简单, 但是也有不少局限性.

1. 不支持 multiple location. 它只可以放一个地点. 如果公司有许多分行. 那就不够用了.

2. marker (中间的 drop point 术语叫 marker) 没有任何点击事件. location info 在右上角并不显眼. 用户可能会点击 marker, 但却没有任何反应.

而这两个局限可以通过 Maps JavaScript API 来解决.

Google Maps JavaScript API

最终效果

一开始有 3 个 location, 点击 location 会出现 location info.

Simple Map

最终效果有几个难点, 我们先从最简单的开始.

HTML

<body>
<div class="map"></div>
<script type="module" src="./home.ts"></script>
</body>

有一个 div 就可以了,待会儿地图出现在 div 这个位置。

Styles

.map {
width: 768px;
aspect-ratio: 3 / 1;
}

div 需要设一个尺寸,地图大小。

Scripts

安装

yarn add @googlemaps/js-api-loader
yarn add @types/google.maps --dev

import and create Loader

import { Loader } from '@googlemaps/js-api-loader';

const apiKey = 'my-api-key';

// 1. 创建 loader
const loader = new Loader({
apiKey,
version: 'weekly', // by default 也是 'weekly',要指定版本就放 version: '3.40'
});

Loader,顾名思义,它就是用来加载 Google Maps Library 用的。

// 2. 加载 MapsLibrary
const { Map } = await loader.importLibrary('maps');
const mapElement = document.querySelector<HTMLElement>('.map')!; // 3. 使用 MapsLibrary 制作地图
new Map(mapElement, {
// 4. 一定要指定位置和 zoom 哦
center: {
lat: 1.284491,
lng: 103.845951,
},
zoom: 10,
});

我们可以选择在任意时刻去 importLibrary,它是异步的。

效果

Scolling Problem

默认情况下,在 map 上 scrolling 的效果是 zoom。这个体验不一定是用户期望的,因为用户 scroll 更可能是想滚动页面内容。

解决方法是在 config 添加 gestureHandling。

new Map(mapElement, {
gestureHandling: "cooperative",
});

效果

当用户不小心 scroll 到的地图,它不会 zoom,而是会 scroll 页面内容,与此同时会出现提示,告知用户想 zoom 的话,请使用 ctrl + scroll。

Custom Map Styles

上面最终效果的主题颜色并不是默认的,它是灰色系的。

我们到 Google Cloud 去创建特定的 styles。

然后打开 editor

有一堆的 styles 可以调

设定好后 save & publish。

接着我们需要创建一个 Map ID。

然后把 Map ID 和 Styles 关联起来

最后记下 Map ID,并回到我们的代码,设置 Map ID。

new Map(mapElement, {
center: {
lat: 1.284491,
lng: 103.845951,
},
zoom: 10,
gestureHandling: 'cooperative',
mapId : 'my-map-id' // 设置 Map ID
});

这样地图就会有自定义的 styles 了。

Styling without Google Cloud

参考:

New map styles missing global saturation option?

Google maps 3.54 adding style to the map

gmapsbook – Styles & Themes

Google Cloud 的 style editor 不是很方便,它没有提供 theme,只能一个一个小细节去调,这非常的麻烦。

我们还有一个 old school (已废弃,但还能用) 的方法,可以自定义 styles。

去这里 mapstyle.withgoogle.com 设置,然后 copy JSON

 

注:它默认的 labels.icon 会把 Singapore 给 hide 起来,所以我修改了它的 JSON file。

接着把 JSON copy paste 到代码中

const { Map, StyledMapType } = await loader.importLibrary('maps');
const mapElement = document.querySelector<HTMLElement>('.map')!; const mapOptions: google.maps.MapOptions = {
center: {
lat: 1.284491,
lng: 103.845951,
},
zoom: 12,
gestureHandling: 'cooperative',
mapId,
}; const map = new Map(mapElement, mapOptions); const myMapStyle = new StyledMapType(
[], // <-- put json array here
);
map.mapTypes.set('myMapStyle', myMapStyle);
map.setMapTypeId('myMapStyle');

提醒:

MapOptions.styles 是早期的设置 styles 方式,在没有设置 mapId 的情况下才可以这样设置 styles。

如果有了 mapId 就必须使用 map.setMapTypeId 来设置 styles。

mapId 不仅仅是为了 map styles,其它功能也可能需要 mapId,所以 Best Practice 是这样的:

  1. 一定要 set mapId,即便不是为了 styles,它还有其它用途

  2. 有了 mapId 就不可能使用 styles 属性,只可以使用 map.setMapTypeId

Places Library

参考: Docs – Places Library

它是一个 service,可以通过 query 找到 place info。比如 query=my-company-name

它会返回坐标、地址、reviews count、reviews rating、opening hours 等等。

它有许多方法可以用来 search,但目前只用到 Find Place from Query 就足够了。

代码

const map = new Map(mapElement, mapOptions);

const { PlacesService, PlacesServiceStatus } = await loader.importLibrary('places'); // 先 import library

const searchTerm = 'Pestline KL';
const businessName = 'Pestline Sdn Bhd - KL Branch'; const service = new PlacesService(map);
service.findPlaceFromQuery(
{
query: searchTerm,
fields: ['place_id', 'geometry', 'name', 'formatted_address', 'rating', 'user_ratings_total'],
},
(results, status) => {
if (status === PlacesServiceStatus.OK && results) {
console.log('found', results.filter(result => result.name === businessName)[0]);
}
},
);

还有什么可用的 fields 可以参考这里

注: query=my-company-name 是有可能会找到超过 1 个 results 的哦,我会通过 filter business name 来获取准确的那一个。

效果

分别对应了 info window 需要的内容

name, formatted_address, rating, user_ratings_total

view larger map 需要 place_id

directions 则是 name + formatted_address

geometry 是给 marker 用的

注: query=my-company-name 也有可能找不到,或者找错 company 的哦。哪怕你的 query 完全等于你的 google business name 也有可能找错哦。

因为它是 query 全世界,所以可能有时候会傻傻的,一个解决方法是限制它的查询范围。

placesService.findPlaceFromQuery(
{
query: searchTerm,
fields: ['place_id', 'geometry', 'name', 'formatted_address', 'rating', 'user_ratings_total'],
// 设置 radius,限制搜索范围
locationBias: { lat: 3.854687, lng: 102.129477, radius: 1000 * 1000 },
},
(results, status) => {
if (status === PlacesServiceStatus.OK && results) {
console.log('found', results!.filter(result => result.name === businessName)[0]);
}
},
);

locationBias 就是用来限制查找范围的,可以给它一个 origin 加上一个 radius 那范围就是一个圈内查找。我试过确实会比较准确一点。

Marker

const { AdvancedMarkerElement } = await loader.importLibrary('marker'); // 先 import library

new AdvancedMarkerElement({
map,
position: {
lat: 1.284491,
lng: 103.845951,
},
title: 'Uluru', // hover marker 时会出现
});

position 就拿上面 place info 的 geometry。

提醒:要使用 marker 一定要设置 mapId (上面教了) 哦。如果只是为了测试,可以使用 Map.DEMO_MAP_ID 暂时替代。

Marker Styling

参考:

Docs – feedbackBasic marker customization

Docs – feedbackCreate markers with HTML and CSS

默认的 marker 是红色的. 通常和网站主题颜色不搭.

想改颜色的话,可以使用 PinElement

const map = new Map(mapElement, mapOptions);

const { AdvancedMarkerElement, PinElement } = await loader.importLibrary('marker');

// 创建 PinElement
const pin = new PinElement({
// 设置喜欢的颜色
background: 'pink',
borderColor: 'blue',
glyphColor: 'green',
scale: 1.5, // 大小
}); new AdvancedMarkerElement({
map,
position: {
lat: 1.284491,
lng: 103.845951,
},
title: 'Uluru',
content: pin.element, // 把 pin element 放进 content
});

效果

content 属性可以放任意的 HTMLElement,所以可以完全自定义

const img = document.createElement('img');
img.src = 'https://maps.google.com/mapfiles/kml/shapes/airports.png'; new AdvancedMarkerElement({
map,
position: {
lat: 1.284491,
lng: 103.845951,
},
title: 'Uluru',
content: img,
});

放图片也行,效果:

Zoom to All Marker

上面 create map 的时候,我们 hardcode 了 center 和 zoom,这样是不 ok 的。

正确的做法是依据所有的 marker 调整 zoom,尽可能的显示所有 markers。

参考: Stack Overflow – Auto-center map with multiple markers in Google Maps API v3

const map = new Map(mapElement, {
gestureHandling: 'cooperative',
mapId,
maxZoom: 10, // 避免 auto zoom 太大
}); const bounds = new google.maps.LatLngBounds();
bounds.extend({
// 收集 marker 坐标 1
lat: 1.284491,
lng: 103.845951,
});
bounds.extend({
// 收集 marker 坐标 2
lat: 1.284491,
lng: 103.845951,
});
map.fitBounds(bounds); // 调整 map zoom 和 center

主要用到了 LatLngBounds 和 fitBounds 方法,担心自动 zoom 太大的话,可以 set 一个 maxZoom。

Info Window

参考: Docs – Info Windows

const map = new Map(mapElement, mapOptions);

const { AdvancedMarkerElement } = await loader.importLibrary('marker');

const img = document.createElement('img');
img.src = 'https://maps.google.com/mapfiles/kml/shapes/airports.png'; const marker = new AdvancedMarkerElement({
map,
position: {
lat: 1.284491,
lng: 103.845951,
},
title: 'Uluru',
content: img,
gmpClickable: true,
}); // 创建 info window
const infowindow = new google.maps.InfoWindow({
content: '<h1 class="my-h1">Hello World</h1>', // raw html 或者 HTMLElement
}); // 点击事件
marker.addListener('click', () => {
infowindow.open({
anchor: marker,
map,
shouldFocus: false,
});
});

assign raw HTML 给 InfoWindow.content 就可以了。CSS Selector 可以直接 select 到 .map .my-h1。

Google Map Link

最后说一下 reviews,view larger map,directions 的 link

参考:

Stack Overflow – Link to Google Maps Directions using place_id

Stack Overflow – Getting Google Maps link from place_id

Docs – directions

reviews link

const reviewsLink = `https://search.google.com/local/reviews?placeid=${data.placeId}`

placeId 到 place info 里拿.

view larger map link

const viewLargerMapLink = `https://maps.google.com/maps?q=place_id:${data.placeId}`

directions link

const directionsLink = `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(
`${data.name},${data.address}`
)}`

direction 没有用到 placeId,反而是用到了 name 和 formatted_address。

Get Latitude, Longitude by Address

参考: Docs – feedbackGeocoding Service

Cloud 需要 enable Geocoding API。

const { Geocoder, GeocoderStatus } = await loader.importLibrary('geocoding'); // 先 import library
const address = 'Skudai'; // can put full address or keyword e.g. city
const geocoder = new Geocoder();
geocoder.geocode({ address }, (results, status) => {
if (status !== GeocoderStatus.OK) {
window.alert('Get latitude longitude from Google API failure.');
throw new Error('Get latitude longitude from Google API failure');
}
console.log(results![0].geometry.location.toJSON()); // { lat: 1.5343616, lng: 103.6594267}
});

不需要 map 对象,直接使用 Geocoder 就可以了。

反过来用 longitube, latitude 找 address 也是可以

const { Geocoder, GeocoderStatus } = await loader.importLibrary('geocoding');
const geocoder = new Geocoder();
geocoder.geocode({ location: { lat: 123, lng: 123 } }, (results, status) => {
if (status === GeocoderStatus.OK) {
const address = results![0].formatted_address;
console.log(address);
}
});

nearbySearch

参考: Docs – Nearby Search Requests

nearby 的过程是这样的

首先给它一个坐标 lat, lng

再给它一个半径 radius。比如想查询方圆百里就是 坐标 + radius 50km

最后再告诉它你要找什么,比如餐厅、酒店,甚至是一些知名公司,比如苹果专卖店等等。

const map = new Map(mapElement, mapOptions);

const { PlacesService } = await loader.importLibrary('places');
const service = new PlacesService(map);
service.nearbySearch(
{
location: await getLatLngAsync('Taman Universiti'), // getLatLngAsync 上面教过的
keyword: 'KLC Language Centre', // 知名店铺
radius: 50 * 1000, // radius 的单位是 meter, * 1000 相等于 50 km 的意思.
},
(result, _status) => {
console.log(result![0].geometry!.location!.toJSON());
},
);

1. PlacesService 依赖 map 对象, 这个上面教过了, 只是没有教 nearbySearch

2. radius 的单位是 meter

3. 返回的 result 是一个 array, 里面有许多资料, 比如公司的 reviews 等等.

Distance, Duration, Direction

参考: Docs – feedbackDirections Service

Cloud 需要 enable Directions API 哦。

上面搜索到了 nearby, 我们自然希望它按照距离排序, 或者显示距离. 这样用户有个谱可以选择.

const { DirectionsService } = await loader.importLibrary('routes');

const directionsService = new DirectionsService();
directionsService.route(
{
origin: await getLatLngAsync('Taman Universiti'),
destination: result![0].geometry!.location!, // result from nearbySearch
travelMode: google.maps.TravelMode.DRIVING,
},
(result, _status) => {
console.log(result!.routes[0].legs[0].distance!.text); // 5.6 km
console.log(result!.routes[0].legs[0].duration!.text); // 12 mins
},
);

result 里面还有 steps,一步一步带你走到目的地,每个 step 里头也有距离,时间哦。

Google Maps Embed API & JavaScript API的更多相关文章

  1. 高德地图 API JavaScript API

    <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm12.aspx ...

  2. BMap:JavaScript API

    ylbtech-Map-Baidu:JavaScript API JavaScript API百度地图JavaScript API是一套由JavaScript语言编写的应用程序接口,可帮助您在网站中构 ...

  3. ☀【移动】Google Maps JavaScript API v3

    Google Maps JavaScript API v3https://developers.google.com/maps/documentation/javascript/tutorial?hl ...

  4. 国内使用Google Maps JavaScript API

    <!DOCTYPE html> <html> <head> <meta name="viewport" content="ini ...

  5. 如何插入谷歌地图并获取javascript api 秘钥--Google Maps API error: MissingKeyMapError

    参考:https://blog.csdn.net/klsstt/article/details/51744866 Google Maps API error: MissingKeyMapError h ...

  6. [转]MBTiles 离线地图演示 - 基于 Google Maps JavaScript API v3 + SQLite

    MBTiles 是一种地图瓦片存储的数据规范,它使用SQLite数据库,可大大提高海量地图瓦片的读取速度,比通过瓦片文件方式的读取要快很多,适用于Android.IPhone等智能手机的离线地图存储. ...

  7. 谷歌地图,国内使用Google Maps JavaScript API,国外业务

    目前还是得墙 <!DOCTYPE html> <html> <head> <meta name="viewport" content=&q ...

  8. Google Maps API V3 之绘图库 信息窗口

    Google官方教程: Google 地图 API V3 使用入门 Google 地图 API V3 针对移动设备进行开发 Google 地图 API V3 之事件 Google 地图 API V3 ...

  9. Google Maps API V3 之 图层

    Google官方教程: Google 地图 API V3 使用入门 Google 地图 API V3 针对移动设备进行开发 Google 地图 API V3 之事件 Google 地图 API V3 ...

  10. Google Maps API V3 之 路线服务

    Google官方教程: Google 地图 API V3 使用入门 Google 地图 API V3 针对移动设备进行开发 Google 地图 API V3 之事件 Google 地图 API V3 ...

随机推荐

  1. mysql 临时表的好处

    客户端新建了一个会话,这个会话只是服务器与客户端1对1的关系,客户端可能在服务端建立一个临时表,满足客户端处理某些事务的需求,当客户端退出会话后,这个临时表自动drop,没有任何数据信息占用数据库空间 ...

  2. oeasy教您玩转vim - 58 - # 块可视化

    ​ 块可视化编辑 回忆上节课内容 上次我们了解到行可视模式 行可视模式 V 也可配合各种motion o切换首尾 选区的开头和结尾是mark标记 开头是 '< 结尾是 '> 可以在选区内进 ...

  3. 题解:AT_abc357_f [ABC357F] Two Sequence Queries

    题意 维护一个数据结构,支持两个数列的区间求和,和查询区间内两数列各元素积的和. 分析 线段树万岁! 这道题要维护两个序列,所以线段树中要同时存储两个区间和.但还要在维护一个信息,是该区间内两序列元素 ...

  4. c++ 17 demo

    1 // Cpp.cpp : 此文件包含 "main" 函数.程序执行将在此处开始并结束. 2 // 3 4 #include <iostream> 5 #includ ...

  5. 记一次NACOS开放公网访问导致服务器被挖矿的解决流程 [kdcflush] acosd

    前言 事情的起因是这样的,昨天领导找到我说服务器内存满了,影响其他程序正常运行了,让我把测试服务器上之前启动的六个JAVA程序停一下,接着我就登上服务器执行docker compose down把服务 ...

  6. Jmeter函数助手20-eval

    eval函数用于执行变量名.嵌套函数,允许在变量中的字符串中插入变量和函数引用 包含变量和函数引用的文本:填入变量名称或者函数或者字符,可以只填一种也可以组合都填入 1.eval函数填入的是变量名时则 ...

  7. 【Java】Maven模块化工程SSM整合

    创建数据库一个演示表User CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NU ...

  8. 所在单位近日购入Dell poweredge T640型号服务器,安装Ubuntu18.04.5 server操作系统,服务器万兆网卡,网线连接到千兆交换机上,不能识别网卡——解决方案

    如题目所说: 所在单位近日购入Dell poweredge T640型号服务器,安装Ubuntu18.04.5 server操作系统,服务器万兆网卡,网线连接到千兆交换机上,不能识别网卡. 服务器  ...

  9. baselines算法库logger.py模块分析

    baselines根目录下logger.py模块代码: import os import sys import shutil import os.path as osp import json imp ...

  10. baselines库中cmd_util.py模块对atari游戏的包装为什么要分成两部分并在中间加入flatten操作呢?

    如题: cmd_util.py模块中对应的代码: 可以看到不论是atari游戏还是retro游戏,在进行游戏环境包装的时候都是分成两部分的,如atari游戏,第一部分是make_atari,第二部分是 ...