This commit is contained in:
2026-01-09 22:54:07 +08:00
commit a11d561dc0
10 changed files with 554 additions and 0 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
AMAP_KEY=***

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
.env
!.env*example

6
address.json Normal file
View File

@@ -0,0 +1,6 @@
[
{
"from": "北京市朝阳区望京SOHO",
"to": "杭州市西湖区雅仕苑"
}
]

8
distance-results.json Normal file
View File

@@ -0,0 +1,8 @@
[
{
"from": "北京市朝阳区望京SOHO",
"to": "杭州市西湖区雅仕苑",
"distanceMeters": 1128950.5753619769,
"distanceKm": "1128.95"
}
]

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "test-map-distance",
"version": "0.0.2",
"description": "",
"main": "index.js",
"scripts": {
"pub": "ev deploy ./public -k test-map-distance -v 0.0.2 -u -y y"
},
"keywords": [],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.26.0",
"type": "module",
"dependencies": {
"dotenv": "^17.2.3"
},
"devDependencies": {
"@types/bun": "^1.3.5",
"@types/node": "^25.0.3"
}
}

34
plan/v0.0.1.md Normal file
View File

@@ -0,0 +1,34 @@
# 地图两点距离计算
## 需求
使用高德地图 API实现输入起点地址和终点地址计算两者之间的直线距离。
## 实现方案
1. 使用高德地图**地理编码 API**将地址转换为经纬度坐标
2. 使用**Haversine 公式**计算两点间的直线距离
## API 依赖
- 高德地理编码 API: `https://restapi.amap.com/v3/geocode/geo`
- 需要在高德开放平台申请 API Key
## 输入输出
- 输入:起点地址(字符串)、终点地址(字符串)
- 输出:距离(米/公里)
## 文件结构
```
src/
index.ts # 主逻辑实现
```
## 参考链接- 高德开放平台:
https://console.amap.com/dev/key/app
创建应用类型,选择 Web 服务,获取 API Key。

56
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,56 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
dotenv:
specifier: ^17.2.3
version: 17.2.3
devDependencies:
'@types/bun':
specifier: ^1.3.5
version: 1.3.5
'@types/node':
specifier: ^25.0.3
version: 25.0.3
packages:
'@types/bun@1.3.5':
resolution: {integrity: sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w==}
'@types/node@25.0.3':
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
bun-types@1.3.5:
resolution: {integrity: sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw==}
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
undici-types@7.16.0:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
snapshots:
'@types/bun@1.3.5':
dependencies:
bun-types: 1.3.5
'@types/node@25.0.3':
dependencies:
undici-types: 7.16.0
bun-types@1.3.5:
dependencies:
'@types/node': 25.0.3
dotenv@17.2.3: {}
undici-types@7.16.0: {}

109
public/docs.md Normal file
View File

@@ -0,0 +1,109 @@
# 地图两点距离计算工具
## 简介
使用高德地图 API输入起点地址和终点地址自动计算两者之间的直线距离。
## 快速开始
### 1. 安装 Deno
```bash
# Linux/macOS (WSL)
curl -fsSL https://deno.land/install.sh | sh
# macOS
brew install deno
```
### 2. 配置高德 API Key
**申请步骤:**
1. 访问 [高德开放平台](https://console.amap.com/dev/key/app)
2. 注册/登录账号
3. 点击「控制台」→「应用管理」→「创建新应用」
4. 填写应用名称,点击「添加 Key」
5. 勾选 **Web 服务** 权限
6. 复制生成的 Key
**配置环境变量:**
在项目根目录创建 `.env` 文件:
```bash
AMAP_KEY=你的高德APIKey
```
### 3. 运行程序
```bash
deno run -A https://kevisual.xiongxiao.me/root/test-map-distance/index.ts
```
### 4. 查看结果
程序运行完成后,会在当前目录生成 `distance-results.json` 文件:
```json
[
{
"from": "起点地址",
"to": "终点地址",
"distanceMeters": 1120000,
"distanceKm": "1120.00"
}
]
```
## 本地开发
代码路径https://git.xiongxiao.me/test/test-map-distance
### 目录结构
```
test-map-distance/
├── .env # 环境变量API Key
├── address.json # 地址列表配置
├── distance-results.json # 计算结果(自动生成)
├── plan/
│ └── v0.0.1.md # 设计文档
└── public/
└── docs.md # 使用说明
```
### address.json 格式
```json
[
{ "from": "北京市", "to": "上海市" },
{ "from": "广州市", "to": "深圳市" }
]
```
## 实现原理
1. **地理编码**:使用高德地图 [地理编码 API](https://restapi.amap.com/v3/geocode/geo) 将地址转换为经纬度坐标
2. **距离计算**:使用 Haversine 公式计算两点间的直线距离
```typescript
// Haversine 公式
const R = 6371000; // 地球半径(米)
const dLat = (to.lat - from.lat) * Math.PI / 180;
const dLng = (to.lng - from.lng) * Math.PI / 180;
const a = Math.sin(dLat/2)² + Math.cos(from.lat) * Math.cos(to.lat) * Math.sin(dLng/2)²;
const distance = R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
```
## 常见问题
| 错误 | 原因 | 解决方案 |
|------|------|----------|
| `USERKEY_PLAT_NOMATCH` | API Key 未启用 Web 服务 | 登录高德开放平台,勾选 Web 服务权限 |
| `请在环境变量中设置 AMAP_KEY` | 未配置 .env 文件 | 创建 .env 文件并设置 AMAP_KEY |
| `地址解析失败` | 地址无法被识别 | 使用更详细或更简化的地址 |
## 参考链接
- [高德开放平台](https://lbs.amap.com/)
- [Deno 官方文档](https://deno.com/)

183
public/index.html Normal file
View File

@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>地图距离计算工具 - 使用说明</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Highlight.js -->
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 0;
}
.card {
border: none;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.card-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 16px 16px 0 0 !important;
padding: 25px 30px;
}
.card-header h1 {
margin: 0;
font-size: 1.8rem;
}
.card-body {
padding: 30px;
}
#content {
font-size: 1rem;
line-height: 1.8;
}
#content h1 {
color: #667eea;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
margin-top: 0;
}
#content h2 {
color: #764ba2;
margin-top: 30px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
#content h3 {
color: #555;
margin-top: 20px;
}
#content h4 {
color: #666;
margin-top: 15px;
}
#content p {
margin-bottom: 1rem;
}
#content ul, #content ol {
margin-bottom: 1rem;
padding-left: 25px;
}
#content li {
margin-bottom: 8px;
}
#content a {
color: #667eea;
text-decoration: none;
}
#content a:hover {
text-decoration: underline;
}
#content pre {
border-radius: 10px;
margin: 15px 0;
}
#content code:not(pre code) {
background: #f3f4f6;
color: #e91e63;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.9em;
}
#content table {
margin: 15px 0;
width: 100%;
}
#content th, #content td {
border: 1px solid #dee2e6;
padding: 12px;
text-align: left;
}
#content th {
background: #f8f9fa;
font-weight: 600;
}
#content tr:hover {
background: #f8f9fa;
}
#content blockquote {
border-left: 4px solid #667eea;
padding-left: 20px;
margin: 15px 0;
color: #666;
background: #f8f9fa;
padding: 15px 20px;
border-radius: 0 8px 8px 0;
}
#content img {
max-width: 100%;
border-radius: 8px;
margin: 15px 0;
}
.loading {
text-align: center;
padding: 50px;
color: #666;
}
.spinner-border {
width: 3rem;
height: 3rem;
}
</style>
</head>
<body>
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-10">
<div class="card">
<div class="card-header">
<h1>地图距离计算工具 - 使用说明</h1>
</div>
<div class="card-body" id="content">
<div class="loading">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">加载中...</span>
</div>
<p class="mt-3">文档加载中...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap Bundle JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Marked JS -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- Highlight.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
async function loadDocs() {
try {
const response = await fetch('./docs.md');
if (!response.ok) {
throw new Error('文档加载失败');
}
const markdown = await response.text();
document.getElementById('content').innerHTML = marked.parse(markdown);
// 代码高亮
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
} catch (error) {
document.getElementById('content').innerHTML = `
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">加载失败</h4>
<p>${error.message}</p>
</div>
`;
}
}
loadDocs();
</script>
</body>
</html>

133
public/index.ts Normal file
View File

@@ -0,0 +1,133 @@
// deno run -A https://kevisual.xiongxiao.me/root/test-map-distance/index.ts?a=1
import path from 'node:path';
import fs from 'node:fs';
/**
* 手动读取 .env 文件
*/
function loadEnv() {
const envPath = path.resolve(process.cwd(), '.env');
if (!fs.existsSync(envPath)) {
return {};
}
const content = fs.readFileSync(envPath, 'utf-8');
const env: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const [key, ...valueParts] = trimmed.split('=');
if (key && valueParts.length > 0) {
env[key.trim()] = valueParts.join('=').trim();
}
}
return env;
}
const env = loadEnv();
const AMAP_KEY = env.AMAP_KEY || '';
if (!AMAP_KEY) {
throw new Error('请在环境变量中设置 AMAP_KEY');
}
/**
* 地理编码:将地址转换为经纬度
*/
async function geocode(address: string): Promise<{ lng: number; lat: number } | null> {
const url = `https://restapi.amap.com/v3/geocode/geo?address=${encodeURIComponent(address)}&key=${AMAP_KEY}`;
const res = await fetch(url).then(r => r.json());
if (res.status === '1' && res.geocodes?.length > 0) {
const [lng, lat] = res.geocodes[0].location.split(',').map(Number);
return { lng, lat };
}
return null;
}
/**
* 计算两点间的直线距离(米)
*/
function calculateDistance(
from: { lng: number; lat: number },
to: { lng: number; lat: number }
): number {
const R = 6371000; // 地球半径(米)
const dLat = (to.lat - from.lat) * Math.PI / 180;
const dLng = (to.lng - from.lng) * Math.PI / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(from.lat * Math.PI / 180) *
Math.cos(to.lat * Math.PI / 180) *
Math.sin(dLng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* 主函数:计算两个地址间的距离
*/
async function getDistance(fromAddress: string, toAddress: string) {
const [from, to] = await Promise.all([
geocode(fromAddress),
geocode(toAddress)
]);
if (!from || !to) {
throw new Error('地址解析失败');
}
const distance = calculateDistance(from, to);
return {
from,
to,
distanceMeters: distance,
distanceKm: (distance / 1000).toFixed(2)
};
}
// 使用示例
// getDistance('北京市', '上海市').then(console.log);
// const fromAddress = '北京市朝阳区望京SOHO';
// const toAddress = '杭州市西湖区雅仕苑';
const addressPath = path.resolve(process.cwd(), 'address.json');
type Address = {
from: string;
to: string;
}
let addressJson: Address[];
// 读取 address.json 文件
const addressData = fs.readFileSync(addressPath, 'utf-8');
try {
addressJson = JSON.parse(addressData) as Address[];
} catch (error) {
console.error('无法解析 address.json 文件,请确保其为有效的 JSON 格式');
process.exit(1);
}
if (!Array.isArray(addressJson)) {
console.error('address.json 文件格式错误,应为地址对象数组');
process.exit(1);
}
const results: any[] = [];
for (const addr of addressJson) {
try {
// 计算距离
const result = await getDistance(addr.from, addr.to);
results.push({
from: addr.from,
to: addr.to,
distanceMeters: result.distanceMeters,
distanceKm: result.distanceKm,
});
console.log(`地址对 (${addr.from} -> ${addr.to}) 的距离为: ${result.distanceKm} 公里`);
} catch (error) {
console.error(`计算地址对 (${addr.from} -> ${addr.to}) 距离时出错:`, error);
}
}
// 将结果写入 distance-results.json 文件
const resultPath = path.resolve(process.cwd(), 'distance-results.json');
fs.writeFileSync(resultPath, JSON.stringify(results, null, 2), 'utf-8');
console.log(`距离结果已保存到 ${resultPath}`);