-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjp_network.py
More file actions
576 lines (534 loc) · 30 KB
/
Copy pathjp_network.py
File metadata and controls
576 lines (534 loc) · 30 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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
"""
日本铁路线路网络 — 从 piuccio/open-data-jp-railway-stations 拉全量,
按 line_code 分组, 用 ekidata_id 末 2 位(站序)排序, 构建每条线的有序站序列(几何)。
缓存:
- lines_cache_jp/stations.json (源, ~6MB, 首次联网下载)
- lines_cache_jp/lines.json (解析后, ~100KB, 145 条 line)
对外接口:
- load_lines(): 返回 [{code, name, points:[(lat,lon),...], n}, ...]
- load_colors(): 返回 {line_code: '#RRGGBB'} 公司/线路代表色
- nearby_lines(bounds, source): 给定视野矩形 + 当前底图(amap/osm),
返 {wsen, neen, lines: [{name, points, color, code}, ...]}
"""
from __future__ import annotations
import json
import math
import os
import urllib.request
from collections import defaultdict
_THIS = os.path.dirname(os.path.abspath(__file__))
# 缓存放可写的 app 数据目录(打包后模块目录是只读 bundle, 写不进去)
try:
import paths
_CACHE_DIR = os.path.join(paths.app_data_dir(), "lines_cache_jp")
except Exception:
_CACHE_DIR = os.path.join(_THIS, "lines_cache_jp")
_SRC_URL = "https://raw.githubusercontent.com/piuccio/open-data-jp-railway-stations/master/stations.json"
_SRC = os.path.join(_CACHE_DIR, "stations.json")
_LINES = os.path.join(_CACHE_DIR, "lines_v3.json") # v3: 真实 ekidata 顺序 + 每点站名(供高亮沿网格切段)
_lines_cache = None
# 公司 / 线路代表色(主要 JR/私铁, 资料来源: 维基百科 + 官方网站)
# 凡是代码中查不到的, fallback 用 ROUTE_GREEN
COLORS = {
# JR 北海道
"JR-Hokkaido.Hakodate": "#EB1C24", "JR-Hokkaido.Sassho": "#EB1C24",
"JR-Hokkaido.Chitose": "#F38B14", "JR-Hokkaido.Sekisho": "#F38B14",
# JR 东
"JR-East.Yamanote": "#9ACD43", "JR-East.ChuoRapid": "#F15A22",
"JR-East.ChuoSobuLocal": "#FFD400", "JR-East.Chuo": "#F15A22",
"JR-East.Sobu": "#FFD400", "JR-East.KeihinTohoku": "#00B2DD",
"JR-East.KeihinTohokuNegishi": "#00B2DD", "JR-East.Tokaido": "#F18E40",
"JR-East.Yokosuka": "#E3522F", "JR-East.Saikyo": "#2DC4A4",
"JR-East.SaikyoKawagoe": "#2DC4A4", "JR-East.Utsunomiya": "#F18E40",
"JR-East.Takasaki": "#F18E40", "JR-East.Joban": "#808080",
"JR-East.JobanLocal": "#808080", "JR-East.JobanRapid": "#808080",
"JR-East.Keiyo": "#D80073", "JR-East.Musashino": "#D80073",
"JR-East.Narita": "#D80073", "JR-East.NaritaAbikoBranch": "#D80073",
"JR-East.NaritaAirportBranch": "#D80073", "JR-East.Yokohama": "#B6B6B6",
"JR-East.Nambu": "#FFD400", "JR-East.NambuBranch": "#FFD400",
"JR-East.Ome": "#F18E40", "JR-East.Itsukaichi": "#F18E40",
"JR-East.Hachiko": "#D7B268", "JR-East.Kawagoe": "#D7B268",
"JR-East.Sotobo": "#E95464", "JR-East.Uchibo": "#E95464",
"JR-East.ShinganMon": "#E2585E", "JR-East.Tohoku": "#E2585E",
"JR-East.Ou": "#E2585E", "JR-East.Senseki": "#3CB371",
"JR-East.Senzan": "#F18E40", "JR-East.Tsurumi": "#F18E40",
"JR-East.Tokaido": "#F18E40", "JR-East.ShonanShinjuku": "#E2585E",
"JR-East.UenoTokyo": "#F18E40", "JR-East.Tazawako": "#E2585E",
"JR-East.Kesennuma": "#3CB371", "JR-East.Ofunato": "#3CB371",
"JR-East.Yamada": "#3CB371", "JR-East.Ishinomaki": "#3CB371",
"JR-East.Kitakami": "#3CB371", "JR-East.Hanawa": "#3CB371",
"JR-East.Ominato": "#3CB371", "JR-East.Gono": "#E2585E",
"JR-East.Tsugaru": "#E2585E", "JR-East.Hachinohe": "#3CB371",
"JR-East.Aterazawa": "#E2585E", "JR-East.Yonesaka": "#E2585E",
"JR-East.RikuuEast": "#E2585E", "JR-East.RikuuWest": "#E2585E",
"JR-East.BanetsuEast": "#E2585E", "JR-East.BanetsuWest": "#E2585E",
"JR-East.Tadami": "#E2585E", "JR-East.Suigun": "#E2585E",
"JR-East.Mito": "#E2585E", "JR-East.Nikko": "#E2585E",
"JR-East.Ryomo": "#E2585E", "JR-East.Joetsu": "#E2585E",
"JR-East.Agatsuma": "#E2585E", "JR-East.Karasuyama": "#E2585E",
"JR-East.Kashima": "#E2585E", "JR-East.Kururi": "#E2585E",
"JR-East.Togane": "#E2585E",
# JR 中央
"JR-Central.Tokaido": "#F18E40", "JR-Central.Mikawa": "#F18E40",
"JR-Central.Iida": "#AB3A2F", "JR-Central.Minobu": "#F18E40",
"JR-Central.Kansai": "#F18E40", "JR-Central.Kisei": "#F18E40",
"JR-Central.Taketoyo": "#F18E40", "JR-Central.Sangu": "#F18E40",
"JR-Central.Meisho": "#F18E40", "JR-Central.Ito": "#F18E40",
"JR-Central.Gotemba": "#F18E40", "JR-Central.Taita": "#F18E40",
"JR-Central.Takayama": "#76BC21", "JR-Central.Himi": "#76BC21",
"JR-Central.Johana": "#76BC21", "JR-Central.Tsuruga": "#76BC21",
"JR-Central.Hokuriku": "#76BC21", "JR-Central.Oito": "#76BC21",
# JR 西
"JR-West.Sanin": "#9078AF", "JR-West.Sanyo": "#F8A11A",
"JR-West.OsakaLoop": "#E80033", "JR-West.Hanwa": "#4D8C49",
"JR-West.Kansai": "#F8A11A", "JR-West.Yamatoji": "#E80033",
"JR-West.Takarazuka": "#E80033", "JR-West.Fukuchiyama": "#E80033",
"JR-West.Sagano": "#9078AF", "JR-West.Biwako": "#E60012",
"JR-West.Kyoto": "#E60012", "JR-West.Kobe": "#E60012",
"JR-West.Nara": "#E60012", "JR-West.Sakurai": "#E60012",
"JR-West.KansaiAirport": "#E60012", "JR-West.Wakayama": "#4D8C49",
"JR-West.Maizuru": "#9078AF", "JR-West.Hagoromo": "#4D8C49",
"JR-West.Tokaido": "#F18E40", "JR-West.SanyoShinkansen": "#0F64B0",
"JR-West.HokurikuShinkansen": "#0F64B0", "JR-West.Yosan": "#A8C100",
"JR-West.Dosan": "#A8C100", "JR-West.Tokushima": "#0099CC",
"JR-West.Kotoku": "#0099CC", "JR-West.Mugi": "#0099CC",
"JR-West.Naruto": "#0099CC", "JR-West.Kibi": "#FF7E00",
"JR-West.Ako": "#F8A11A", "JR-West.Bantan": "#F8A11A",
"JR-West.Kishin": "#F8A11A", "JR-West.Hakubi": "#9078AF",
"JR-West.Inbi": "#9078AF", "JR-West.Sakai": "#9078AF",
"JR-West.Kisuki": "#9078AF", "JR-West.Yamaguchi": "#E2585E",
"JR-West.Uno": "#FF7E00", "JR-West.Kure": "#E2585E",
"JR-West.Kabe": "#E2585E", "JR-West.Fukuen": "#E2585E",
"JR-West.Iwami": "#9078AF", "JR-West.SaninShinkansen": "#0F64B0",
"JR-West.YamaguchiShinkansen": "#0F64B0", "JR-West.NishiKyushuShinkansen": "#0F64B0",
# JR 九州
"JR-Kyushu.Kagoshima": "#EE3D38", "JR-Kyushu.Nippo": "#3CB371",
"JR-Kyushu.Nagasaki": "#F8A11A", "JR-Kyushu.Sasebo": "#F8A11A",
"JR-Kyushu.Kyudai": "#FF7E00", "JR-Kyushu.Hitahikosan": "#3CB371",
"JR-Kyushu.Gotoji": "#F8A11A", "JR-Kyushu.Kashii": "#FF7E00",
"JR-Kyushu.Chikuhi": "#F8A11A", "JR-Kyushu.Hohi": "#76BC21",
"JR-Kyushu.Misumi": "#3CB371", "JR-Kyushu.IbusukiMakurazaki":"EE3D38",
"JR-Kyushu.KyushuShinkansen": "#EE3D38", "JR-Kyushu.NishiKyushuShinkansen": "#EE3D38",
"JR-Kyushu.Chikuho": "#3CB371", "JR-Kyushu.FukuhokuYutaka": "#3CB371",
"JR-Kyushu.Karatsu": "#F8A11A", "JR-Kyushu.Omura": "#F8A11A",
"JR-Kyushu.Sasaguri": "#3CB371", "JR-Kyushu.Tagawa": "#3CB371",
"JR-Kyushu.Itoda": "#3CB371", "JR-Kyushu.Gotanji": "#3CB371",
"JR-Kyushu.Mojiko": "#3CB371", "JR-Kyushu.TenjinOmuta": "#3CB371",
"JR-Kyushu.kyudaiMain": "#FF7E00",
# 新干线
"JR-East.TohokuShinkansen": "#0F64B0", "JR-East.JoetsuShinkansen": "#0F64B0",
"JR-East.HokurikuShinkansen": "#0F64B0", "JR-East.YamagataShinkansen":"0F64B0",
"JR-East.AkitaShinkansen": "#0F64B0", "JR-Central.TokaidoShinkansen": "#F18040",
"JR-Central.TokaidoShinkansens": "#F18040", "JR-Central.TokaidoShinkansensBranch": "#F18040",
"JR-Hokkaido.HokkaidoShinkansen": "#0F64B0",
# 东京地铁
"TokyoMetro.Ginza": "#FF9500", "TokyoMetro.Marunouchi": "#F62E36",
"TokyoMetro.Hibiya": "#B5B5AC", "TokyoMetro.Tozai": "#009BBF",
"TokyoMetro.Chiyoda": "#00BB85", "TokyoMetro.Yurakucho": "#C1A470",
"TokyoMetro.Hanzomon": "#8F76D6", "TokyoMetro.Namboku": "#00AC9B",
"TokyoMetro.Fukutoshin": "#9C5E31",
# 都营
"Toei.Asakusa": "#EC619F", "Toei.Mita": "#0079C2",
"Toei.Shinjuku": "#6CBB5A", "Toei.Oedo": "#B6007A",
# 其他主要私铁
"Tokyu.Toyoko": "#DA0042", "Tokyu.Denentoshi": "#20A286",
"Tokyu.Oimachi": "#EE8E33", "Tokyu.Meguro": "#0072BA",
"Tokyu.TokyuTamagawa": "#AE0072", "Tokyu.Ikegami": "#E5171F",
"Tokyu.Poolyakken": "#E5171F", "Tokyu.Setagaya": "#FF6E00",
"Keio.Keio": "#C00", "Keio.KeioNew": "#C00",
"Keio.Sagamihara": "#C00", "Keio.KeioDobutsuen": "#C00",
"Keio.Takao": "#C00",
"Keisei.KeiseiMain": "#005AAA", "Keisei.KeiseiNaritaAirport": "#FF8000",
"Keisei.KeiseiOta": "#FF8000", "Keisei.KeiseiHigashiNarawa": "#FF8000",
"Keisei.KeiseiKanamachi": "#005AAA", "Keisei.Skyliner": "#FF8000",
"Odakyu.Odakyu": "#008833", "Odakyu.OdakyuEnoshima": "#008833",
"Seibu.Ikebukuro": "#E03C31", "Seibu.Shinjuku": "#A14012",
"Seibu.SeibuIkebukuro": "#E03C31", "Seibu.SeibuShinjuku": "#A14012",
"Tobu.TobuSkytree": "#1F60A1", "Tobu.TobuIsesaki": "#1F60A1",
"Tobu.TobuTojo": "#E5171F", "Tobu.TobuNikko": "#1F60A1",
"Tobu.TobuKameido": "#1F60A1", "Tobu.TobuDaishi": "#1F60A1",
"Tobu.TobuUtsunomiya": "#1F60A1", "Tobu.TobuKawagoe": "#1F60A1",
"Keihin.Keihin": "#1F60A1",
# 京急
"Keikyu.Main": "#E5171F", "Keikyu.Airport": "#E5171F",
"Keikyu.Daishi": "#E5171F", "Keikyu.Zushi": "#E5171F",
"Keikyu.Kurihama": "#E5171F",
# 小田急
"Odakyu.Odawara": "#008833", "Odakyu.Enoshima": "#1F60A1",
"Odakyu.Tama": "#008833",
# 京王新线
"Keio.Inokashira": "#DD0077", "Keio.Keibajo": "#C00",
"Keio.KeioDobutsuen": "#C00", "Keio.Takao": "#C00",
# 西武
"Seibu.Haijima": "#E03C31", "Seibu.Kokubunji": "#E03C31",
"Seibu.SeibuYurakucho": "#E03C31", "Seibu.Tamagawa": "#E03C31",
"Seibu.Tamako": "#E03C31", "Seibu.Toshima": "#E03C31",
"Seibu.SeibuIkebukuro": "#E03C31", "Seibu.SeibuShinjuku": "#A14012",
# 京成
"Keisei.Kanamachi": "#005AAA", "Keisei.Oshiage": "#005AAA",
"Keisei.NaritaSkyAccess": "#FF8000", "Keisei.Main": "#005AAA",
# 临海 / 百合海鸥
"TWR.Rinkai": "#00418E", "Yurikamome.Yurikamome": "#0073C2",
# 东武分支
"Tobu.Daishi": "#1F60A1", "Tobu.Kameido": "#1F60A1",
"Tobu.TobuSkytreeBranch": "#1F60A1", "Tobu.TobuIsesaki": "#1F60A1",
"Tobu.TobuNikko": "#1F60A1", "Tobu.TobuTojo": "#E5171F",
"Tobu.TobuUtsunomiya": "#1F60A1", "Tobu.TobuKawagoe": "#1F60A1",
"SaitamaRailway.SaitamaRailway": "#00B5B8",
# 都电 / 新交通
"Toei.Arakawa": "#70C419", "Toei.NipporiToneri": "#E68C3A",
# 北总 / 各类
"Hokuso.Hokuso": "#00418E", "JR-East.SobuRapid": "#FFD400",
"Tobu.Tojo": "#E5171F", "TokyoMetro.MarunouchiBranch":"F62E36",
# 各种新干线
"JR-East.TohokuShinkansen": "#0F64B0", "JR-East.JoetsuShinkansen": "#0F64B0",
"JR-East.HokurikuShinkansen": "#0F64B0", "JR-East.YamagataShinkansen":"#0F64B0",
"JR-East.AkitaShinkansen": "#0F64B0",
"JR-Central.TokaidoShinkansen":"#F18040", "JR-Hokkaido.HokkaidoShinkansen": "#0F64B0",
# 其他 JR 区域
"JR-Hokkaido.Hakodate": "#EB1C24", "JR-Hokkaido.Sassho": "#EB1C24",
"JR-Hokkaido.Chitose": "#F38B14", "JR-Hokkaido.Sekisho": "#F38B14",
"JR-Hokkaido.Nemuro": "#F38B14", "JR-Hokkaido.Muroran": "#F38B14",
"JR-East.Joban": "#808080",
"JR-Central.Tokaido": "#F18E40", "JR-Central.Mikawa": "#F18E40",
}
# 兜底色(JP unknown)
_FALLBACK_COLOR = "#3CFF6A" # ROUTE_GREEN
def _ensure_cache():
"""首次启动下载 stations.json(若缺失)。"""
os.makedirs(_CACHE_DIR, exist_ok=True)
if not os.path.exists(_SRC):
req = urllib.request.Request(_SRC_URL, headers={"User-Agent": "NekoCardReader/1.0"})
with urllib.request.urlopen(req, timeout=60) as r:
data = r.read()
with open(_SRC, "wb") as f:
f.write(data)
def _build_lines_index():
"""从 stations.json 解析成 [{code,name,points,n}] 列表, 写缓存。
用 ekidata 自带的真实线内顺序排站:
- 按 ekidata_line_id 分组(同一条物理线)
- 组内按 int(ekidata_id) 升序 —— ekidata_id = 线号 + 站序, 整数排序即真实走向
(已用山手线验证: 大崎→…→品川, 0 处异常跳跃)
color/name 用 line_code(如 JR-East.Yamanote), 没有就退回 ekidata_line_id。"""
d = json.load(open(_SRC, encoding="utf-8"))
byline = defaultdict(list)
for grp in d:
for s in grp.get("stations", []):
lid = s.get("ekidata_line_id") or ""
lat, lon = s.get("lat"), s.get("lon")
if not lid or lat is None or lon is None:
continue
try:
seq = int(s.get("ekidata_id") or "0")
except (TypeError, ValueError):
seq = 0
byline[lid].append((seq, lat, lon, s.get("line_code") or "", s.get("name_kanji") or ""))
out = []
for lid, rows in byline.items():
rows.sort(key=lambda r: r[0]) # 真实线内顺序
seen = set(); pts = []; names = []; lc = ""
for _seq, lat, lon, code, nm in rows:
if code and not lc:
lc = code
k = (round(lat, 5), round(lon, 5))
if k in seen: # 去重但保持顺序
continue
seen.add(k); pts.append((lat, lon)); names.append(nm)
if len(pts) < 2:
continue
code = lc or lid
out.append({
"code": code,
"name": code.split(".", 1)[-1] if "." in code else code,
"points": pts,
"names": names, # 每点对应站名, 供 route_between 沿网格切段
"n": len(pts),
})
out.sort(key=lambda x: x["code"])
with open(_LINES, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, separators=(",", ":"))
return out
def load_lines(force=False):
"""返回全部线路 [{code,name,points,n}], 内存缓存, 二次启动零延迟。"""
global _lines_cache
if _lines_cache is not None and not force:
return _lines_cache
if not os.path.exists(_LINES) or force:
_ensure_cache()
_lines_cache = _build_lines_index()
else:
try:
_lines_cache = json.load(open(_LINES, encoding="utf-8"))
except (OSError, json.JSONDecodeError):
_ensure_cache()
_lines_cache = _build_lines_index()
return _lines_cache
def color_for(line_code: str) -> str:
return COLORS.get(line_code, _FALLBACK_COLOR)
def _norm_name(s: str) -> str:
return (s or "").strip().replace("ヶ", "ケ").replace("ヵ", "カ").replace(" ", "").replace(" ", "").replace("駅", "").replace("站", "")
_st_index = None
def _station_index():
"""站名(规整) -> 所属线路 code 列表。一次性构建并缓存。"""
global _st_index
if _st_index is not None:
return _st_index
idx = {}
for L in load_lines():
code = L.get("code")
for nm in (L.get("names") or []):
k = _norm_name(nm)
if not k:
continue
lst = idx.setdefault(k, [])
if code not in lst:
lst.append(code)
_st_index = idx
return idx
# 线路代码 → 日文线名(关东为主, 覆盖常用线; 没收录的退回罗马字)
LINE_KANJI = {
# JR 东日本
"JR-East.Yamanote": "山手線", "JR-East.ChuoRapid": "中央線快速", "JR-East.Chuo": "中央線",
"JR-East.ChuoSobuLocal": "中央・総武線", "JR-East.Sobu": "総武線",
"JR-East.KeihinTohoku": "京浜東北線", "JR-East.KeihinTohokuNegishi": "京浜東北・根岸線",
"JR-East.Tokaido": "東海道線", "JR-East.Yokosuka": "横須賀線", "JR-East.Saikyo": "埼京線",
"JR-East.SaikyoKawagoe": "埼京・川越線", "JR-East.Utsunomiya": "宇都宮線", "JR-East.Takasaki": "高崎線",
"JR-East.Joban": "常磐線", "JR-East.JobanLocal": "常磐線各駅停車", "JR-East.JobanRapid": "常磐線快速",
"JR-East.Keiyo": "京葉線", "JR-East.Musashino": "武蔵野線", "JR-East.Narita": "成田線",
"JR-East.Yokohama": "横浜線", "JR-East.Nambu": "南武線", "JR-East.NambuBranch": "南武支線",
"JR-East.Ome": "青梅線", "JR-East.Itsukaichi": "五日市線", "JR-East.Hachiko": "八高線",
"JR-East.Kawagoe": "川越線", "JR-East.Sotobo": "外房線", "JR-East.Uchibo": "内房線",
"JR-East.ShonanShinjuku": "湘南新宿ライン", "JR-East.UenoTokyo": "上野東京ライン",
"JR-East.Tsurumi": "鶴見線",
# 东京地下铁(Tokyo Metro)
"TokyoMetro.Ginza": "銀座線", "TokyoMetro.Marunouchi": "丸ノ内線", "TokyoMetro.Hibiya": "日比谷線",
"TokyoMetro.Tozai": "東西線", "TokyoMetro.Chiyoda": "千代田線", "TokyoMetro.Yurakucho": "有楽町線",
"TokyoMetro.Hanzomon": "半蔵門線", "TokyoMetro.Namboku": "南北線", "TokyoMetro.Fukutoshin": "副都心線",
# 都营(Toei)
"Toei.Asakusa": "都営浅草線", "Toei.Mita": "都営三田線", "Toei.Shinjuku": "都営新宿線",
"Toei.Oedo": "都営大江戸線",
# 私铁
"Keio.Keio": "京王線", "Keio.Inokashira": "井の頭線", "Keio.Sagamihara": "京王相模原線",
"Keio.Takao": "京王高尾線", "Keio.Keibajo": "京王競馬場線", "Keio.Dobutsuen": "京王動物園線",
"Odakyu.Odawara": "小田急小田原線", "Odakyu.Enoshima": "小田急江ノ島線", "Odakyu.Tama": "小田急多摩線",
"Tokyu.Toyoko": "東急東横線", "Tokyu.Denentoshi": "東急田園都市線", "Tokyu.Oimachi": "東急大井町線",
"Tokyu.Meguro": "東急目黒線", "Tokyu.Ikegami": "東急池上線", "Tokyu.Tamagawa": "東急多摩川線",
"Tokyu.Setagaya": "東急世田谷線", "Tokyu.Kodomonokuni": "こどもの国線",
"Keikyu.Main": "京急本線", "Keikyu.Airport": "京急空港線", "Keikyu.Daishi": "京急大師線",
"Keikyu.Zushi": "京急逗子線", "Keikyu.Kurihama": "京急久里浜線",
"Seibu.Ikebukuro": "西武池袋線", "Seibu.Shinjuku": "西武新宿線", "Seibu.Toshima": "西武豊島線",
"Seibu.Tamako": "西武多摩湖線", "Seibu.Kokubunji": "西武国分寺線",
"Tobu.Tojo": "東武東上線", "Tobu.Isesaki": "東武伊勢崎線", "Tobu.Skytree": "東武スカイツリーライン",
"Tobu.Nikko": "東武日光線", "Tobu.Noda": "東武野田線",
"Keisei.Main": "京成本線", "Keisei.Oshiage": "京成押上線", "Keisei.Chiba": "京成千葉線",
"TsukubaExpress.TX": "つくばエクスプレス", "Yurikamome.Yurikamome": "ゆりかもめ",
"TWR.Rinkai": "りんかい線", "TokyoMonorail.Haneda": "東京モノレール",
"Sotetsu.Main": "相鉄本線", "Sotetsu.Izumino": "相鉄いずみ野線",
# 数字 ekidata_line_id(这些线在数据里 line_code 为空, 只能按 ID 映射) —— 关东
"11311": "中央線快速", "11328": "東海道線", "11343": "上野東京ライン", "11505": "御殿場線",
"22002": "西武秩父線", "23002": "京成金町線", "23006": "京成本線", "24001": "京王線",
"26001": "東急東横線", "26002": "東急目黒線", "26003": "東急田園都市線", "26004": "東急大井町線",
"26005": "東急池上線", "26006": "東急多摩川線", "26007": "東急世田谷線", "26008": "こどもの国線",
"29001": "相鉄本線", "29002": "相鉄いずみ野線", "99306": "秩父鉄道", "99307": "埼玉高速鉄道",
"99308": "いすみ鉄道", "99309": "つくばエクスプレス", "99310": "みなとみらい線", "99313": "ユーカリが丘線",
"99314": "伊豆箱根大雄山線", "99316": "横浜市営地下鉄ブルーライン", "99317": "金沢シーサイドライン",
"99320": "江ノ島電鉄", "99321": "ニューシャトル", "99324": "京成東成田線", "99325": "小湊鉄道",
"99326": "湘南モノレール", "99329": "新京成線", "99331": "千葉モノレール", "99332": "千葉モノレール",
"99333": "流鉄流山線", "99334": "多摩都市モノレール", "99335": "銚子電鉄", "99336": "東京モノレール",
"99338": "東葉高速線", "99339": "箱根登山鉄道", "99340": "北総線",
"99343": "横浜市営地下鉄グリーンライン", "99344": "箱根登山ケーブルカー",
}
def line_display(code: str) -> str:
"""线路代码 → 日文线名(查 LINE_KANJI); 罗马字代码退回其后缀;
纯数字 ekidata_id 且未收录 → 返回 ""(别让 "26004" 这种丑数字漏出去, 上层退回公司名)。"""
if code in LINE_KANJI:
return LINE_KANJI[code]
if "." in code:
return code.split(".", 1)[-1]
return "" if str(code).isdigit() else code
def lines_for_trip(name_a, name_b):
"""从站名反推线路代码。同一条线 → [一个]; 跨线/换乘(两端不同线)→ [两端各自线]; 找不到 → []。"""
try:
idx = _station_index()
except Exception:
return []
la = idx.get(_norm_name(name_a), [])
lb = idx.get(_norm_name(name_b), [])
common = [c for c in la if c in lb]
if common:
return [common[0]]
out = []
if la:
out.append(la[0])
if lb and (not out or lb[0] != out[0]):
out.append(lb[0])
return out
def colors_for_trip(name_a, name_b):
"""线路代表色, 和 lines_for_trip 一一对应。同线 → [单色]; 跨线 → [两色]; 找不到 → []。"""
return [color_for(c) for c in lines_for_trip(name_a, name_b)]
def route_between(name_a, name_b):
"""在网格线里找同时经过 a、b 两站的线, 截取它们之间那一段(沿真实网格走向),
返回 [{name,lat,lon}, ...](含端点)。多条命中取最短一段。找不到返回 None。
高亮行程线用它 → 必然贴着网格底图。"""
na, nb = _norm_name(name_a), _norm_name(name_b)
if not na or not nb or na == nb:
return None
best = None
for L in load_lines():
names = L.get("names")
if not names:
continue
ia = ib = -1
for i, nm in enumerate(names):
n = _norm_name(nm)
if n == na and ia < 0:
ia = i
if n == nb:
ib = i
if ia < 0 or ib < 0:
continue
pts = L["points"]
idxs = range(ia, ib + 1) if ia <= ib else range(ia, ib - 1, -1)
seg = [{"name": names[i], "lat": pts[i][0], "lon": pts[i][1]} for i in idxs]
if len(seg) >= 2 and (best is None or len(seg) < len(best)):
best = seg
return best
def nearby_lines(trips, padding_deg=0.5):
"""按行程经纬度矩形(自动扩 padding)取该区域内涉及的线路。
返回 [{name, points:[[lat,lon],...], color, code}], 给 web_map 当 network 用。
trips: [{ci/co/path...}] — 任一端点 / 路径点 都参与计算边界
padding_deg: 边界向外扩多少度(默认 0.15° ≈ 16km, 城市内刚好覆盖)
"""
all_lats, all_lons = [], []
for t in trips:
path = t.get("path") or []
if path:
for p in path:
if p.get("lat") is not None: all_lats.append(p["lat"])
if p.get("lon") is not None: all_lons.append(p["lon"])
else:
for k in ("ci", "co"):
ll = t.get(k)
if ll and ll[0] is not None: all_lats.append(ll[0])
if ll and ll[1] is not None: all_lons.append(ll[1])
if not all_lats or not all_lons:
return []
min_lat = min(all_lats) - padding_deg
max_lat = max(all_lats) + padding_deg
min_lon = min(all_lons) - padding_deg
max_lon = max(all_lons) + padding_deg
out = []
for L in load_lines():
pts = L["points"]
# 任一站落在范围内即纳入
hit = False
for lat, lon in pts:
if min_lat <= lat <= max_lat and min_lon <= lon <= max_lon:
hit = True; break
if not hit:
continue
out.append({
"name": L["name"],
"code": L["code"],
"color": color_for(L["code"]),
"points": [[p[0], p[1]] for p in pts],
})
return out
def _clip_line_segment(a, b, bounds):
"""Clip one (lat, lon) segment to bounds with Liang-Barsky."""
min_lat, min_lon, max_lat, max_lon = bounds
y0, x0 = a; y1, x1 = b
dx, dy = x1 - x0, y1 - y0
low, high = 0.0, 1.0
for p, q in ((-dx, x0 - min_lon), (dx, max_lon - x0),
(-dy, y0 - min_lat), (dy, max_lat - y0)):
if abs(p) < 1e-12:
if q < 0:
return None
continue
ratio = q / p
if p < 0:
low = max(low, ratio)
else:
high = min(high, ratio)
if low > high:
return None
return ((y0 + low * dy, x0 + low * dx),
(y0 + high * dy, x0 + high * dx))
def clipped_network(trips, padding_deg=None, view_aspect=2.35):
"""Return known-color local rail lines clipped to the current trip view.
This is a bounded supplement for gaps around the OSM query/cache extent.
"""
lats, lons = [], []
for trip in trips:
for key in ("ci", "co"):
point = trip.get(key)
if point and point[0] is not None and point[1] is not None:
lats.append(float(point[0])); lons.append(float(point[1]))
for point in (trip.get("path") or []):
if point.get("lat") is not None and point.get("lon") is not None:
lats.append(float(point["lat"])); lons.append(float(point["lon"]))
if not lats or not lons:
return []
raw_lat_span = max(lats) - min(lats)
raw_lon_span = max(lons) - min(lons)
lat_span = max(raw_lat_span, 0.02)
lon_span = max(raw_lon_span, 0.03)
if padding_deg is None:
# fitBounds expands the short axis to the map viewport. Mirror that
# expansion here so a wide window does not expose an empty outer strip.
cos_lat = max(0.2, math.cos(math.radians(sum(lats) / len(lats))))
aspect = max(view_aspect, 0.5)
target_lat = max(lat_span, lon_span * cos_lat / aspect)
target_lon = max(lon_span, lat_span * aspect / cos_lat)
bounds_lat_span = max(raw_lat_span + 0.08, target_lat * 1.16)
bounds_lon_span = max(raw_lon_span + 0.14, target_lon * 1.16)
projected_ratio = aspect / cos_lat
bounds_lon_span = max(bounds_lon_span, bounds_lat_span * projected_ratio)
bounds_lat_span = max(bounds_lat_span, bounds_lon_span / projected_ratio)
pad_lat = (bounds_lat_span - raw_lat_span) * 0.5
pad_lon = (bounds_lon_span - raw_lon_span) * 0.5
else:
pad_lat = pad_lon = float(padding_deg)
bounds = (min(lats) - pad_lat, min(lons) - pad_lon,
max(lats) + pad_lat, max(lons) + pad_lon)
out = []
for line in load_lines():
code = str(line.get("code", ""))
# Unknown codes have no verified representative color; never paint them green.
if code not in COLORS:
continue
points = line.get("points") or []
segments, current = [], []
for index in range(1, len(points)):
clipped = _clip_line_segment(points[index - 1], points[index], bounds)
if not clipped:
if len(current) >= 2:
segments.append(current)
current = []
continue
start, end = clipped
if current and abs(current[-1][0] - start[0]) < 1e-9 \
and abs(current[-1][1] - start[1]) < 1e-9:
current.append([end[0], end[1]])
else:
if len(current) >= 2:
segments.append(current)
current = [[start[0], start[1]], [end[0], end[1]]]
if len(current) >= 2:
segments.append(current)
if segments:
out.append({"name": line.get("name", ""), "code": code,
"color": COLORS[code], "segments": segments,
"bounds": bounds})
return out
if __name__ == "__main__":
ls = load_lines()
print(f"已加载 {len(ls)} 条日本线路")
print("前 5 条:", [L["code"] for L in ls[:5]])
# 测附近搜索: 东京站附近
sample = [{"ci": (35.681, 139.767), "co": (35.466, 139.622)}]
near = nearby_lines(sample, padding_deg=0.3)
print(f"东京附近线路(0.3° 范围): {len(near)} 条")
for L in near[:5]:
print(f" {L['code']:30} color={L['color']}")