-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheki-finder-by-dom.user.js
More file actions
268 lines (207 loc) · 7.83 KB
/
eki-finder-by-dom.user.js
File metadata and controls
268 lines (207 loc) · 7.83 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
// ==UserScript==
// @name Anitabi Eki/Bus Finder (By DOM)
// @namespace https://anitabi.cn/
// @version 3.2
// @description 在 Anitabi 地标卡片插入两个按钮:① 8 km 内最近的 3 个电车站 ② 8 km 内最近的 3 个公交站。
// @match https://anitabi.cn/map*
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
(function () {
console.log('[Eki/Bus Finder] userscript loaded');
'use strict';
/* ===================== Haversine ===================== */
const haversine = (lat1, lon1, lat2, lon2) => {
const R = 6371000;
const rad = d => d * Math.PI / 180;
const dφ = rad(lat2 - lat1);
const dλ = rad(lon2 - lon1);
const a = Math.sin(dφ / 2) ** 2 +
Math.cos(rad(lat1)) * Math.cos(rad(lat2)) * Math.sin(dλ / 2) ** 2;
return 2 * R * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};
/* ===================== 坐标解析 ===================== */
/* ——— 通过弹窗 DOM 提取 ll= / cbll= ——— */
const coordFromDom = root => {
const a = root.querySelector(
'a[href*="maps"][href*="ll="], a[href*="maps"][href*="cbll="]'
);
if (!a) {
console.warn('[Eki/Bus Finder] 没有找到 Google Maps 链接 (ll/cbll)');
return null;
}
const m = /[?&](?:ll|cbll)=([-\.\d]+),([-\.\d]+)/.exec(a.href);
if (!m) {
console.warn('[Eki/Bus Finder] Google 链接存在,但未匹配 ll/cbll:', a.href);
return null;
}
console.log('[Eki/Bus Finder] 通过 DOM 解析坐标成功:', m[1], m[2]);
return { lat: +m[1], lon: +m[2] }; // 参数顺序为 lat,lon
};
/* ——— 若 DOM 失败则调用官方 API ——— */
const coordFromApi = async () => {
const p = new URLSearchParams(location.search);
const bangumiId = p.get('bangumiId');
const pid = p.get('pid');
if (!bangumiId || !pid) {
console.warn('[Eki/Bus Finder] URL 缺少 bangumiId 或 pid;无法调用 API');
return null;
}
const apiUrl = `https://api.anitabi.cn/bangumi/${bangumiId}/points/detail?haveImage=false`;
console.log('[Eki/Bus Finder] 调用 API 获取坐标:', apiUrl);
try {
const arr = await (await fetch(apiUrl)).json();
const pt = arr.find(x => x.id === pid);
if (pt && Array.isArray(pt.geo)) {
const [lat, lon] = pt.geo;
console.log('[Eki/Bus Finder] API 返回坐标:', lat, lon);
return { lat, lon };
}
console.warn('[Eki/Bus Finder] API 中未找到匹配 pid 的地标');
} catch (err) {
console.error('[Eki/Bus Finder] API 调用异常', err);
}
return null;
};
/* ——— 统一坐标解析入口 ——— */
const resolveCoord = async root => coordFromDom(root) || await coordFromApi();
/* ===================== Overpass 查询 ===================== */
function queryTopN(lat, lon, filterQL, N, cb) {
const ql = `
[out:json][timeout:25];
(
${filterQL}(around:8000,${lat},${lon});
);
out body;`;
console.log('[Eki/Bus Finder] Overpass 查询体:\n', ql);
GM_xmlhttpRequest({
method : 'POST',
url : 'https://overpass-api.de/api/interpreter',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data : 'data=' + encodeURIComponent(ql),
onload: r => {
if (r.status !== 200) {
console.error('[Eki/Bus Finder] Overpass 返回状态非 200:', r.status, r.statusText);
cb(false);
return;
}
try {
const js = JSON.parse(r.responseText);
if (!js.elements.length) {
console.info('[Eki/Bus Finder] 8 km 内无匹配节点');
cb(null);
return;
}
const list = js.elements
.map(n => ({
name : n.tags?.name || '(未知站点)',
dist : haversine(lat, lon, n.lat, n.lon)
}))
.sort((a, b) => a.dist - b.dist)
.slice(0, N);
console.log('[Eki/Bus Finder] Overpass 结果:', list);
cb(list);
} catch (e) {
console.error('[Eki/Bus Finder] JSON 解析失败', e);
cb(false);
}
},
onerror: e => {
console.error('[Eki/Bus Finder] Overpass 请求失败', e);
cb(false);
}
});
}
/* ===================== UI Helpers ===================== */
const fmt = arr => arr
.map((o, i) => `${i + 1}. ${o.name} ${(o.dist / 1000).toFixed(2)} km`)
.join(' | ');
const mkBtn = (label, handler) => {
const b = document.createElement('button');
b.textContent = label;
b.className = 'eki-btn';
b.style.cssText = [
'display:inline-block',
'margin:6px 6px 0 0',
'padding:4px 8px',
'font-size:12px',
'color:#fff',
'background:#ff7b00',
'border:none',
'border-radius:4px',
'cursor:pointer'
].join(';');
b.addEventListener('click', handler);
return b;
};
/* ——— 在每个弹窗插入按钮面板 ——— */
function attach(card) {
if (card.querySelector('.eki-btn')) return;
const out = document.createElement('div');
out.style.cssText = 'font-size:12px;margin:4px 0;white-space:pre-wrap;';
const railBtn = mkBtn('8 km 内电车站', async () => {
railBtn.disabled = busBtn.disabled = true;
out.textContent = '⌛ 获取坐标…';
const pos = await resolveCoord(card);
if (!pos) {
out.textContent = '❓ 坐标缺失(详见控制台)';
railBtn.disabled = busBtn.disabled = false;
return;
}
out.textContent = '⌛ 查询中…';
queryTopN(
pos.lat,
pos.lon,
'node["railway"~"^(station|halt|tram_stop)$"]',
3,
res => {
railBtn.disabled = busBtn.disabled = false;
if (res === false) { out.textContent = '🚫 查询失败(详见控制台)'; return; }
if (res === null) { out.textContent = 'ℹ 8 km 内无电车站'; return; }
out.textContent = '🚉 ' + fmt(res);
}
);
});
const busBtn = mkBtn('8 km 内公交站', async () => {
railBtn.disabled = busBtn.disabled = true;
out.textContent = '⌛ 获取坐标…';
const pos = await resolveCoord(card);
if (!pos) {
out.textContent = '❓ 坐标缺失(详见控制台)';
railBtn.disabled = busBtn.disabled = false;
return;
}
out.textContent = '⌛ 查询中…';
queryTopN(
pos.lat,
pos.lon,
'node["highway"="bus_stop"]',
3,
res => {
railBtn.disabled = busBtn.disabled = false;
if (res === false) { out.textContent = '🚫 查询失败(详见控制台)'; return; }
if (res === null) { out.textContent = 'ℹ 8 km 内无公交站'; return; }
out.textContent = '🚌 ' + fmt(res);
}
);
});
card.append(railBtn, busBtn, out);
}
/* ===================== MutationObserver ===================== */
const SELS = [
'.poi-card',
'.leaflet-popup-content',
'.mapboxgl-popup-content'
];
const obs = new MutationObserver(mutations => {
mutations.forEach(m => m.addedNodes.forEach(node => {
if (!(node instanceof HTMLElement)) return;
SELS.forEach(s => node.matches(s) && attach(node));
SELS.forEach(s => node.querySelectorAll(s).forEach(attach));
}));
});
obs.observe(document.body, { childList: true, subtree: true });
window.addEventListener('load', () => {
SELS.forEach(sel => document.querySelectorAll(sel).forEach(attach));
});
})();