udpate
This commit is contained in:
109
public/docs.md
Normal file
109
public/docs.md
Normal 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
183
public/index.html
Normal 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
133
public/index.ts
Normal 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}`);
|
||||
Reference in New Issue
Block a user