1. 背景介绍

Google Map 的 API 在国内不可用, 社区里面有一些通过反向代理的方式使用 Google Map API 的方式也不稳定, 经常遇到的问题就是ECONNREFUSED. 为了提供稳定的服务本文试图调研 Bing Map API 来替换 Google Map API 的可行性, 结合业务背景, 主要调研替换两个接口:

  • 海拔数据: Elevation API, 可返回地球上某个位置的海拔数据,或沿路径的抽样海拔数据
  • 时区: Time Zone API 服务可接受纬度/经度坐标的 HTTP 请求以及所需的日期和时间。它返回该位置的时区数据,包括世界协调时间 (UTC) 的偏移量和夏令时

2. Bing Map 开发者

与其他服务一样, 使用 Bing Map 需要注册开发者账号并创建一个 Key, 使用 API 时需要提供 Key. 具体获取 Key 的步骤如下:

  • 开发者中心注册账号
  • My Account中选择My keys: 创建账号

完整的帮助文档请查看:Getting a Bing Maps Key

3. 使用和对比

3.1 代码

使用 nodejs 的 axios 客户端发送请求, 详细代码如下:

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
import axios from "axios";

const BingMapsKey = "使用个人开发者中心生成的Key";

// https://learn.microsoft.com/en-us/bingmaps/rest-services/timezone/find-time-zone
const getTimezone = async (point: string, datetime_utc: string) => {
const url = `https://dev.virtualearth.net/REST/v1/TimeZone/${point}?datetime=${datetime_utc}&key=${BingMapsKey}`;
const res = await axios.get(url, {
headers: {
"Content-Type": "application/json",
},
});
const { resourceSets, statusCode, errorDetails } = res.data;
if (errorDetails) {
console.error("%j", errorDetails);
}
if (
statusCode === 200 &&
resourceSets[0]?.resources[0]?.timeZone?.utcOffset
) {
console.log(statusCode);
console.log("%j", resourceSets);
const utcOffsets =
resourceSets[0].resources[0].timeZone.utcOffset.split(":");
const hours =
+utcOffsets[0] > 0
? +utcOffsets[0] + utcOffsets[1] / 60
: +utcOffsets[0] + (-1 * utcOffsets[1]) / 60;
console.log(hours);
} else {
// 没有报错, 但是无数据, 提供坐标和UTC时间不匹配时会出现
console.warn("%j", resourceSets);
}
};

// https://learn.microsoft.com/en-us/bingmaps/rest-services/elevations/get-elevations
const getElevation = async (point: string) => {
const url = `http://dev.virtualearth.net/REST/v1/Elevation/List?points=${point}&key=${BingMapsKey}`;
const res = await axios.get(url, {
headers: {
"Content-Type": "application/json",
},
});
const { resourceSets, statusCode, errorDetails } = res.data;
if (errorDetails) {
console.error("%j", errorDetails);
}
if (statusCode === 200 && resourceSets[0]?.resources[0]?.elevations?.length) {
console.log(statusCode);
console.log("%j", resourceSets);
console.log(resourceSets[0].resources[0].elevations[0]);
} else {
// 没有报错, 但是无数据
console.warn("%j", resourceSets);
}
};

(async () => {
// 北京故宫: 8
// await getTimezone('39.918953,116.397357', new Date().toUTCString())
// 新疆乌鲁木齐: 8
// await getTimezone('43.844556,87.69757', new Date().toUTCString())
// Google: -28800(秒), Bing: -8(小时)
await getTimezone(
"39.6034810,-119.6822510",
new Date(1331766000000).toUTCString()
);

// 北京故宫: 52
// await getElevation('39.918953,116.397357')
// 新疆乌鲁木齐: 882
// await getElevation('43.844556,87.69757')
// Google: 1608.637939453125, Bing: 1609
await getElevation("39.7391536,-104.9847034");
})()
.then(() => {
console.log("all done");
})
.catch((err) => {
// console.error(err)
console.log(err.response.data);
});

3.2 测试说明

  • 海拔数据 Google Map API 取其返回值中的elevation字段, 而 Bing Map API 取elevations数组中的值, 单位都是米
  • 时区数据 Google Map API 取其返回值中的rawOffset字端,单位是秒, 而 Bing Map API 取utcOffset, 是(+/-)hh:mm格式, 上面的代码中将其换算成小时进行比较
  • 代码中使用 Google Map API 中文档中的例子中的数据请求 Bing Map API 并将结果和 Google Map 文档中的案例值进行对比

3.3 测试结论

  • 数据有效性上如果 Google Map 的结果四舍五入和 Bing Map 结果相差不大, 可以完全替换
  • Bing Map API 国内使用更友好, 推荐在国内使用 Bing Map API 替换 Google Map API

3.4 压缩数据

在使用海拔接口时如果点太多可以考虑压缩, 详细查看Point Compression Algorithm, 压缩算法代码(Typescript)如下:

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
const encodePoints = (points: number[][]) => {
let latitude = 0;
let longitude = 0;
const result: string[] = [];

for (const point in points) {
// step 2
const newLatitude = Math.round(points[point][0] * 100000);
const newLongitude = Math.round(points[point][1] * 100000);

// step 3
let dy = newLatitude - latitude;
let dx = newLongitude - longitude;
latitude = newLatitude;
longitude = newLongitude;

// step 4 and 5
dy = (dy << 1) ^ (dy >> 31);
dx = (dx << 1) ^ (dx >> 31);

// step 6
let index = ((dy + dx) * (dy + dx + 1)) / 2 + dy;

while (index > 0) {
// step 7
let rem = index & 31;
index = (index - rem) / 32;

// step 8
if (index > 0) rem += 32;

// step 9
result.push(
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-"[rem]
);
}
}

// step 10
return result.join("");
};

4. 许可证

  • 许可介绍

    • 开发者许可证: 每年少于 125000 个计费请求事务, 免费
    • 企业许可证: 超过则需要请求报价单, 使用企业许可证
  • Bing Map API 请求事务: 在 REST Services 的统计表中, 除了以下几个之外, 其余的都是计费事务

    • RESTImagery-BasicMetadata
    • Route-IsochroneAsyncCallback
    • Routes-OptimizeItineraryAsyncCallbac
    • Route-SnapToRoadAsyncCallback
    • Route-TruckAsyncCallback
    • GeospatialEndpoint

    所以本文使用的时区(RESTTimezone)和海拔(RESTElevations)都是计费事务