build: 切换包管理器至 pnpm 并新增西安参考底图工具与资源

- 移除 npm 锁文件 package-lock.json 与本地 node_modules 目录
- 配置 pnpm 工作区,允许 esbuild 的安装期构建脚本
- 添加 scripts/fetch-reference-maps.py 用于抓取拼接高德地图瓦片生成参考底图
- 生成两张西安参考底图并保存至 public/assets/maps/ 目录
- 更新 CHANGELOG.md 记录版本变更历史
This commit is contained in:
Stephen committed 2026-10-19 00:19:40 +08:00
1 parent b0467054c8
commit 415103a284
8 files changed
+974 -1248

No files matched your search

+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""
抓取高德地图瓦片并拼接为西安参考底图。
文件用途:
- 生成两张高清参考地图(仅作绘制参照与人工核对,不参与游戏运行时加载):
1) 西安全域概览(z12,覆盖游戏 viewBox 对应的真实地理范围);
2) 主城区高清图(z14,覆盖城墙 / 大雁塔 / 曲江 / 大明宫一带)。
核心职责:
1. 由经纬度 bbox 计算指定 zoom 下的瓦片行列范围(Web 墨卡托);
2. 下载高德矢量路网瓦片(style=8,中文标注),失败重试并以灰块占位;
3. 用 PIL 拼接瓦片并在底部加版权署名条("底图数据 © 高德地图")。
主要流程:
python3 scripts/fetch-reference-maps.py
→ 依次抓取两组配置 → 拼接 → 输出到 public/assets/maps/
"""
import io
import math
import time
import urllib.request
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
# 项目根目录(脚本位于 scripts/ 下)。
PROJECT_ROOT = Path(__file__).resolve().parent.parent
# 瓦片输出目录。
OUT_DIR = PROJECT_ROOT / "public" / "assets" / "maps"
# 高德瓦片服务(style=8 标准矢量路网图,中文标注)。
TILE_URL = (
"https://webrd0{sub}.is.autonavi.com/appmaptile"
"?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}"
)
# 请求 UA(遵守瓦片服务礼仪)。
UA = "XianTravelGame/0.1 (local reference fetching; contact: dev)"
# 瓦片尺寸(像素)。
TILE_SIZE = 256
# 单瓦片下载失败重试次数。
MAX_RETRY = 3
def deg2tile(lon: float, lat: float, zoom: int) -> tuple[int, int]:
"""经纬度转 Web 墨卡托瓦片坐标。
Args:
lon: 经度(度)。
lat: 纬度(度)。
zoom: 缩放级别。
Returns:
(x, y) 瓦片行列号(左上角所属瓦片)。
"""
n = 2 ** zoom
x = int((lon + 180.0) / 360.0 * n)
lat_rad = math.radians(lat)
y = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
return x, y
def fetch_tile(zoom: int, x: int, y: int) -> Image.Image | None:
"""下载单张瓦片(子域名轮转与重试)。
高德路网瓦片子域名为 webrd01~webrd04;重试时轮换子域名以规避单点故障。
Args:
zoom: 缩放级别。
x: 瓦片列号。
y: 瓦片行号。
Returns:
成功返回 PIL 图像,最终失败返回 None。
"""
for attempt in range(MAX_RETRY):
sub = (x + y + attempt) % 4 + 1 # 轮转 1-4 子域名,重试时换域。
url = TILE_URL.format(sub=sub, x=x, y=y, z=zoom)
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
with urllib.request.urlopen(req, timeout=10) as resp:
data = resp.read()
return Image.open(io.BytesIO(data)).convert("RGB")
except Exception:
time.sleep(0.5)
return None
def stitch(bbox: dict, zoom: int, out_name: str) -> Path:
"""按 bbox 抓取并拼接整图。
Args:
bbox: {"west": float, "east": float, "south": float, "north": float}。
zoom: 缩放级别。
out_name: 输出文件名。
Returns:
输出文件路径。
"""
x0, y0 = deg2tile(bbox["west"], bbox["north"], zoom)
x1, y1 = deg2tile(bbox["east"], bbox["south"], zoom)
cols, rows = x1 - x0 + 1, y1 - y0 + 1
total = cols * rows
print(f"[{out_name}] z{zoom} 瓦片 {cols}×{rows}({total} 张),范围 x{x0}-{x1} y{y0}-{y1}")
canvas = Image.new("RGB", (cols * TILE_SIZE, rows * TILE_SIZE), "#e8e0d0")
failed = 0
for row in range(rows):
for col in range(cols):
img = fetch_tile(zoom, x0 + col, y0 + row)
if img is None:
failed += 1
draw = ImageDraw.Draw(canvas)
left = col * TILE_SIZE
top = row * TILE_SIZE
draw.rectangle(
[left, top, left + TILE_SIZE - 1, top + TILE_SIZE - 1],
fill="#d0d0d0",
)
else:
canvas.paste(img, (col * TILE_SIZE, row * TILE_SIZE))
time.sleep(0.08) # 温和限速。
if failed:
print(f"[{out_name}] 警告:{failed}/{total} 张瓦片下载失败(已用灰块占位)")
add_attribution(canvas)
out_path = OUT_DIR / out_name
canvas.save(out_path, optimize=True)
print(f"[{out_name}] 已保存 {canvas.width}×{canvas.height} → {out_path}")
return out_path
def add_attribution(canvas: Image.Image) -> None:
"""在拼接图底部叠加版权署名条。
Args:
canvas: 待标注的拼接图。
"""
bar_h = 34
draw = ImageDraw.Draw(canvas)
draw.rectangle([0, canvas.height - bar_h, canvas.width, canvas.height], fill=(255, 253, 247))
text = "底图数据 © 高德地图 | 仅作项目内部绘制参考,禁止商用分发"
font = None
for candidate in ("PingFang.ttc", "Hiragino Sans GB.ttc", "Arial Unicode.ttf"):
try:
font = ImageFont.truetype(candidate, 16)
break
except OSError:
continue
if font is None:
font = ImageFont.load_default()
draw.text((12, canvas.height - bar_h + 8), text, fill=(107, 78, 46), font=font)
def main() -> None:
"""入口:依次生成全域概览与主城区高清图。"""
OUT_DIR.mkdir(parents=True, exist_ok=True)
# 全域概览(z12):对应游戏 viewBox 的真实地理范围(西至沣河,东至兵马俑)。
stitch(
bbox={"west": 108.82, "east": 109.31, "south": 34.14, "north": 34.42},
zoom=12,
out_name="xian-reference-full-z12.png",
)
# 主城区高清(z14):城墙 / 回民街 / 大明宫 / 小雁塔 / 陕历博 / 大雁塔 / 曲江。
stitch(
bbox={"west": 108.87, "east": 109.04, "south": 34.19, "north": 34.33},
zoom=14,
out_name="xian-reference-city-z14.png",
)
if __name__ == "__main__":
main()