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

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}`);