-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstation_data.py
More file actions
198 lines (170 loc) · 7.87 KB
/
Copy pathstation_data.py
File metadata and controls
198 lines (170 loc) · 7.87 KB
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
"""
全国车站坐标数据库
- 首次运行从开源数据库 (Seo-4d696b75/station_database) 下载全日本车站数据,
解析为紧凑的本地缓存 stations.json (name -> [(lat, lng, prefecture), ...])。
- 之后离线可用。
- 提供按名称查坐标(含同名站消歧)、都道府县 -> 地区 映射。
- 下载失败时回退到内置的主要车站表,保证 App 仍可用。
"""
from __future__ import annotations
import json
import os
import threading
import urllib.request
_HERE = os.path.dirname(os.path.abspath(__file__))
# v2: 记录改为 [lat, lng, pref, lines],新增线路字段(用于真实线路轨迹)
_CACHE = os.path.join(_HERE, "stations_v2.json")
# 全量数据源(含 name / lat / lng / prefecture 字段)
_SOURCE_URL = "https://raw.githubusercontent.com/Seo-4d696b75/station_database/main/out/main/station.json"
# ── 都道府县(1-47) → 地区 ─────────────────────────────────────────────────────
# (地区名, 概览图上的近似中心 lat/lon, [都道府县编号...])
REGIONS = [
("北海道", 43.1, 142.4, [1]),
("東北", 39.5, 140.6, [2, 3, 4, 5, 6, 7]),
("関東", 35.9, 139.8, [8, 9, 10, 11, 12, 13, 14]),
("中部", 36.3, 137.9, [15, 16, 17, 18, 19, 20, 21, 22, 23]),
("近畿", 34.7, 135.6, [24, 25, 26, 27, 28, 29, 30]),
("中国", 34.7, 132.7, [31, 32, 33, 34, 35]),
("四国", 33.8, 133.5, [36, 37, 38, 39]),
("九州", 32.2, 130.8, [40, 41, 42, 43, 44, 45, 46]),
("沖縄", 26.3, 127.8, [47]),
]
_PREF_TO_REGION = {}
_REGION_CENTER = {}
for _name, _lat, _lon, _prefs in REGIONS:
_REGION_CENTER[_name] = (_lat, _lon)
for _p in _prefs:
_PREF_TO_REGION[_p] = _name
def region_of_pref(pref: int) -> str:
return _PREF_TO_REGION.get(pref, "その他")
def region_center(name: str):
return _REGION_CENTER.get(name, (37.0, 138.0))
# ── 内置回退表(下载失败时用,覆盖主要车站)──────────────────────────────────
# name -> (lat, lng, prefecture)
_FALLBACK = {
"東京": (35.6812, 139.7671, 13), "新宿": (35.6896, 139.7006, 13),
"渋谷": (35.6580, 139.7016, 13), "池袋": (35.7295, 139.7109, 13),
"品川": (35.6284, 139.7387, 13), "上野": (35.7141, 139.7774, 13),
"秋葉原": (35.6984, 139.7731, 13), "新橋": (35.6661, 139.7579, 13),
"高田馬場": (35.7122, 139.7034, 13), "表参道": (35.6653, 139.7124, 13),
"代々木上原": (35.6690, 139.6800, 13), "田原町": (35.7106, 139.7910, 13),
"本駒込": (35.7330, 139.7470, 13), "飯田橋": (35.7021, 139.7449, 13),
"早稲田": (35.7058, 139.7197, 13), "豊島園": (35.7430, 139.6470, 13),
"横浜": (35.4658, 139.6223, 14), "川崎": (35.5308, 139.7025, 14),
"大宮": (35.9062, 139.6237, 11), "千葉": (35.6133, 140.1130, 12),
"大阪": (34.7024, 135.4959, 27), "梅田": (34.7025, 135.4983, 27),
"難波": (34.6659, 135.5012, 27), "京都": (34.9858, 135.7588, 26),
"三宮": (34.6947, 135.1980, 28), "名古屋": (35.1709, 136.8815, 23),
"札幌": (43.0686, 141.3508, 1), "仙台": (38.2601, 140.8821, 4),
"広島": (34.3975, 132.4753, 34), "博多": (33.5897, 130.4207, 40),
"天神": (33.5912, 130.3990, 40), "那覇": (26.2124, 127.6792, 47),
}
# ── 站库 ──────────────────────────────────────────────────────────────────────
class StationDB:
def __init__(self, table: dict):
# table: name -> list[[lat, lng, pref]]
self._t = {}
self._normalized = {}
for name, lst in table.items():
if isinstance(lst, (list, tuple)) and lst and isinstance(lst[0], (int, float)):
lst = [list(lst)] # 单条 (lat,lng,pref)
values = [tuple(x) for x in lst]
self._t[name] = values
self._normalized.setdefault(self._norm(name), []).extend(values)
@staticmethod
def _norm(name: str) -> str:
if not name:
return ""
return (name.strip()
.replace("ヶ", "ケ").replace("ヵ", "カ")
.replace(" ", ""))
def candidates(self, name: str):
"""返回该站名所有候选 (lat,lng,pref)。"""
if not name:
return []
if name in self._t:
return self._t[name]
return self._normalized.get(self._norm(name), [])
def resolve(self, name: str, anchor=None):
"""选最合适的一个坐标。anchor=(lat,lng) 时选离 anchor 最近的同名站。"""
cands = self.candidates(name)
if not cands:
return None
if anchor is None or len(cands) == 1:
return cands[0]
ay, ax = anchor
return min(cands, key=lambda c: (c[0]-ay)**2 + (c[1]-ax)**2)
def lines_of(self, entry):
"""从一个 resolve() 返回的记录里取线路 line_cd 列表。"""
if entry and len(entry) > 3 and isinstance(entry[3], (list, tuple)):
return list(entry[3])
return []
def __len__(self):
return len(self._t)
# ── 下载 / 加载 ────────────────────────────────────────────────────────────────
def _build_cache_from_source(timeout=90) -> dict:
"""下载全量数据,解析为 name -> [[lat,lng,pref],...],写入缓存。"""
req = urllib.request.Request(_SOURCE_URL, headers={"User-Agent": "SFCardViewerPro/1.0"})
with urllib.request.urlopen(req, timeout=timeout) as r:
raw = r.read()
arr = json.loads(raw.decode("utf-8"))
table: dict = {}
for s in arr:
try:
if s.get("closed"):
continue
name = s["name"]
lat = round(float(s["lat"]), 6)
lng = round(float(s["lng"]), 6)
pref = int(s.get("prefecture", 0))
except (KeyError, TypeError, ValueError):
continue
try:
lines = [int(x) for x in (s.get("lines") or [])]
except (TypeError, ValueError):
lines = []
table.setdefault(name, []).append([lat, lng, pref, lines])
if table:
try:
with open(_CACHE, "w", encoding="utf-8") as f:
json.dump(table, f, ensure_ascii=False, separators=(",", ":"))
except OSError:
pass
return table
def load(force_download=False) -> StationDB:
"""
返回 StationDB。优先用本地缓存;没有则下载;下载失败回退内置表。
这是一个可能联网的阻塞调用,建议放在后台线程里执行。
"""
if not force_download and os.path.exists(_CACHE):
try:
with open(_CACHE, "r", encoding="utf-8") as f:
return StationDB(json.load(f))
except (OSError, json.JSONDecodeError):
pass
try:
table = _build_cache_from_source()
if table:
return StationDB(table)
except Exception:
pass
return StationDB(_FALLBACK)
def has_cache() -> bool:
return os.path.exists(_CACHE)
# 异步加载封装
class AsyncLoader:
def __init__(self):
self.db: StationDB | None = None
self._thread: threading.Thread | None = None
def start(self, on_ready):
def run():
db = load()
self.db = db
on_ready(db)
self._thread = threading.Thread(target=run, daemon=True)
self._thread.start()
if __name__ == "__main__":
db = load()
print(f"车站数: {len(db)} 来源: {'缓存/下载' if len(db) > 100 else '回退表'}")
for n in ("表参道", "代々木上原", "豊島園", "梅田", "札幌"):
print(f" {n}: {db.resolve(n)}")