技术交流 • Re: 更换Home Assistant的地图为高德地图,并接入Traccar服务
HA升级到2026.9后,原代码已经不能用了,下面是高德与天地图的新代码:
1.配置文件:
Code: [全选] [Expand/Collapse]
-
frontend:
-
themes: !include_dir_merge_named themes
-
extra_module_url:
-
– /local/tdt_tiles_fix.js
-
# – /local/amap_tiles_fix.js
2.调用高德地图代码:
Code: [全选] [Expand/Collapse]
-
/* amap_tiles_fix.js v3.5.1 2026-09-22
-
* v3.5.1 修复:内存泄漏、MutationObserver未释放、FALLBACK_DOM开关失效、shadowRoots数组只增不减、全局patchLogLeft日志计数不稳定、原型劫持无法恢复;增加资源自动清理
-
* v3.5 重大修正:tLat 正弦项自变量由 x 改回 y,高德官方接口实测,误差<0.4米
-
*/
-
(function () {
-
"use strict";
-
if (window.__amapTilesFixLoaded) {
-
console.warn("[amap_fix] 检测到重复加载,本次跳过(如页面曾热更新过,请整页强刷)");
-
return;
-
}
-
window.__amapTilesFixLoaded = true;
-
var BASE = "vector"; // "vector" | "satellite"
-
var RETINA = false; // true -> scale=2 高清瓦片
-
var FALLBACK_DOM = true; // Leaflet 栅格回退路径的瓦片改写开关
-
var CONVERT_COORDS = true; // 坐标纠偏开关
-
var DBG = true; // 调试日志开关(问题定位完可改 false)
-
var SCALE = RETINA ? 2 : 1;
-
-
// 资源句柄保存,用于页面卸载清理
-
var mo = null;
-
var pollTimerId = null;
-
var origAttach = null;
-
var origAttachPatched = false;
-
var shadowRoots = [];
-
var coordSamples = [];
-
const MAX_SAMPLE_COUNT = 60;
-
const MAX_LOG_PER_RUN = 14;
-
-
function log() {
-
if (!DBG) return;
-
console.log.apply(console, ["[amap_fix]"].concat([…arguments]));
-
}
-
-
var counters = {
-
styleHit: 0, tilejsonHit: 0, spriteHit: 0, rasterDomHit: 0,
-
haMapSeen: 0, mapsHooked: 0, layersPatched: 0, sweeps: 0, clicksFix: 0
-
};
-
window.amapDbg = { counters: counters, samples: coordSamples, version: "3.5.1" };
-
-
/* ================= GCJ-02 纠偏 ================= */
-
var PI = Math.PI, EE = 0.00669342162296594323, AA = 6378245.0;
-
function outOfChina(lat, lng) {
-
if (typeof lat !== "number" || typeof lng !== "number" ||
-
isNaN(lat) || isNaN(lng) || !isFinite(lat) || !isFinite(lng)) return true;
-
return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
-
}
-
-
function tLat(x, y) {
-
var r = –100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y +
-
0.2 * Math.sqrt(Math.abs(x));
-
r += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
-
r += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
-
r += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
-
return r;
-
}
-
function tLng(x, y) {
-
var r = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y +
-
0.1 * Math.sqrt(Math.abs(x));
-
r += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
-
r += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
-
r += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
-
return r;
-
}
-
function wgs2gcj(lat, lng) {
-
if (outOfChina(lat, lng)) return [lat, lng];
-
var dLat = tLat(lng – 105.0, lat – 35.0);
-
var dLng = tLng(lng – 105.0, lat – 35.0);
-
var radLat = lat / 180.0 * PI;
-
var magic = 1 – EE * Math.sin(radLat) * Math.sin(radLat);
-
var sqrtMagic = Math.sqrt(magic);
-
dLat = (dLat * 180.0) / ((AA * (1 – EE)) / (magic * sqrtMagic) * PI);
-
dLng = (dLng * 180.0) / (AA / sqrtMagic * Math.cos(radLat) * PI);
-
return [lat + dLat, lng + dLng];
-
}
-
function gcj2wgs(lat, lng) {
-
if (outOfChina(lat, lng)) return [lat, lng];
-
var wLat = lat, wLng = lng, g, eLat, eLng;
-
for (var i = 0; i < 6; i++) {
-
g = wgs2gcj(wLat, wLng);
-
eLat = g[0] – lat;
-
eLng = g[1] – lng;
-
wLat -= eLat;
-
wLng -= eLng;
-
if (Math.abs(eLat) < 1e–9 && Math.abs(eLng) < 1e–9) break;
-
}
-
return [wLat, wLng];
-
}
-
window.amapDbg.test = function (lat, lng) {
-
var g = wgs2gcj(lat, lng);
-
var m = Math.round(Math.hypot((g[0] – lat) * 111320,
-
(g[1] – lng) * 111320 * Math.cos(lat * PI / 180)));
-
var r = { in: [lat, lng], out: g.map(function (v) { return +v.toFixed(6); }), shift_m: m, inChina: !outOfChina(lat, lng) };
-
console.table ? console.table(r) : console.log(r);
-
return r;
-
};
-
-
function isLatLngObj(v) {
-
return v && typeof v === "object" && typeof v.lat === "number" &&
-
typeof v.lng === "number";
-
}
-
function isBoundsObj(v) {
-
return v && typeof v === "object" && isLatLngObj(v._southWest) &&
-
isLatLngObj(v._northEast);
-
}
-
function convInPlace(v, dir, tag, logCountRef) {
-
try {
-
if (isLatLngObj(v)) {
-
if (dir > 0 && v.__amapGcj) {
-
if (DBG && logCountRef.val > 0) {
-
logCountRef.val–;
-
log("跳过重复转换(同一坐标对象已转过)", tag);
-
}
-
return;
-
}
-
var oLat = v.lat, oLng = v.lng;
-
var g1 = dir > 0 ? wgs2gcj(v.lat, v.lng) : gcj2wgs(v.lat, v.lng);
-
v.lat = g1[0]; v.lng = g1[1];
-
if (dir > 0) {
-
try { v.__amapGcj = true; } catch (e) {}
-
if (tag && coordSamples.length < MAX_SAMPLE_COUNT) {
-
const exists = coordSamples.some(s => s.ref === v);
-
if (!exists) {
-
coordSamples.push({ tag: tag, ref: v, orig: [oLat, oLng] });
-
}
-
}
-
}
-
if (DBG && tag && logCountRef.val > 0) {
-
logCountRef.val–;
-
log("转换", tag, "+(" + (v.lat – oLat).toFixed(5) + "," +
-
(v.lng – oLng).toFixed(5) + ")", oLat.toFixed(5) + "," + oLng.toFixed(5) +
-
" -> " + v.lat.toFixed(5) + "," + v.lng.toFixed(5));
-
}
-
return;
-
}
-
if (isBoundsObj(v)) { convInPlace(v._southWest, dir, tag, logCountRef); convInPlace(v._northEast, dir, tag, logCountRef); return; }
-
if (Array.isArray(v)) {
-
if (v.length === 2 && typeof v[0] === "number" && typeof v[1] === "number") {
-
var o0 = v[0], o1 = v[1];
-
var g2 = dir > 0 ? wgs2gcj(v[0], v[1]) : gcj2wgs(v[0], v[1]);
-
v[0] = g2[0]; v[1] = g2[1];
-
if (dir > 0 && tag && coordSamples.length < MAX_SAMPLE_COUNT) {
-
const exists = coordSamples.some(s => s.ref.lat === v[0] && s.ref.lng === v[1]);
-
if (!exists) {
-
coordSamples.push({ tag: tag, ref: { lat: v[0], lng: v[1] }, orig: [o0, o1] });
-
}
-
}
-
return;
-
}
-
for (var i = 0; i < v.length; i++) convInPlace(v[i], dir, tag, logCountRef);
-
}
-
} catch (e) { log("坐标转换异常(忽略)", e); }
-
}
-
function typeName(layer) {
-
try { return (layer && layer.constructor && layer.constructor.name) || "?"; } catch (e) { return "?"; }
-
}
-
function patchLayerCoords(layer, depth) {
-
if (!layer || depth > 4 || layer.__amapConv) return;
-
layer.__amapConv = true;
-
counters.layersPatched++;
-
var t = typeName(layer);
-
const logCounter = { val: MAX_LOG_PER_RUN };
-
if (layer._latlng) convInPlace(layer._latlng, 1, t + "._latlng", logCounter);
-
if (layer._latlngs) convInPlace(layer._latlngs, 1, t + "._latlngs", logCounter);
-
if (isBoundsObj(layer._bounds)) convInPlace(layer._bounds, 1, null, logCounter);
-
if (layer._defaultCenter) convInPlace(layer._defaultCenter, 1, null, logCounter);
-
if (typeof layer.getLayers === "function") {
-
try {
-
var subs = layer.getLayers();
-
for (var i = 0; i < subs.length; i++) patchLayerCoords(subs[i], depth + 1);
-
} catch (e) {}
-
}
-
if (layer.decorationLayer) patchLayerCoords(layer.decorationLayer, depth + 1);
-
ensureProtoPatched(layer);
-
}
-
var PATCH_SETTERS = ["setLatLng", "setLatLngs", "setCenter", "setBounds"];
-
function ensureProtoPatched(layer) {
-
var p = Object.getPrototypeOf(layer);
-
if (!p || p.__amapProto) return;
-
p.__amapProto = true;
-
PATCH_SETTERS.forEach(function (m) {
-
if (typeof p[m] !== "function") return;
-
var orig = p[m];
-
p[m] = function () {
-
const logCounter = { val: MAX_LOG_PER_RUN };
-
for (var i = 0; i < arguments.length; i++) {
-
if (arguments[i] && typeof arguments[i] === "object") {
-
convInPlace(arguments[i], 1, "运行时" + m, logCounter);
-
}
-
}
-
try { this.__amapConv = true; } catch (e) {}
-
return orig.apply(this, arguments);
-
};
-
});
-
if (DBG) log("已钩住原型 " + typeName(layer) + " 的坐标 setter");
-
}
-
function installClickFix(el) {
-
if (el.__amapClick) return;
-
el.__amapClick = true;
-
el.addEventListener("map-clicked", function (e) {
-
if (e.__amapFixed) return;
-
var loc = e.detail && e.detail.location;
-
if (!loc || typeof loc[0] !== "number") return;
-
counters.clicksFix++;
-
log("点击坐标反算", loc[0].toFixed(5) + "," + loc[1].toFixed(5));
-
e.stopImmediatePropagation();
-
var w = gcj2wgs(loc[0], loc[1]);
-
var ne = new CustomEvent("map-clicked", {
-
detail: { location: w }, bubbles: true, composed: true, cancelable: true
-
});
-
ne.__amapFixed = true;
-
el.dispatchEvent(ne);
-
}, true);
-
}
-
function processMapEl(el) {
-
if (el.__amapDone) return;
-
counters.haMapSeen++;
-
var map = el.leafletMap;
-
if (counters.haMapSeen < 12) {
-
log("发现 ha-map 实例,leafletMap " + (map ? "已就绪" : "尚未初始化(等待重试)"), el);
-
}
-
installClickFix(el);
-
if (!map) { setTimeout(function () { processMapEl(el); }, 150); return; }
-
if (map.__amapAddHooked) return;
-
map.__amapAddHooked = true;
-
counters.mapsHooked++;
-
var proto = Object.getPrototypeOf(map);
-
if (!proto.__amapMapProto) {
-
proto.__amapMapProto = true;
-
var origAdd = proto.addLayer;
-
proto.addLayer = function (layer) {
-
try { if (CONVERT_COORDS) patchLayerCoords(layer, 0); }
-
catch (e) { log("addLayer 钩异常(放行)", e); }
-
return origAdd.apply(this, arguments);
-
};
-
log("已钩住 map.addLayer 原型");
-
}
-
try {
-
var before = counters.layersPatched;
-
map.eachLayer(function (l) { patchLayerCoords(l, 0); });
-
counters.sweeps++;
-
log("存量图层补转完成,本次补转 " + (counters.layersPatched – before) + " 个图层");
-
} catch (e) { log("存量补转异常", e); }
-
el.__amapDone = true;
-
}
-
window.amapDbg.scanNow = function () {
-
var n = 0;
-
if (document.querySelector) {
-
document.querySelectorAll("ha-map").forEach(function (el) { processMapEl(el); n++; });
-
}
-
shadowRoots.forEach(function (sr) {
-
if (sr.querySelectorAll) { sr.querySelectorAll("ha-map").forEach(function (el) { processMapEl(el); n++; }); }
-
});
-
log("手动扫描:处理了 " + n + " 个 ha-map;计数:", JSON.stringify(counters));
-
return n;
-
};
-
window.amapDbg.compare = function () {
-
if (!coordSamples.length) {
-
console.warn("[amap_fix] 暂无样本(等地图加载出实体后再调用,或重进地图页)");
-
return [];
-
}
-
function distM(a, b) {
-
var dLat = (a[0] – b[0]) * 111320;
-
var dLng = (a[1] – b[1]) * Math.cos(a[0] * Math.PI / 180) * 111320;
-
return Math.round(Math.hypot(dLat, dLng));
-
}
-
var rows = coordSamples.slice(0, 20).map(function (s) {
-
var now = [s.ref.lat, s.ref.lng];
-
var exp1 = wgs2gcj(s.orig[0], s.orig[1]);
-
var exp2 = wgs2gcj(exp1[0], exp1[1]);
-
var dOK = distM(now, exp1), dDbl = distM(now, exp2), dNone = distM(now, s.orig);
-
var verdict = dOK <= 3 ? "OK" : (dDbl <= 3 ? "DOUBLE!" : (dNone <= 3 ? "NONE!" : (dOK <= 30 ? "NEAR" : "?")));
-
return {
-
tag: s.tag,
-
原始: s.orig.map(function (v) { return v.toFixed(5); }).join(","),
-
当前: now.map(function (v) { return v.toFixed(5); }).join(","),
-
期望: exp1.map(function (v) { return v.toFixed(5); }).join(","),
-
偏差米: dOK,
-
判定: verdict
-
};
-
});
-
console.table(rows);
-
return rows;
-
};
-
-
/* ================= 底图替换 ================= */
-
function amapTemplates() {
-
var out = [];
-
for (var i = 1; i <= 4; i++) {
-
if (BASE === "satellite") {
-
out.push("https://webst0" + i + ".is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}");
-
} else {
-
out.push("https://webrd0" + i + ".is.autonavi.com/appmaptile" +
-
"?lang=zh_cn&size=1&scale=" + SCALE + "&style=8&x={x}&y={y}&z={z}");
-
}
-
}
-
return out;
-
}
-
var TILES = amapTemplates();
-
var STYLE_JSON = JSON.stringify({
-
version: 8,
-
name: "Amap (amap_tiles_fix v3.5.1)",
-
sources: {
-
amap: {
-
type: "raster", tiles: TILES, tileSize: 256,
-
minzoom: 3, maxzoom: 18, attribution: "\u00a9 AutoNavi-GC-2"
-
}
-
},
-
layers: [{ id: "amap-raster", type: "raster", source: "amap" }]
-
});
-
function tilejsonBody() {
-
return JSON.stringify({
-
tilejson: "3.0.1", name: "Amap (browser rewrite)", scheme: "xyz",
-
tiles: TILES, minzoom: 3, maxzoom: 18,
-
bounds: [–180, –85.051129, 180, 85.051129],
-
center: [116.397, 39.909, 11],
-
attribution: "\u00a9 AutoNavi-GC-2 \u00a9 OpenStreetMap contributors"
-
});
-
}
-
var SPRITE_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=";
-
function b64ToBytes(b64) {
-
var bin = atob(b64), arr = new Uint8Array(bin.length);
-
for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
-
return arr;
-
}
-
var spritePng = null;
-
try { spritePng = b64ToBytes(SPRITE_PNG_B64); } catch (e) { spritePng = null; }
-
function jsonRes(text) {
-
return new Response(text, { status: 200, headers: { "Content-Type": "application/json" } });
-
}
-
var origFetch = window.fetch;
-
window.fetch = function (input, init) {
-
try {
-
var url = (typeof input === "string" || input instanceof String)
-
? String(input) : (input && input.url) || "";
-
if (/\/static\/map\/(light|dark)\.json/.test(url)) {
-
counters.styleHit++;
-
log("样式已替换为高德栅格 style #" + counters.styleHit, url);
-
return Promise.resolve(jsonRes(STYLE_JSON));
-
}
-
if (url.indexOf("/api/map_tiles/tilejson") !== –1) {
-
counters.tilejsonHit++;
-
return Promise.resolve(jsonRes(tilejsonBody()));
-
}
-
if (url.indexOf("/api/map_tiles/sprites/") !== –1) {
-
counters.spriteHit++;
-
if (/\.json(\?|#|$)/.test(url)) return Promise.resolve(jsonRes("{}"));
-
if (/\.png(\?|#|$)/.test(url) && spritePng) {
-
return Promise.resolve(new Response(spritePng, {
-
status: 200, headers: { "Content-Type": "image/png" }
-
}));
-
}
-
}
-
} catch (err) { /* 异常则原样透传 */ }
-
return origFetch.apply(this, arguments);
-
};
-
-
/* ================= shadow DOM 观察 ================= */
-
function observeRoot(root) {
-
try {
-
mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["src"] });
-
} catch (e) {}
-
}
-
function registerShadow(sh) {
-
if (!sh || shadowRoots.includes(sh)) return;
-
shadowRoots.push(sh);
-
observeRoot(sh);
-
}
-
// 劫持 attachShadow,增加标记防止重复劫持
-
if (!origAttachPatched) {
-
origAttach = Element.prototype.attachShadow;
-
Element.prototype.attachShadow = function (opt) {
-
var sh = origAttach.call(this, opt);
-
registerShadow(sh);
-
return sh;
-
};
-
origAttachPatched = true;
-
}
-
-
function fixTileImg(img) {
-
if (!FALLBACK_DOM) return;
-
try {
-
var s = img.src || "";
-
if (s.indexOf("/api/map_tiles/raster/") === –1) return;
-
var m = s.match(/\/raster\/(\d+)\/(\d+)\/(\d+)\.png/);
-
if (!m) return;
-
var z = +m[1];
-
if (z > 18) return;
-
var x = +m[2], y = +m[3], sub = (x + y) % 4 + 1;
-
img.src = (BASE === "satellite")
-
? "https://webst0" + sub + ".is.autonavi.com/appmaptile?style=6&x=" + x + "&y=" + y + "&z=" + z
-
: "https://webrd0" + sub + ".is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=" + SCALE + "&style=7&x=" + x + "&y=" + y + "&z=" + z;
-
counters.rasterDomHit++;
-
} catch (e) {}
-
}
-
-
mo = new MutationObserver(function (list) {
-
list.forEach(function (rec) {
-
if (rec.addedNodes) {
-
rec.addedNodes.forEach(function (n) {
-
if (n.nodeType !== 1) return;
-
if (n.tagName === "IMG") fixTileImg(n);
-
else if (n.querySelectorAll) n.querySelectorAll("img").forEach(fixTileImg);
-
if (!CONVERT_COORDS) return;
-
try {
-
if (n.tagName === "HA-MAP") processMapEl(n);
-
else if (n.shadowRoot) registerShadow(n.shadowRoot);
-
else if (n.querySelector && n.querySelector("ha-map")) {
-
n.querySelectorAll("ha-map").forEach(processMapEl);
-
}
-
} catch (e) { log("观察回调异常", e); }
-
});
-
}
-
if (rec.type === "attributes" && rec.target && rec.target.tagName === "IMG") {
-
fixTileImg(rec.target);
-
}
-
});
-
});
-
observeRoot(document);
-
-
if (CONVERT_COORDS) {
-
pollTimerId = setInterval(function () {
-
try {
-
if (document.querySelectorAll) {
-
document.querySelectorAll("ha-map").forEach(processMapEl);
-
}
-
for (var i = 0; i < shadowRoots.length; i++) {
-
var sr = shadowRoots[i];
-
if (sr.querySelectorAll) sr.querySelectorAll("ha-map").forEach(processMapEl);
-
}
-
} catch (e) { /* 静默 */ }
-
}, 800);
-
}
-
-
// 页面卸载/SPA路由切换,资源清理,恢复原生原型
-
function cleanup() {
-
log("[amap_fix] 执行资源清理");
-
if (pollTimerId) clearInterval(pollTimerId);
-
if (mo) mo.disconnect();
-
if (origAttachPatched) {
-
Element.prototype.attachShadow = origAttach;
-
origAttachPatched = false;
-
}
-
shadowRoots.length = 0;
-
coordSamples.length = 0;
-
window.__amapTilesFixLoaded = false;
-
}
-
window.addEventListener("beforeunload", cleanup);
-
// HA SPA 路由跳转监听(HA前端使用popstate)
-
window.addEventListener("popstate", cleanup);
-
-
log("v3.5.1 启动完成 | base=" + BASE + " retina=" + RETINA +
-
" 坐标纠偏=" + CONVERT_COORDS + " DOM回退=" + FALLBACK_DOM +
-
" | 调试命令: amapDbg.counters / amapDbg.test(纬度,经度) / amapDbg.scanNow() / amapDbg.compare()");
-
})();
3.调用天地图代码:
Code: [全选] [Expand/Collapse]
-
/* tdt_tiles_fix.js v2.0 2026-09-23
-
*
-
* 【v2.0 重大修正】
-
* v1.0 灰屏根因:WMTS 端点的 LAYER 标识符必须不带 _w 后缀(LAYER=vec),
-
* 脚本拼的 LAYER=vec_w 天地图不报错、只返回 103 字节空瓦片——所以
-
* "200 但浅灰无内容"。本版改用 DataServer 端点:T=vec_w 直接可用
-
* (已实测 vec/cva/img/cia 四类瓦片均返回真实内容)。
-
* 同时吸收 gist 的三项改进:
-
* 1. Leaflet 回退路径改为代理 img.src setter + 入 DOM 时替换:
-
* backgroundImage 放底图、src 放注记,一个元素叠两层;
-
* 2. 19/20 级降级:取 18 级父瓦放大填充,不再留空白;
-
* 3. 暗色主题滤镜注入 ha-map 的 shadow root,深色模式地图自动反色。
-
*
-
* 【坐标系】天地图 CGCS2000 ≈ WGS84(厘米级差异),本脚本不做任何坐标转换。
-
*
-
* 【安装】
-
* 1) 覆盖 <config目录>/www/tdt_tiles.js(或任意名,与 yaml 对应即可)
-
* 2) configuration.yaml:
-
* frontend:
-
* extra_module_url:
-
* – /local/tdt_tiles.js
-
* 3) 重启 HA,浏览器 Ctrl+F5。回滚:删 yaml 两行 + 本文件。
-
* 与高德版互斥:两者都劫持 light.json,保留一个。
-
*
-
* 【配置】TDT_KEY:天地图 key;若控制台配了域名白名单,需包含 HA 访问域名。
-
* BASE:"vector"=vec_w+cva_w / "satellite"=img_w+cia_w(都带中文标注)
-
* DBG:诊断日志开关。
-
* 【合规】attribution 保留天地图声明,勿随意外发或去标。
-
*/
-
(function () {
-
"use strict";
-
-
if (window.__tdtTilesFixLoaded) {
-
console.warn("[tdt_fix] 检测到重复加载,跳过(请整页强刷)");
-
return;
-
}
-
window.__tdtTilesFixLoaded = true;
-
-
var TDT_KEY = "093bd09dad43c9948c11506cc9fa2254";
-
var BASE = "vector"; // "vector" | "satellite"
-
var FALLBACK_DOM = true; // Leaflet 栅格回退路径(无 WebGL2 时)改写开关
-
var DARK_FILTER = true; // 暗色主题给画布加反色滤镜(借鉴 gist)
-
var DBG = true;
-
-
var LAYER_BASE = BASE === "satellite" ? "img_w" : "vec_w";
-
var LAYER_LABEL = BASE === "satellite" ? "cia_w" : "cva_w";
-
-
function log() {
-
if (DBG && console.log) console.log.apply(console, ["[tdt_fix]"].concat([].slice.call(arguments)));
-
}
-
-
/* ———- 天地图 DataServer 端点(T= 参数接受 vec_w 等带后缀名) ———- */
-
function tileTemplate(layer) {
-
var out = [];
-
for (var i = 0; i < 8; i++) {
-
out.push("https://t" + i + ".tianditu.gov.cn/DataServer?T=" + layer +
-
"&x={x}&y={y}&l={z}&tk=" + TDT_KEY);
-
}
-
return out;
-
}
-
function tileUrl(layer, z, x, y) {
-
var sub = "t" + ((x + y) % 8);
-
return "https://" + sub + ".tianditu.gov.cn/DataServer?T=" + layer +
-
"&x=" + x + "&y=" + y + "&l=" + z + "&tk=" + TDT_KEY;
-
}
-
-
/* ———- MapLibre 栅格样式:底图 + 注记双图层 ———- */
-
var STYLE_JSON = JSON.stringify({
-
version: 8,
-
name: "TDT " + LAYER_BASE + "+" + LAYER_LABEL + " (tdt_tiles_fix v2.0)",
-
sources: {
-
tdt_base: { type: "raster", tiles: tileTemplate(LAYER_BASE), tileSize: 256, minzoom: 1, maxzoom: 18 },
-
tdt_label: { type: "raster", tiles: tileTemplate(LAYER_LABEL), tileSize: 256, minzoom: 1, maxzoom: 18 }
-
},
-
layers: [
-
{ id: "tdt_base", type: "raster", source: "tdt_base" },
-
{ id: "tdt_label", type: "raster", source: "tdt_label" }
-
]
-
});
-
-
function jsonRes(text) {
-
return new Response(text, { status: 200, headers: { "Content-Type": "application/json" } });
-
}
-
-
/* ———- fetch 劫持:样式替换 + tilejson/sprites 兜底 ———- */
-
var styleHits = 0;
-
var origFetch = window.fetch;
-
window.fetch = function (input, init) {
-
try {
-
var url = (typeof input === "string" || input instanceof String)
-
? String(input) : (input && input.url) || "";
-
if (/\/static\/map\/(light|dark)\.json/.test(url)) {
-
log("样式已替换为天地图 DataServer 栅格 " + LAYER_BASE + "+" + LAYER_LABEL +
-
" #" + (++styleHits), url);
-
return Promise.resolve(jsonRes(STYLE_JSON));
-
}
-
if (url.indexOf("/api/map_tiles/tilejson") !== –1) {
-
return Promise.resolve(jsonRes(JSON.stringify({
-
tilejson: "3.0.0", name: "Tianditu", scheme: "xyz",
-
tiles: tileTemplate(LAYER_BASE), minzoom: 1, maxzoom: 18,
-
attribution: "\u00a9 \u5929\u5730\u56fe"
-
})));
-
}
-
if (url.indexOf("/api/map_tiles/sprites/") !== –1) {
-
if (/\.json(\?|#|$)/.test(url)) return Promise.resolve(jsonRes("{}"));
-
}
-
} catch (err) { /* 异常则原样透传 */ }
-
return origFetch.apply(this, arguments);
-
};
-
-
/* ———- Leaflet 栅格回退路径(借鉴 gist:src setter 代理) ———-
-
* HA 无 WebGL2 时给 Leaflet 瓦片层设置 img.src=/api/map_tiles/raster/z/x/y.png。
-
* 这里代理 src setter:命中该模式先占位并暂存坐标,等 img 入 DOM 时
-
* backgroundImage 放底图、src 放注记(一个元素叠两层),并做 19/20 级降级。 */
-
var PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
-
var RASTER_RE = /\/api\/map_tiles\/raster\/(\d+)\/(\d+)\/(\d+)\.png/;
-
var existsCoordSet = new Set();
-
-
function handleImg(img) {
-
if (!img._tdt) return;
-
var z = img._tdt[0], x = img._tdt[1], y = img._tdt[2];
-
delete img._tdt;
-
-
var scale = Math.pow(2, Math.max(0, z – 18));
-
var sx = Math.floor(x / scale), sy = Math.floor(y / scale), sz = Math.min(z, 18);
-
-
if (scale > 1) { // 超出天地图 18 级:取父瓦放大
-
var dk = sx + "," + sy + "," + sz + "," + z;
-
if (existsCoordSet.has(dk)) { // 同父瓦只需一份,其余隐藏
-
img.style.display = "none";
-
return;
-
}
-
img._downgradeKey = dk;
-
existsCoordSet.add(dk);
-
var m = img.style.transform && img.style.transform.match(
-
/translate3d\(([^,]+),\s*([^,]+),\s*([^)]+)\)/);
-
if (m) {
-
var tx = parseFloat(m[1]) – (x % scale) * 256;
-
var ty = parseFloat(m[2]) – (y % scale) * 256;
-
img.style.transform = img.style.transform.replace(
-
/translate3d\([^)]+\)/, "translate3d(" + tx + "px, " + ty + "px, 0)") +
-
" scale(" + scale + ")";
-
}
-
img.style.width = img.style.height = "256px";
-
img.style.backgroundSize = "256px 256px";
-
img.style.transformOrigin = "top left";
-
}
-
-
img.style.backgroundImage = ‘url("’ + tileUrl(LAYER_BASE, sz, sx, sy) + ‘")’;
-
origSrcDesc.set.call(img, tileUrl(LAYER_LABEL, sz, sx, sy));
-
counters.domHits++;
-
}
-
-
var origSrcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
-
if (FALLBACK_DOM && origSrcDesc && origSrcDesc.set) {
-
Object.defineProperty(HTMLImageElement.prototype, "src", {
-
configurable: true,
-
enumerable: origSrcDesc.enumerable,
-
get: origSrcDesc.get,
-
set: function (value) {
-
var m = String(value).match(RASTER_RE);
-
if (m) {
-
this._tdt = [+m[1], +m[2], +m[3]];
-
value = PLACEHOLDER;
-
}
-
origSrcDesc.set.call(this, value);
-
}
-
});
-
}
-
-
var counters = { domHits: 0 };
-
var mo = new MutationObserver(function (list) {
-
list.forEach(function (rec) {
-
var i, n, imgs;
-
for (i = 0; i < rec.addedNodes.length; i++) {
-
n = rec.addedNodes[i];
-
if (n.nodeType !== 1) continue;
-
if (n.tagName === "IMG") handleImg(n);
-
else if (n.querySelectorAll) {
-
imgs = n.querySelectorAll("img");
-
for (var k = 0; k < imgs.length; k++) handleImg(imgs[k]);
-
}
-
if (n.shadowRoot) registerShadow(n.shadowRoot);
-
}
-
for (i = 0; i < rec.removedNodes.length; i++) {
-
n = rec.removedNodes[i];
-
if (n.nodeType !== 1) continue;
-
imgs = (n.tagName === "IMG") ? [n] : (n.querySelectorAll ? n.querySelectorAll("img") : []);
-
for (k = 0; k < imgs.length; k++) {
-
if (imgs[k]._downgradeKey) existsCoordSet.delete(imgs[k]._downgradeKey);
-
}
-
}
-
});
-
});
-
-
function observeRoot(root) {
-
try { mo.observe(root, { childList: true, subtree: true }); } catch (e) {}
-
}
-
var shadowRoots = [];
-
function registerShadow(sh) {
-
if (!sh || shadowRoots.indexOf(sh) !== –1) return;
-
shadowRoots.push(sh);
-
observeRoot(sh);
-
// 暗色主题滤镜(借鉴 gist):ha-map 的 shadow 里给 MapLibre 画布反色
-
if (DARK_FILTER && sh.host && sh.host.localName === "ha-map") {
-
try {
-
var st = document.createElement("style");
-
st.textContent = "#map.dark .maplibregl-canvas{" +
-
"filter:invert(.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(.3)}";
-
sh.append(st);
-
} catch (e) {}
-
}
-
}
-
var origAttach = Element.prototype.attachShadow;
-
Element.prototype.attachShadow = function (opt) {
-
var sh = origAttach.call(this, opt);
-
registerShadow(sh);
-
return sh;
-
};
-
observeRoot(document);
-
-
log("v2.0 启动完成 | 端点=DataServer 底图=" + LAYER_BASE + " 注记=" + LAYER_LABEL +
-
" | 坐标纠偏=不需要(CGCS2000≈WGS84) | 暗色滤镜=" + DARK_FILTER);
-
})();
统计信息: 发表于 由 BG6RSH — 周三 9月 23, 2026 10:47 am