返回文章列表
2025年12月10日yodfz
#飞牛相册Hack搜索框
javascript飞牛油猴
115 分钟阅读
34371 字
yodfz我的NAS一直都是使用飞牛系统,但是有个问题,他的相册AI搜索不好用,并且在地图模式下没有任何可以搜索的内容。但是我又需要在地图模式下搜索地址,不然整体拖拽非常的麻烦。
最终痛定思痛,自己写了一个脚本HACK了飞牛相册,在飞牛相册加载之后,自动初始化一个搜索框进去。采用公开的数据地址,进行定位搜索。
// ==UserScript==
// @name 飞牛相册地图增强
// @match [飞牛相册地址]/map*
// @run-at document-start
// @grant none
// ==/UserScript==
(function () {
if (window.__TM_FN_ENHANCED) {
console.log("[TM] script already loaded, skip");
return;
}
window.__TM_FN_ENHANCED = true;
const tmLog = (...a) => console.log("[TM]", ...a);
// 注入转圈动画样式(若未存在)
(() => {
if (document.getElementById("tm-spin-style")) return;
const style = document.createElement("style");
style.id = "tm-spin-style";
style.textContent = `
@keyframes tm-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg);} }
`;
document.head.appendChild(style);
})();
// 1) 捕获/兜底腾讯地图 key
const prevOnLoad = window.onLoad;
tmLog("install onLoad interceptor");
window.TX_MAP_KEY = "OZNBZ-DDAKG-7WDQB-Q2WJH-NUBP5-NPFSX"; // 兜底
window.onLoad = function (cfg, ...rest) {
tmLog("onLoad called", cfg);
if (cfg && cfg.key) {
window.TX_MAP_KEY = cfg.key;
tmLog("captured TX key:", cfg.key);
}
if (typeof prevOnLoad === "function") {
return prevOnLoad.apply(this, [cfg, ...rest]);
}
};
const tryParseKeyFromGljs = async () => {
try {
const script =
document.querySelector('script[src*="gljs"]') ||
document.querySelector('script[src*="map.qq.com"]');
if (!script || !script.src) return;
const res = await fetch(script.src);
const txt = await res.text();
const m =
txt.match(/"key"\s*:\s*"([A-Z0-9-]+)"/i) ||
txt.match(/key["']?\s*[:=]\s*["']([A-Z0-9-]{10,})["']/i);
if (m && m[1]) {
window.TX_MAP_KEY = m[1];
tmLog("captured TX key via fetch:", m[1]);
}
} catch (e) {
tmLog("parse key from gljs failed:", e);
}
};
setTimeout(() => {
if (!window.TX_MAP_KEY) tryParseKeyFromGljs();
}, 1000);
const wait = (fn, key = "TMap", timeout = 20000) =>
new Promise((res, rej) => {
const t0 = Date.now();
(function poll() {
if (window[key]) return res(window[key]);
if (Date.now() - t0 > timeout) return rej(new Error("wait timeout"));
requestAnimationFrame(poll);
})();
});
// 2) 全局引用
let firstMap = null;
let TMapRef = null;
let firstCluster = null;
let scanAttempts = 0;
let geocoderPromise = null;
let lastSuggestToken = 0;
let lastSuggestQ = "";
let suggestLoadingEl = null;
// 点高亮层移除;仅保留行政区热力
let highlightLayer = null;
const highlightGeos = new Map();
const highlightQueue = [];
let highlightScheduled = false;
const ENABLE_HIGHLIGHT = false; // 关闭点高亮
// 行政区域热力(简化用 bounding box)
let adminLayer = null;
const adminPolys = new Map(); // key=adminName, val={id,path,weight}
const adminQueue = [];
let adminScheduled = false;
const ADMIN_MAX = 120; // 控制最多绘制的区域数
let adminLastDrawLog = 0;
const adminCellCache = new Map(); // key=cell lat,lng -> {name, paths}
const adminRegionCache = new Map(); // key=region name -> {paths, bbox:[s,n,w,e]}
const adminCacheTrim = (map, max = 400) => {
if (map.size <= max) return;
const keys = Array.from(map.keys());
for (let i = 0; i < map.size - max; i++) map.delete(keys[i]);
};
const latLngInBbox = (lat, lng, bbox) => {
if (!bbox || bbox.length < 4) return false;
const [s, n, w, e] = bbox;
return (
Number.isFinite(s) &&
Number.isFinite(n) &&
Number.isFinite(w) &&
Number.isFinite(e) &&
lat >= s - 0.01 &&
lat <= n + 0.01 &&
lng >= w - 0.01 &&
lng <= e + 0.01
);
};
const adminVisibleFirst = (lat, lng) => {
if (!firstMap || typeof firstMap.getBounds !== "function") return false;
try {
const b = firstMap.getBounds();
if (!b || !b.southwest || !b.northeast) return false;
const sw = b.southwest;
const ne = b.northeast;
return (
lat >= sw.lat - 0.1 &&
lat <= ne.lat + 0.1 &&
lng >= sw.lng - 0.1 &&
lng <= ne.lng + 0.1
);
} catch (_) {
return false;
}
};
const enqueueHighlight = () => {
if (!ENABLE_HIGHLIGHT) return;
if (highlightScheduled) return;
highlightScheduled = true;
requestAnimationFrame(() => {
highlightScheduled = false;
if (!highlightLayer) return;
// 控制数量,避免过多导致卡顿/黑屏
const MAX = 1500;
if (highlightGeos.size > MAX) {
const extra = highlightGeos.size - MAX;
const keys = Array.from(highlightGeos.keys());
for (let i = 0; i < extra; i++) highlightGeos.delete(keys[i]);
}
highlightLayer.setGeometries([...highlightGeos.values()]);
});
};
const enqueueAdminDraw = () => {
if (adminScheduled) return;
adminScheduled = true;
requestAnimationFrame(() => {
adminScheduled = false;
if (!adminLayer) return;
// 控制数量
if (adminPolys.size > ADMIN_MAX) {
const keys = Array.from(adminPolys.keys());
const extra = adminPolys.size - ADMIN_MAX;
for (let i = 0; i < extra; i++) adminPolys.delete(keys[i]);
}
const geos = [...adminPolys.values()];
adminLayer.setGeometries(geos);
if (Date.now() - adminLastDrawLog > 2000) {
adminLastDrawLog = Date.now();
tmLog("admin heat draw", {
regions: geos.length,
sample: geos
.slice(0, 3)
.map((g) => `${g.id}:${g.weight}`)
.join(","),
});
}
});
};
// 3) 捕获 map
const captureMap = (m) => {
if (m) m.__isTMapMap = true;
if (!firstMap && m) {
firstMap = m;
tmLog("captured map instance");
}
return m;
};
// 4) 提前安装 window.TMap setter,防止早期创建溜走
try {
const desc = Object.getOwnPropertyDescriptor(window, "TMap");
if (!desc || desc.configurable) {
let _tmap;
Object.defineProperty(window, "TMap", {
configurable: true,
get() {
return _tmap;
},
set(v) {
_tmap = v;
tmLog("window.TMap assigned (setter)");
if (v) {
// wrap Map
if (v.Map && !v.Map.__TM_WRAPPED) {
const RawMapEarly = v.Map;
const WrappedMap = function (...args) {
const m = new RawMapEarly(...args);
captureMap(m);
return m;
};
WrappedMap.prototype = RawMapEarly.prototype;
Object.setPrototypeOf(WrappedMap, RawMapEarly);
WrappedMap.__TM_WRAPPED = true;
v.Map = WrappedMap;
tmLog("wrapped TMap.Map (setter)");
}
// wrap MarkerCluster
if (v.MarkerCluster && !v.MarkerCluster.__TM_WRAPPED) {
const RawMCEarly = v.MarkerCluster;
const mcAddEarly =
RawMCEarly.prototype && RawMCEarly.prototype.add;
const WrappedMC = function (opts = {}) {
// 仅在 map 缺失时回退到 firstMap,避免传入非 Map 实例导致 SDK 报错
if (!opts.map && firstMap) opts.map = firstMap;
const inst = new RawMCEarly(opts);
if (opts?.map) captureMap(opts.map);
if (!firstCluster) {
firstCluster = inst;
tmLog("captured MarkerCluster (setter)");
}
return inst;
};
WrappedMC.prototype = RawMCEarly.prototype;
Object.setPrototypeOf(WrappedMC, RawMCEarly);
WrappedMC.__TM_WRAPPED = true;
v.MarkerCluster = WrappedMC;
if (mcAddEarly) {
RawMCEarly.prototype.add = function (geos = []) {
const patched = geos.map((g) => {
const p = g.properties || {};
const hasPhoto =
p.count > 0 ||
p.url ||
p.photos?.length > 0 ||
p.images?.length > 0 ||
p.imgUrl;
return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
});
return mcAddEarly.call(this, patched);
};
}
tmLog("wrapped MarkerCluster (setter)");
}
}
},
});
} else {
tmLog("window.TMap not configurable, setter skipped");
}
} catch (e) {
tmLog("install TMap setter failed", e);
}
// 5) 样式与搜索
const photoIcon =
"https://mapapi.qq.com/web/lbs/javascriptGL/demo/img/markerDefault.png";
const highlightStyles = () => ({
photoMarker: new TMapRef.MarkerStyle({
width: 22,
height: 30,
src: photoIcon,
anchor: { x: 11, y: 28 },
color: "#ffd966",
strokeColor: "#d48806",
strokeWidth: 1,
}),
});
const photoMeta = new Map();
const rawFetch = window.fetch;
window.fetch = async (...args) => {
const resp = await rawFetch(...args);
try {
const url = (args[0] || "").toString();
if (/photo|album|image/i.test(url)) {
resp
.clone()
.json()
.then((data) => {
const list = data?.list || data?.data || [];
list.forEach((item) => {
if (!item.lat || !item.lng) return;
const key = `${item.lat},${item.lng}`;
const count =
item.photoCount || item.count || item.photos?.length || 0;
photoMeta.set(key, { photoCount: count });
});
});
}
} catch (_) {}
return resp;
};
const addSearchBox = (map) => {
if (!map || !TMapRef) {
tmLog("addSearchBox skipped: map/TMap missing");
return;
}
if (document.getElementById("tm-search-box")) {
tmLog("search box already exists");
return;
}
const box = document.createElement("div");
box.id = "tm-search-box";
box.style.cssText = `
position:absolute;top:12px;right:100px;z-index:9999;
background:#fff;padding:8px;border-radius:6px;
box-shadow:0 2px 10px rgba(0,0,0,0.2);
display:flex;gap:6px;align-items:center;font-size:13px;`;
box.innerHTML = `
<div style="position:relative;display:flex;flex-direction:column;gap:4px;">
<div style="display:flex;gap:6px;align-items:center;">
<input id="tm-addr" placeholder="输入地址回车" style="color:#333;width:400px;padding:6px 8px;border:1px solid #ccc;border-radius:4px;">
<button id="tm-go" style="padding:6px 10px;border:0;background:#1677ff;color:#fff;border-radius:4px;cursor:pointer;">定位</button>
<div id="tm-loading" style="display:none;margin-left:4px;width:16px;height:16px;border:2px solid #1677ff;border-top-color:transparent;border-radius:50%;animation:tm-spin 0.6s linear infinite;"></div>
</div>
<div id="tm-suggest" style="color:#000;display:none;position:absolute;top:38px;left:0;width:280px;max-height:200px;overflow:auto;background:#fff;border:1px solid #ddd;border-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,0.15);z-index:10000;"></div>
</div>
`;
(document.body || document.documentElement).appendChild(box);
tmLog("search box injected; key =", window.TX_MAP_KEY);
const GeocoderCls = TMapRef && TMapRef.service && TMapRef.service.Geocoder;
let geocoder = null;
if (GeocoderCls) {
geocoder = new GeocoderCls({
key: window.TX_MAP_KEY || "",
});
} else {
tmLog("Geocoder class missing; will load qq.maps Geocoder");
if (!geocoderPromise) {
geocoderPromise = new Promise((resolve, reject) => {
const cb = `TM_QQ_GEO_CB_${Date.now()}`;
window[cb] = () => {
try {
if (window.qq && window.qq.maps && window.qq.maps.Geocoder) {
const geo = new window.qq.maps.Geocoder({
complete: (res) => resolve(res),
});
resolve(geo);
} else {
reject(new Error("qq.maps.Geocoder unavailable"));
}
} catch (e) {
reject(e);
}
};
const s = document.createElement("script");
s.src = `https://map.qq.com/api/js?v=2.exp&libraries=convertor,geocoder&key=${
window.TX_MAP_KEY || ""
}&callback=${cb}`;
s.onerror = (e) => {
delete window[cb];
reject(e);
};
document.head.appendChild(s);
});
}
}
const pinStyle = new TMapRef.MultiMarker({
map,
styles: {
temp: new TMapRef.MarkerStyle({
width: 26,
height: 36,
src: photoIcon,
anchor: { x: 13, y: 34 },
}),
},
geometries: [],
});
const suggestBox = box.querySelector("#tm-suggest");
suggestLoadingEl = box.querySelector("#tm-loading");
suggestLoadingEl = box.querySelector("#tm-loading");
const renderSuggest = (items) => {
if (!items || !items.length) {
suggestBox.style.display = "none";
suggestBox.innerHTML = "";
return;
}
suggestBox.innerHTML = items
.map(
(it, idx) =>
`<div data-idx="${idx}" style="padding:6px 10px;cursor:pointer;border-bottom:1px solid #f0f0f0;">${
it.display_name || it.title || it.name
}</div>`
)
.join("");
suggestBox.style.display = "block";
suggestBox.querySelectorAll("[data-idx]").forEach((el) => {
el.onclick = () => {
const i = Number(el.getAttribute("data-idx"));
const it = items[i];
if (it) {
input.value = it.display_name || it.title || it.name || "";
suggestBox.style.display = "none";
suggestBox.innerHTML = "";
const loc =
it.location ||
(it.lat && it.lng
? { lat: it.lat, lng: it.lng }
: it.lat && it.lon
? { lat: parseFloat(it.lat), lng: parseFloat(it.lon) }
: null);
if (loc) {
map.setCenter(new TMapRef.LatLng(loc.lat, loc.lng));
map.zoomTo(16);
pinStyle.setGeometries([
{
id: "search",
styleId: "temp",
position: new TMapRef.LatLng(loc.lat, loc.lng),
},
]);
}
}
};
});
};
const input = box.querySelector("#tm-addr");
let suggestTimer = null;
const fetchSuggest = (q) => {
// 使用 OSM Nominatim 作为模糊搜索
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
q
)}&format=json&limit=6&addressdetails=0`;
return fetch(url, {
headers: { "User-Agent": "userscript-geocoder" },
}).then((r) => r.json());
};
const locate = () => {
const addr = input.value.trim();
if (!addr) return;
lastSuggestQ = addr;
const doGeocode = geocoder
? () =>
geocoder
.getLocation({ address: addr })
.then((res) => res.result?.location)
: () =>
geocoderPromise
.then((geo) => {
return new Promise((resolve, reject) => {
geo.setComplete((res) => {
const loc = res.detail && res.detail.location;
resolve(loc);
});
geo.setError((e) => reject(e));
geo.getLocation(addr);
});
})
.catch(() => {
// QQ geocoder无权限则回退到 OSM Nominatim
return fetch(
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
addr
)}&format=json&limit=1`,
{
headers: { "User-Agent": "userscript-geocoder" },
}
)
.then((r) => r.json())
.then((j) =>
j && j[0]
? { lat: parseFloat(j[0].lat), lng: parseFloat(j[0].lon) }
: null
);
});
doGeocode()
.then((loc) => {
if (!loc) return alert("未找到地址");
map.setCenter(new TMapRef.LatLng(loc.lat, loc.lng));
map.zoomTo(16);
pinStyle.setGeometries([
{
id: "search",
styleId: "temp",
position: new TMapRef.LatLng(loc.lat, loc.lng),
},
]);
})
.catch((e) => {
tmLog("geocode failed", e);
alert("搜索失败");
});
};
box.querySelector("#tm-go").onclick = locate;
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
suggestBox.style.display = "none";
suggestBox.innerHTML = "";
locate();
}
});
input.addEventListener("input", () => {
const val = input.value.trim();
if (!val) {
renderSuggest([]);
return;
}
if (suggestTimer) clearTimeout(suggestTimer);
suggestTimer = setTimeout(() => {
const token = ++lastSuggestToken;
lastSuggestQ = val;
if (suggestLoadingEl) suggestLoadingEl.style.display = "inline-block";
fetchSuggest(val)
.then((list) => {
if (token !== lastSuggestToken || val !== lastSuggestQ) return;
renderSuggest(list || []);
})
.catch(() => {
if (token !== lastSuggestToken) return;
renderSuggest([]);
})
.finally(() => {
if (token === lastSuggestToken && suggestLoadingEl)
suggestLoadingEl.style.display = "none";
});
}, 300);
});
input.addEventListener("focus", () => {
if (suggestBox.innerHTML.trim()) suggestBox.style.display = "block";
});
document.addEventListener("click", (e) => {
if (!box.contains(e.target)) {
renderSuggest([]);
}
});
};
// 6) 主流程
wait()
.then((TMap) => {
tmLog("TMap ready; TX_MAP_KEY =", window.TX_MAP_KEY);
TMapRef = TMap;
// wrap Map
if (TMap.Map && !TMap.Map.__TM_WRAPPED) {
const RawMap = TMap.Map;
const WrappedMap = function (...args) {
const map = new RawMap(...args);
captureMap(map);
return map;
};
WrappedMap.prototype = RawMap.prototype;
Object.setPrototypeOf(WrappedMap, RawMap);
WrappedMap.__TM_WRAPPED = true;
TMap.Map = WrappedMap;
tmLog("wrapped TMap.Map (wait)");
}
// wrap MarkerCluster
if (TMap.MarkerCluster && !TMap.MarkerCluster.__TM_WRAPPED) {
const RawMC = TMap.MarkerCluster;
const mcAdd = RawMC && RawMC.prototype && RawMC.prototype.add;
const WrappedMC = function (opts = {}) {
// 仅在 map 缺失时回退到 firstMap
if (!opts.map && firstMap) opts.map = firstMap;
const inst = new RawMC(opts);
if (opts?.map) captureMap(opts.map);
if (!firstCluster) {
firstCluster = inst;
tmLog("captured MarkerCluster (wait)");
}
return inst;
};
WrappedMC.prototype = RawMC.prototype;
Object.setPrototypeOf(WrappedMC, RawMC);
WrappedMC.__TM_WRAPPED = true;
TMap.MarkerCluster = WrappedMC;
if (mcAdd) {
RawMC.prototype.add = function (geos = []) {
const patched = geos.map((g) => {
const p = g.properties || {};
const hasPhoto =
p.count > 0 ||
p.url ||
p.photos?.length > 0 ||
p.images?.length > 0 ||
p.imgUrl ||
photoMeta.get(`${g.position?.lat},${g.position?.lng}`)
?.photoCount > 0;
// 仅入队行政区反查,不再绘制点高亮
if (hasPhoto && g.position) {
if (adminQueue.length < 300) {
adminQueue.push({
lat: g.position.lat,
lng: g.position.lng,
count: p.count || 1,
});
}
}
return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
});
return mcAdd.call(this, patched);
};
}
tmLog("wrapped MarkerCluster (wait)");
}
// hook Map prototype以捕获已有实例的后续调用
if (TMap.Map && TMap.Map.prototype && !TMap.Map.prototype.__TM_HOOKED) {
const mp = TMap.Map.prototype;
const hook = (name) => {
const orig = mp[name];
if (typeof orig === "function") {
mp[name] = function (...args) {
captureMap(this);
return orig.apply(this, args);
};
}
};
["setCenter", "easeTo", "setZoom", "zoomTo", "panTo"].forEach(hook);
mp.__TM_HOOKED = true;
tmLog("hooked Map prototype methods");
}
// hook MarkerCluster prototype以捕获已有实例的 add 调用(仅入队行政区,无点高亮)
if (
TMap.MarkerCluster &&
TMap.MarkerCluster.prototype &&
!TMap.MarkerCluster.prototype.__TM_HOOKED
) {
const mcp = TMap.MarkerCluster.prototype;
const origAddProto = mcp.add;
if (typeof origAddProto === "function") {
mcp.add = function (geos = []) {
if (!firstCluster) {
firstCluster = this;
tmLog("captured MarkerCluster via proto add");
}
const patched = geos.map((g) => {
const p = g.properties || {};
const hasPhoto =
p.count > 0 ||
p.url ||
p.photos?.length > 0 ||
p.images?.length > 0 ||
p.imgUrl ||
photoMeta.get(`${g.position?.lat},${g.position?.lng}`)
?.photoCount > 0;
if (hasPhoto && g.position) {
if (adminQueue.length < 300) {
adminQueue.push({
lat: g.position.lat,
lng: g.position.lng,
count: p.count || 1,
});
}
}
return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
});
return origAddProto.call(this, patched);
};
}
mcp.__TM_HOOKED = true;
tmLog("hooked MarkerCluster prototype add");
}
// 自定义样式
const getStyles = () => ({
default: new TMapRef.MarkerStyle({
width: 24,
height: 35,
src: photoIcon,
anchor: { x: 12, y: 32 },
}),
photoMarker: new TMapRef.MarkerStyle({
width: 26,
height: 36,
src: photoIcon,
anchor: { x: 13, y: 34 },
color: "#ffcc00",
strokeColor: "#ff8800",
strokeWidth: 2,
}),
});
// hook MultiMarker,合并样式并高亮(已禁用点高亮,仅入队行政区)
const RawMultiMarker = TMap.MultiMarker;
if (RawMultiMarker && !RawMultiMarker.__TM_WRAPPED) {
const WrappedMM = function (opts) {
if (!firstMap && opts?.map) firstMap = opts.map;
if (opts?.styles && opts?.geometries) {
opts.styles = Object.assign(getStyles(), opts.styles);
opts.geometries = opts.geometries.map((g) => {
const p = g.properties || {};
const key = `${g.position?.lat},${g.position?.lng}`;
const meta = photoMeta.get(key);
const hasPhoto =
p.photoCount > 0 ||
p.photos?.length > 0 ||
p.images?.length > 0 ||
p.imgUrl ||
meta?.photoCount > 0;
if (hasPhoto && g.position) {
if (adminQueue.length < 300) {
const item = {
lat: g.position.lat,
lng: g.position.lng,
count: p.count || 1,
};
const cellKey = `${item.lat.toFixed(3)},${item.lng.toFixed(
3
)}`;
// 若格网已缓存结果,减少重复入队
if (adminCellCache.has(cellKey)) {
const cached = adminCellCache.get(cellKey);
const existing = adminPolys.get(cached.name);
const weight = (existing?.weight || 0) + (item.count || 1);
adminPolys.set(cached.name, {
id: cached.name,
styleId: "region",
paths: cached.paths,
weight,
});
enqueueAdminDraw();
return { ...g, styleId: "photoMarker" };
}
// 可视范围内的优先处理
if (adminVisibleFirst(item.lat, item.lng)) {
adminQueue.unshift(item);
} else {
adminQueue.push(item);
}
}
}
return hasPhoto ? { ...g, styleId: "photoMarker" } : g;
});
}
return new RawMultiMarker(opts);
};
WrappedMM.__TM_WRAPPED = true;
WrappedMM.prototype = RawMultiMarker.prototype;
Object.setPrototypeOf(WrappedMM, RawMultiMarker);
TMap.MultiMarker = WrappedMM;
}
// 自动注入搜索框
const timer = setInterval(() => {
if (firstMap) {
clearInterval(timer);
addSearchBox(firstMap);
}
}, 500);
// 周期扫描 window,兜底抓 map(防止业务先解构了 Map 类)
const duckScan = () => {
if (firstMap) return;
scanAttempts += 1;
try {
const values = Object.values(window);
for (const v of values) {
if (
v &&
typeof v === "object" &&
typeof v.getCenter === "function" &&
typeof v.setCenter === "function" &&
typeof v.zoomTo === "function"
) {
captureMap(v);
addSearchBox(firstMap);
tmLog("captured map via duck-scan");
return;
}
}
} catch (_) {}
if (scanAttempts > 30) clearInterval(scanTimer);
};
const scanTimer = setInterval(duckScan, 500);
// 控制台入口
const runEnhance = () => {
if (!firstMap) {
tmLog("manual run skipped: map not ready");
return;
}
tmLog("manual run: addSearchBox");
addSearchBox(firstMap);
};
window.TM_RUN = runEnhance;
window.TM_LOG = () => {
tmLog("status", {
map: !!firstMap,
cluster: !!firstCluster,
TMapWrapped: !!(TMapRef && TMapRef.Map && TMapRef.Map.__TM_WRAPPED),
MCWrapped: !!(
TMapRef &&
TMapRef.MarkerCluster &&
TMapRef.MarkerCluster.__TM_WRAPPED
),
TX_KEY: window.TX_MAP_KEY,
});
};
window.TM_SET_MAP = (m) => {
captureMap(m);
addSearchBox(firstMap);
};
window.TM_SCAN = () => duckScan();
// 监控容器尝试抓实例(若 SDK 挂 _tmap_instance)
(() => {
const tryGrab = () => {
const el = document.getElementById("map-container");
if (el && el._tmap_instance) {
captureMap(el._tmap_instance);
tmLog("captured map from container _tmap_instance");
addSearchBox(firstMap);
return true;
}
return false;
};
const obs = new MutationObserver(() => {
if (tryGrab()) obs.disconnect();
});
obs.observe(document.documentElement, {
childList: true,
subtree: true,
});
setTimeout(() => obs.disconnect(), 15000);
setTimeout(tryGrab, 2000);
})();
// 行政区反查与渲染(bbox 近似)
const processAdmin = () => {
return;
if (!adminQueue.length || !firstMap || !TMapRef) return;
// 可视区域优先:取出第一个可视的,若无则取队首
let idx = adminQueue.findIndex((t) => adminVisibleFirst(t.lat, t.lng));
if (idx < 0) idx = 0;
const task = adminQueue.splice(idx, 1)[0];
if (!task) return;
const { lat, lng, count } = task;
const cellKey = `${lat.toFixed(3)},${lng.toFixed(3)}`;
// 若格网已缓存,直接复用
const cacheHit = adminCellCache.get(cellKey);
if (cacheHit && cacheHit.name && cacheHit.paths) {
const existing = adminPolys.get(cacheHit.name);
const weight = (existing?.weight || 0) + (count || 1);
adminPolys.set(cacheHit.name, {
id: cacheHit.name,
styleId: "region",
paths: cacheHit.paths,
weight,
});
enqueueAdminDraw();
tmLog("admin heat cache hit", {
name: cacheHit.name,
weight,
total: adminPolys.size,
});
return;
}
// 若已有同名行政区且包含当前点,直接复用
for (const [name, info] of adminRegionCache.entries()) {
if (latLngInBbox(lat, lng, info.bbox)) {
const existing = adminPolys.get(name);
const weight = (existing?.weight || 0) + (count || 1);
adminPolys.set(name, {
id: name,
styleId: "region",
paths: info.paths,
weight,
});
adminCellCache.set(cellKey, { name, paths: info.paths });
enqueueAdminDraw();
tmLog("admin heat region reuse", {
name,
weight,
total: adminPolys.size,
});
return;
}
}
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&zoom=10&addressdetails=1&polygon_geojson=1`;
const toPaths = (geojson) => {
if (!geojson) return null;
const coords =
geojson.type === "MultiPolygon"
? geojson.coordinates?.[0]?.[0]
: geojson.type === "Polygon"
? geojson.coordinates?.[0]
: null;
if (!coords || !coords.length) return null;
// 下采样,减少点数
const MAX_POINTS = 500;
const step = Math.max(1, Math.ceil(coords.length / MAX_POINTS));
const trimmed = coords.filter((_, i) => i % step === 0);
return [
trimmed.map(
(c) =>
new TMapRef.LatLng(parseFloat(c[1]) || 0, parseFloat(c[0]) || 0)
),
];
};
fetch(url, { headers: { "User-Agent": "userscript-admin" } })
.then((r) => r.json())
.then((j) => {
// 只保留到市级,县/区若存在城市则归并到城市
const cityLike =
j.address?.city || j.address?.town || j.address?.village;
const provinceLike = j.address?.state || j.address?.region;
const name = cityLike || provinceLike;
const bbox = j.boundingbox;
if (!name || !bbox || bbox.length < 4) return;
const south = parseFloat(bbox[0]);
const north = parseFloat(bbox[1]);
const west = parseFloat(bbox[2]);
const east = parseFloat(bbox[3]);
if (
!Number.isFinite(south) ||
!Number.isFinite(north) ||
!Number.isFinite(west) ||
!Number.isFinite(east)
)
return;
if (north - south > 10 || east - west > 10) return;
if (!adminLayer) {
adminLayer = new TMapRef.MultiPolygon({
map: firstMap,
styles: {
region: new TMapRef.PolygonStyle({
color: "rgba(255,153,0,0.12)",
showBorder: true,
borderColor: "rgba(255,102,0,0.5)",
borderWidth: 2,
}),
},
geometries: [],
});
tmLog("admin layer created");
}
const path = [
new TMapRef.LatLng(south, west),
new TMapRef.LatLng(south, east),
new TMapRef.LatLng(north, east),
new TMapRef.LatLng(north, west),
];
// 优先使用真实多边形,缺失时退回 bbox
const geoPaths = toPaths(j.geojson) || [path];
const id = name;
const existing = adminPolys.get(id);
const weight = (existing?.weight || 0) + (count || 1);
adminPolys.set(id, {
id,
styleId: "region",
paths: geoPaths,
weight,
});
const bboxArr = [south, north, west, east];
adminCellCache.set(cellKey, { name: id, paths: geoPaths });
adminRegionCache.set(id, { paths: geoPaths, bbox: bboxArr });
adminCacheTrim(adminCellCache, 600);
adminCacheTrim(adminRegionCache, 300);
tmLog("admin heat queue consumed", {
name: id,
weight,
total: adminPolys.size,
});
enqueueAdminDraw();
})
.catch(() => {});
};
setInterval(processAdmin, 1000);
})
.catch((err) => {
tmLog("TMap wait failed:", err);
});
})();
将这个脚本放到油猴里面就可以了。
javascript飞牛油猴
分享: