跳至正文
-
Subscribe to our newsletter & never miss our best posts. Subscribe Now!
BG6RSH's 随记.

我们总是在感叹昨天,忽视今天,幻想着明天中渡过,且过且珍惜.....

BG6RSH's 随记.

我们总是在感叹昨天,忽视今天,幻想着明天中渡过,且过且珍惜.....

  • 首页
  • 论坛
  • 随记
  • 编程
  • 软件
  • 留言
  • 首页
  • 论坛
  • 随记
  • 编程
  • 软件
  • 留言
关

搜索

  • https://www.facebook.com/
  • https://twitter.com/
  • https://t.me/
  • https://www.instagram.com/
  • https://youtube.com/
Subscribe
技术交流

技术交流 • Re: 更换Home Assistant的地图为高德地图,并接入Traccar服务

作者 admin
2026年10月15日 17 分钟阅读
0

HA升级到2026.9后,原代码已经不能用了,下面是高德与天地图的新代码:

tdt.png

1.配置文件:

Code: [全选] [Expand/Collapse]
  1. frontend:
  2.   themes: !include_dir_merge_named themes
  3.   extra_module_url:
  4.    – /local/tdt_tiles_fix.js
  5.     # – /local/amap_tiles_fix.js
GeSHi © Codebox Plus Extension

2.调用高德地图代码:

Code: [全选] [Expand/Collapse]
  1. /* amap_tiles_fix.js  v3.5.1  2026-09-22
  2.  * v3.5.1 修复:内存泄漏、MutationObserver未释放、FALLBACK_DOM开关失效、shadowRoots数组只增不减、全局patchLogLeft日志计数不稳定、原型劫持无法恢复;增加资源自动清理
  3.  * v3.5 重大修正:tLat 正弦项自变量由 x 改回 y,高德官方接口实测,误差<0.4米
  4.  */
  5. (function () {
  6.   "use strict";
  7.   if (window.__amapTilesFixLoaded) {
  8.     console.warn("[amap_fix] 检测到重复加载,本次跳过(如页面曾热更新过,请整页强刷)");
  9.     return;
  10.   }
  11.   window.__amapTilesFixLoaded = true;
  12.   var BASE = "vector";       // "vector" | "satellite"
  13.   var RETINA = false;        // true -> scale=2 高清瓦片
  14.   var FALLBACK_DOM = true;   // Leaflet 栅格回退路径的瓦片改写开关
  15.   var CONVERT_COORDS = true; // 坐标纠偏开关
  16.   var DBG = true;            // 调试日志开关(问题定位完可改 false)
  17.   var SCALE = RETINA ? 2 : 1;
  18.  
  19.   // 资源句柄保存,用于页面卸载清理
  20.   var mo = null;
  21.   var pollTimerId = null;
  22.   var origAttach = null;
  23.   var origAttachPatched = false;
  24.   var shadowRoots = [];
  25.   var coordSamples = [];
  26.   const MAX_SAMPLE_COUNT = 60;
  27.   const MAX_LOG_PER_RUN = 14;
  28.  
  29.   function log() {
  30.     if (!DBG) return;
  31.     console.log.apply(console, ["[amap_fix]"].concat([…arguments]));
  32.   }
  33.  
  34.   var counters = {
  35.     styleHit: 0, tilejsonHit: 0, spriteHit: 0, rasterDomHit: 0,
  36.     haMapSeen: 0, mapsHooked: 0, layersPatched: 0, sweeps: 0, clicksFix: 0
  37.   };
  38.   window.amapDbg = { counters: counters, samples: coordSamples, version: "3.5.1" };
  39.  
  40.   /* ================= GCJ-02 纠偏 ================= */
  41.   var PI = Math.PI, EE = 0.00669342162296594323, AA = 6378245.0;
  42.   function outOfChina(lat, lng) {
  43.     if (typeof lat !== "number" || typeof lng !== "number" ||
  44.         isNaN(lat) || isNaN(lng) || !isFinite(lat) || !isFinite(lng)) return true;
  45.     return !(lng > 73.66 && lng < 135.05 && lat > 3.86 && lat < 53.55);
  46.   }
  47.  
  48.   function tLat(x, y) {
  49.     var r = –100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y +
  50.       0.2 * Math.sqrt(Math.abs(x));
  51.     r += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
  52.     r += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
  53.     r += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
  54.     return r;
  55.   }
  56.   function tLng(x, y) {
  57.     var r = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y +
  58.       0.1 * Math.sqrt(Math.abs(x));
  59.     r += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
  60.     r += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
  61.     r += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
  62.     return r;
  63.   }
  64.   function wgs2gcj(lat, lng) {
  65.     if (outOfChina(lat, lng)) return [lat, lng];
  66.     var dLat = tLat(lng – 105.0, lat – 35.0);
  67.     var dLng = tLng(lng – 105.0, lat – 35.0);
  68.     var radLat = lat / 180.0 * PI;
  69.     var magic = 1 – EE * Math.sin(radLat) * Math.sin(radLat);
  70.     var sqrtMagic = Math.sqrt(magic);
  71.     dLat = (dLat * 180.0) / ((AA * (1 – EE)) / (magic * sqrtMagic) * PI);
  72.     dLng = (dLng * 180.0) / (AA / sqrtMagic * Math.cos(radLat) * PI);
  73.     return [lat + dLat, lng + dLng];
  74.   }
  75.   function gcj2wgs(lat, lng) {
  76.     if (outOfChina(lat, lng)) return [lat, lng];
  77.     var wLat = lat, wLng = lng, g, eLat, eLng;
  78.     for (var i = 0; i < 6; i++) {
  79.       g = wgs2gcj(wLat, wLng);
  80.       eLat = g[0] – lat;
  81.       eLng = g[1] – lng;
  82.       wLat -= eLat;
  83.       wLng -= eLng;
  84.       if (Math.abs(eLat) < 1e–9 && Math.abs(eLng) < 1e–9) break;
  85.     }
  86.     return [wLat, wLng];
  87.   }
  88.   window.amapDbg.test = function (lat, lng) {
  89.     var g = wgs2gcj(lat, lng);
  90.     var m = Math.round(Math.hypot((g[0] – lat) * 111320,
  91.       (g[1] – lng) * 111320 * Math.cos(lat * PI / 180)));
  92.     var r = { in: [lat, lng], out: g.map(function (v) { return +v.toFixed(6); }), shift_m: m, inChina: !outOfChina(lat, lng) };
  93.     console.table ? console.table(r) : console.log(r);
  94.     return r;
  95.   };
  96.  
  97.   function isLatLngObj(v) {
  98.     return v && typeof v === "object" && typeof v.lat === "number" &&
  99.       typeof v.lng === "number";
  100.   }
  101.   function isBoundsObj(v) {
  102.     return v && typeof v === "object" && isLatLngObj(v._southWest) &&
  103.       isLatLngObj(v._northEast);
  104.   }
  105.   function convInPlace(v, dir, tag, logCountRef) {
  106.     try {
  107.       if (isLatLngObj(v)) {
  108.         if (dir > 0 && v.__amapGcj) {
  109.           if (DBG && logCountRef.val > 0) {
  110.             logCountRef.val–;
  111.             log("跳过重复转换(同一坐标对象已转过)", tag);
  112.           }
  113.           return;
  114.         }
  115.         var oLat = v.lat, oLng = v.lng;
  116.         var g1 = dir > 0 ? wgs2gcj(v.lat, v.lng) : gcj2wgs(v.lat, v.lng);
  117.         v.lat = g1[0]; v.lng = g1[1];
  118.         if (dir > 0) {
  119.           try { v.__amapGcj = true; } catch (e) {}
  120.           if (tag && coordSamples.length < MAX_SAMPLE_COUNT) {
  121.             const exists = coordSamples.some(s => s.ref === v);
  122.             if (!exists) {
  123.               coordSamples.push({ tag: tag, ref: v, orig: [oLat, oLng] });
  124.             }
  125.           }
  126.         }
  127.         if (DBG && tag && logCountRef.val > 0) {
  128.           logCountRef.val–;
  129.           log("转换", tag, "+(" + (v.lat – oLat).toFixed(5) + "," +
  130.             (v.lng – oLng).toFixed(5) + ")", oLat.toFixed(5) + "," + oLng.toFixed(5) +
  131.             " -> " + v.lat.toFixed(5) + "," + v.lng.toFixed(5));
  132.         }
  133.         return;
  134.       }
  135.       if (isBoundsObj(v)) { convInPlace(v._southWest, dir, tag, logCountRef); convInPlace(v._northEast, dir, tag, logCountRef); return; }
  136.       if (Array.isArray(v)) {
  137.         if (v.length === 2 && typeof v[0] === "number" && typeof v[1] === "number") {
  138.           var o0 = v[0], o1 = v[1];
  139.           var g2 = dir > 0 ? wgs2gcj(v[0], v[1]) : gcj2wgs(v[0], v[1]);
  140.           v[0] = g2[0]; v[1] = g2[1];
  141.           if (dir > 0 && tag && coordSamples.length < MAX_SAMPLE_COUNT) {
  142.             const exists = coordSamples.some(s => s.ref.lat === v[0] && s.ref.lng === v[1]);
  143.             if (!exists) {
  144.               coordSamples.push({ tag: tag, ref: { lat: v[0], lng: v[1] }, orig: [o0, o1] });
  145.             }
  146.           }
  147.           return;
  148.         }
  149.         for (var i = 0; i < v.length; i++) convInPlace(v[i], dir, tag, logCountRef);
  150.       }
  151.     } catch (e) { log("坐标转换异常(忽略)", e); }
  152.   }
  153.   function typeName(layer) {
  154.     try { return (layer && layer.constructor && layer.constructor.name) || "?"; } catch (e) { return "?"; }
  155.   }
  156.   function patchLayerCoords(layer, depth) {
  157.     if (!layer || depth > 4 || layer.__amapConv) return;
  158.     layer.__amapConv = true;
  159.     counters.layersPatched++;
  160.     var t = typeName(layer);
  161.     const logCounter = { val: MAX_LOG_PER_RUN };
  162.     if (layer._latlng) convInPlace(layer._latlng, 1, t + "._latlng", logCounter);
  163.     if (layer._latlngs) convInPlace(layer._latlngs, 1, t + "._latlngs", logCounter);
  164.     if (isBoundsObj(layer._bounds)) convInPlace(layer._bounds, 1, null, logCounter);
  165.     if (layer._defaultCenter) convInPlace(layer._defaultCenter, 1, null, logCounter);
  166.     if (typeof layer.getLayers === "function") {
  167.       try {
  168.         var subs = layer.getLayers();
  169.         for (var i = 0; i < subs.length; i++) patchLayerCoords(subs[i], depth + 1);
  170.       } catch (e) {}
  171.     }
  172.     if (layer.decorationLayer) patchLayerCoords(layer.decorationLayer, depth + 1);
  173.     ensureProtoPatched(layer);
  174.   }
  175.   var PATCH_SETTERS = ["setLatLng", "setLatLngs", "setCenter", "setBounds"];
  176.   function ensureProtoPatched(layer) {
  177.     var p = Object.getPrototypeOf(layer);
  178.     if (!p || p.__amapProto) return;
  179.     p.__amapProto = true;
  180.     PATCH_SETTERS.forEach(function (m) {
  181.       if (typeof p[m] !== "function") return;
  182.       var orig = p[m];
  183.       p[m] = function () {
  184.         const logCounter = { val: MAX_LOG_PER_RUN };
  185.         for (var i = 0; i < arguments.length; i++) {
  186.           if (arguments[i] && typeof arguments[i] === "object") {
  187.             convInPlace(arguments[i], 1, "运行时" + m, logCounter);
  188.           }
  189.         }
  190.         try { this.__amapConv = true; } catch (e) {}
  191.         return orig.apply(this, arguments);
  192.       };
  193.     });
  194.     if (DBG) log("已钩住原型 " + typeName(layer) + " 的坐标 setter");
  195.   }
  196.   function installClickFix(el) {
  197.     if (el.__amapClick) return;
  198.     el.__amapClick = true;
  199.     el.addEventListener("map-clicked", function (e) {
  200.       if (e.__amapFixed) return;
  201.       var loc = e.detail && e.detail.location;
  202.       if (!loc || typeof loc[0] !== "number") return;
  203.       counters.clicksFix++;
  204.       log("点击坐标反算", loc[0].toFixed(5) + "," + loc[1].toFixed(5));
  205.       e.stopImmediatePropagation();
  206.       var w = gcj2wgs(loc[0], loc[1]);
  207.       var ne = new CustomEvent("map-clicked", {
  208.         detail: { location: w }, bubbles: true, composed: true, cancelable: true
  209.       });
  210.       ne.__amapFixed = true;
  211.       el.dispatchEvent(ne);
  212.     }, true);
  213.   }
  214.   function processMapEl(el) {
  215.     if (el.__amapDone) return;
  216.     counters.haMapSeen++;
  217.     var map = el.leafletMap;
  218.     if (counters.haMapSeen < 12) {
  219.       log("发现 ha-map 实例,leafletMap " + (map ? "已就绪" : "尚未初始化(等待重试)"), el);
  220.     }
  221.     installClickFix(el);
  222.     if (!map) { setTimeout(function () { processMapEl(el); }, 150); return; }
  223.     if (map.__amapAddHooked) return;
  224.     map.__amapAddHooked = true;
  225.     counters.mapsHooked++;
  226.     var proto = Object.getPrototypeOf(map);
  227.     if (!proto.__amapMapProto) {
  228.       proto.__amapMapProto = true;
  229.       var origAdd = proto.addLayer;
  230.       proto.addLayer = function (layer) {
  231.         try { if (CONVERT_COORDS) patchLayerCoords(layer, 0); }
  232.         catch (e) { log("addLayer 钩异常(放行)", e); }
  233.         return origAdd.apply(this, arguments);
  234.       };
  235.       log("已钩住 map.addLayer 原型");
  236.     }
  237.     try {
  238.       var before = counters.layersPatched;
  239.       map.eachLayer(function (l) { patchLayerCoords(l, 0); });
  240.       counters.sweeps++;
  241.       log("存量图层补转完成,本次补转 " + (counters.layersPatched – before) + " 个图层");
  242.     } catch (e) { log("存量补转异常", e); }
  243.     el.__amapDone = true;
  244.   }
  245.   window.amapDbg.scanNow = function () {
  246.     var n = 0;
  247.     if (document.querySelector) {
  248.       document.querySelectorAll("ha-map").forEach(function (el) { processMapEl(el); n++; });
  249.     }
  250.     shadowRoots.forEach(function (sr) {
  251.       if (sr.querySelectorAll) { sr.querySelectorAll("ha-map").forEach(function (el) { processMapEl(el); n++; }); }
  252.     });
  253.     log("手动扫描:处理了 " + n + " 个 ha-map;计数:", JSON.stringify(counters));
  254.     return n;
  255.   };
  256.   window.amapDbg.compare = function () {
  257.     if (!coordSamples.length) {
  258.       console.warn("[amap_fix] 暂无样本(等地图加载出实体后再调用,或重进地图页)");
  259.       return [];
  260.     }
  261.     function distM(a, b) {
  262.       var dLat = (a[0] – b[0]) * 111320;
  263.       var dLng = (a[1] – b[1]) * Math.cos(a[0] * Math.PI / 180) * 111320;
  264.       return Math.round(Math.hypot(dLat, dLng));
  265.     }
  266.     var rows = coordSamples.slice(0, 20).map(function (s) {
  267.       var now = [s.ref.lat, s.ref.lng];
  268.       var exp1 = wgs2gcj(s.orig[0], s.orig[1]);
  269.       var exp2 = wgs2gcj(exp1[0], exp1[1]);
  270.       var dOK = distM(now, exp1), dDbl = distM(now, exp2), dNone = distM(now, s.orig);
  271.       var verdict = dOK <= 3 ? "OK" : (dDbl <= 3 ? "DOUBLE!" : (dNone <= 3 ? "NONE!" : (dOK <= 30 ? "NEAR" : "?")));
  272.       return {
  273.         tag: s.tag,
  274.         原始: s.orig.map(function (v) { return v.toFixed(5); }).join(","),
  275.         当前: now.map(function (v) { return v.toFixed(5); }).join(","),
  276.         期望: exp1.map(function (v) { return v.toFixed(5); }).join(","),
  277.         偏差米: dOK,
  278.         判定: verdict
  279.       };
  280.     });
  281.     console.table(rows);
  282.     return rows;
  283.   };
  284.  
  285.   /* ================= 底图替换 ================= */
  286.   function amapTemplates() {
  287.     var out = [];
  288.     for (var i = 1; i <= 4; i++) {
  289.       if (BASE === "satellite") {
  290.         out.push("https://webst0" + i + ".is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}");
  291.       } else {
  292.         out.push("https://webrd0" + i + ".is.autonavi.com/appmaptile" +
  293.           "?lang=zh_cn&size=1&scale=" + SCALE + "&style=8&x={x}&y={y}&z={z}");
  294.       }
  295.     }
  296.     return out;
  297.   }
  298.   var TILES = amapTemplates();
  299.   var STYLE_JSON = JSON.stringify({
  300.     version: 8,
  301.     name: "Amap (amap_tiles_fix v3.5.1)",
  302.     sources: {
  303.       amap: {
  304.         type: "raster", tiles: TILES, tileSize: 256,
  305.         minzoom: 3, maxzoom: 18, attribution: "\u00a9 AutoNavi-GC-2"
  306.       }
  307.     },
  308.     layers: [{ id: "amap-raster", type: "raster", source: "amap" }]
  309.   });
  310.   function tilejsonBody() {
  311.     return JSON.stringify({
  312.       tilejson: "3.0.1", name: "Amap (browser rewrite)", scheme: "xyz",
  313.       tiles: TILES, minzoom: 3, maxzoom: 18,
  314.       bounds: [–180, –85.051129, 180, 85.051129],
  315.       center: [116.397, 39.909, 11],
  316.       attribution: "\u00a9 AutoNavi-GC-2 \u00a9 OpenStreetMap contributors"
  317.     });
  318.   }
  319.   var SPRITE_PNG_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII=";
  320.   function b64ToBytes(b64) {
  321.     var bin = atob(b64), arr = new Uint8Array(bin.length);
  322.     for (var i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
  323.     return arr;
  324.   }
  325.   var spritePng = null;
  326.   try { spritePng = b64ToBytes(SPRITE_PNG_B64); } catch (e) { spritePng = null; }
  327.   function jsonRes(text) {
  328.     return new Response(text, { status: 200, headers: { "Content-Type": "application/json" } });
  329.   }
  330.   var origFetch = window.fetch;
  331.   window.fetch = function (input, init) {
  332.     try {
  333.       var url = (typeof input === "string" || input instanceof String)
  334.         ? String(input) : (input && input.url) || "";
  335.       if (/\/static\/map\/(light|dark)\.json/.test(url)) {
  336.         counters.styleHit++;
  337.         log("样式已替换为高德栅格 style #" + counters.styleHit, url);
  338.         return Promise.resolve(jsonRes(STYLE_JSON));
  339.       }
  340.       if (url.indexOf("/api/map_tiles/tilejson") !== –1) {
  341.         counters.tilejsonHit++;
  342.         return Promise.resolve(jsonRes(tilejsonBody()));
  343.       }
  344.       if (url.indexOf("/api/map_tiles/sprites/") !== –1) {
  345.         counters.spriteHit++;
  346.         if (/\.json(\?|#|$)/.test(url)) return Promise.resolve(jsonRes("{}"));
  347.         if (/\.png(\?|#|$)/.test(url) && spritePng) {
  348.           return Promise.resolve(new Response(spritePng, {
  349.             status: 200, headers: { "Content-Type": "image/png" }
  350.           }));
  351.         }
  352.       }
  353.     } catch (err) { /* 异常则原样透传 */ }
  354.     return origFetch.apply(this, arguments);
  355.   };
  356.  
  357.   /* ================= shadow DOM 观察 ================= */
  358.   function observeRoot(root) {
  359.     try {
  360.       mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["src"] });
  361.     } catch (e) {}
  362.   }
  363.   function registerShadow(sh) {
  364.     if (!sh || shadowRoots.includes(sh)) return;
  365.     shadowRoots.push(sh);
  366.     observeRoot(sh);
  367.   }
  368.   // 劫持 attachShadow,增加标记防止重复劫持
  369.   if (!origAttachPatched) {
  370.     origAttach = Element.prototype.attachShadow;
  371.     Element.prototype.attachShadow = function (opt) {
  372.       var sh = origAttach.call(this, opt);
  373.       registerShadow(sh);
  374.       return sh;
  375.     };
  376.     origAttachPatched = true;
  377.   }
  378.  
  379.   function fixTileImg(img) {
  380.     if (!FALLBACK_DOM) return;
  381.     try {
  382.       var s = img.src || "";
  383.       if (s.indexOf("/api/map_tiles/raster/") === –1) return;
  384.       var m = s.match(/\/raster\/(\d+)\/(\d+)\/(\d+)\.png/);
  385.       if (!m) return;
  386.       var z = +m[1];
  387.       if (z > 18) return;
  388.       var x = +m[2], y = +m[3], sub = (x + y) % 4 + 1;
  389.       img.src = (BASE === "satellite")
  390.         ? "https://webst0" + sub + ".is.autonavi.com/appmaptile?style=6&x=" + x + "&y=" + y + "&z=" + z
  391.         : "https://webrd0" + sub + ".is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=" + SCALE + "&style=7&x=" + x + "&y=" + y + "&z=" + z;
  392.       counters.rasterDomHit++;
  393.     } catch (e) {}
  394.   }
  395.  
  396.   mo = new MutationObserver(function (list) {
  397.     list.forEach(function (rec) {
  398.       if (rec.addedNodes) {
  399.         rec.addedNodes.forEach(function (n) {
  400.           if (n.nodeType !== 1) return;
  401.           if (n.tagName === "IMG") fixTileImg(n);
  402.           else if (n.querySelectorAll) n.querySelectorAll("img").forEach(fixTileImg);
  403.           if (!CONVERT_COORDS) return;
  404.           try {
  405.             if (n.tagName === "HA-MAP") processMapEl(n);
  406.             else if (n.shadowRoot) registerShadow(n.shadowRoot);
  407.             else if (n.querySelector && n.querySelector("ha-map")) {
  408.               n.querySelectorAll("ha-map").forEach(processMapEl);
  409.             }
  410.           } catch (e) { log("观察回调异常", e); }
  411.         });
  412.       }
  413.       if (rec.type === "attributes" && rec.target && rec.target.tagName === "IMG") {
  414.         fixTileImg(rec.target);
  415.       }
  416.     });
  417.   });
  418.   observeRoot(document);
  419.  
  420.   if (CONVERT_COORDS) {
  421.     pollTimerId = setInterval(function () {
  422.       try {
  423.         if (document.querySelectorAll) {
  424.           document.querySelectorAll("ha-map").forEach(processMapEl);
  425.         }
  426.         for (var i = 0; i < shadowRoots.length; i++) {
  427.           var sr = shadowRoots[i];
  428.           if (sr.querySelectorAll) sr.querySelectorAll("ha-map").forEach(processMapEl);
  429.         }
  430.       } catch (e) { /* 静默 */ }
  431.     }, 800);
  432.   }
  433.  
  434.   // 页面卸载/SPA路由切换,资源清理,恢复原生原型
  435.   function cleanup() {
  436.     log("[amap_fix] 执行资源清理");
  437.     if (pollTimerId) clearInterval(pollTimerId);
  438.     if (mo) mo.disconnect();
  439.     if (origAttachPatched) {
  440.       Element.prototype.attachShadow = origAttach;
  441.       origAttachPatched = false;
  442.     }
  443.     shadowRoots.length = 0;
  444.     coordSamples.length = 0;
  445.     window.__amapTilesFixLoaded = false;
  446.   }
  447.   window.addEventListener("beforeunload", cleanup);
  448.   // HA SPA 路由跳转监听(HA前端使用popstate)
  449.   window.addEventListener("popstate", cleanup);
  450.  
  451.   log("v3.5.1 启动完成 | base=" + BASE + " retina=" + RETINA +
  452.     " 坐标纠偏=" + CONVERT_COORDS + " DOM回退=" + FALLBACK_DOM +
  453.     " | 调试命令: amapDbg.counters / amapDbg.test(纬度,经度) / amapDbg.scanNow() / amapDbg.compare()");
  454. })();
GeSHi © Codebox Plus Extension

3.调用天地图代码:

Code: [全选] [Expand/Collapse]
  1. /* tdt_tiles_fix.js  v2.0  2026-09-23
  2.  *
  3.  * 【v2.0 重大修正】
  4.  *   v1.0 灰屏根因:WMTS 端点的 LAYER 标识符必须不带 _w 后缀(LAYER=vec),
  5.  *   脚本拼的 LAYER=vec_w 天地图不报错、只返回 103 字节空瓦片——所以
  6.  *   "200 但浅灰无内容"。本版改用 DataServer 端点:T=vec_w 直接可用
  7.  *   (已实测 vec/cva/img/cia 四类瓦片均返回真实内容)。
  8.  *   同时吸收 gist 的三项改进:
  9.  *   1. Leaflet 回退路径改为代理 img.src setter + 入 DOM 时替换:
  10.  *      backgroundImage 放底图、src 放注记,一个元素叠两层;
  11.  *   2. 19/20 级降级:取 18 级父瓦放大填充,不再留空白;
  12.  *   3. 暗色主题滤镜注入 ha-map 的 shadow root,深色模式地图自动反色。
  13.  *
  14.  * 【坐标系】天地图 CGCS2000 ≈ WGS84(厘米级差异),本脚本不做任何坐标转换。
  15.  *
  16.  * 【安装】
  17.  *   1) 覆盖 <config目录>/www/tdt_tiles.js(或任意名,与 yaml 对应即可)
  18.  *   2) configuration.yaml:
  19.  *        frontend:
  20.  *          extra_module_url:
  21.  *            – /local/tdt_tiles.js
  22.  *   3) 重启 HA,浏览器 Ctrl+F5。回滚:删 yaml 两行 + 本文件。
  23.  *   与高德版互斥:两者都劫持 light.json,保留一个。
  24.  *
  25.  * 【配置】TDT_KEY:天地图 key;若控制台配了域名白名单,需包含 HA 访问域名。
  26.  *         BASE:"vector"=vec_w+cva_w / "satellite"=img_w+cia_w(都带中文标注)
  27.  *         DBG:诊断日志开关。
  28.  * 【合规】attribution 保留天地图声明,勿随意外发或去标。
  29.  */
  30. (function () {
  31.   "use strict";
  32.  
  33.   if (window.__tdtTilesFixLoaded) {
  34.     console.warn("[tdt_fix] 检测到重复加载,跳过(请整页强刷)");
  35.     return;
  36.   }
  37.   window.__tdtTilesFixLoaded = true;
  38.  
  39.   var TDT_KEY = "093bd09dad43c9948c11506cc9fa2254";
  40.   var BASE = "vector";      // "vector" | "satellite"
  41.   var FALLBACK_DOM = true;  // Leaflet 栅格回退路径(无 WebGL2 时)改写开关
  42.   var DARK_FILTER = true;   // 暗色主题给画布加反色滤镜(借鉴 gist)
  43.   var DBG = true;
  44.  
  45.   var LAYER_BASE = BASE === "satellite" ? "img_w" : "vec_w";
  46.   var LAYER_LABEL = BASE === "satellite" ? "cia_w" : "cva_w";
  47.  
  48.   function log() {
  49.     if (DBG && console.log) console.log.apply(console, ["[tdt_fix]"].concat([].slice.call(arguments)));
  50.   }
  51.  
  52.   /* ———- 天地图 DataServer 端点(T= 参数接受 vec_w 等带后缀名) ———- */
  53.   function tileTemplate(layer) {
  54.     var out = [];
  55.     for (var i = 0; i < 8; i++) {
  56.       out.push("https://t" + i + ".tianditu.gov.cn/DataServer?T=" + layer +
  57.         "&x={x}&y={y}&l={z}&tk=" + TDT_KEY);
  58.     }
  59.     return out;
  60.   }
  61.   function tileUrl(layer, z, x, y) {
  62.     var sub = "t" + ((x + y) % 8);
  63.     return "https://" + sub + ".tianditu.gov.cn/DataServer?T=" + layer +
  64.       "&x=" + x + "&y=" + y + "&l=" + z + "&tk=" + TDT_KEY;
  65.   }
  66.  
  67.   /* ———- MapLibre 栅格样式:底图 + 注记双图层 ———- */
  68.   var STYLE_JSON = JSON.stringify({
  69.     version: 8,
  70.     name: "TDT " + LAYER_BASE + "+" + LAYER_LABEL + " (tdt_tiles_fix v2.0)",
  71.     sources: {
  72.       tdt_base: { type: "raster", tiles: tileTemplate(LAYER_BASE), tileSize: 256, minzoom: 1, maxzoom: 18 },
  73.       tdt_label: { type: "raster", tiles: tileTemplate(LAYER_LABEL), tileSize: 256, minzoom: 1, maxzoom: 18 }
  74.     },
  75.     layers: [
  76.       { id: "tdt_base", type: "raster", source: "tdt_base" },
  77.       { id: "tdt_label", type: "raster", source: "tdt_label" }
  78.     ]
  79.   });
  80.  
  81.   function jsonRes(text) {
  82.     return new Response(text, { status: 200, headers: { "Content-Type": "application/json" } });
  83.   }
  84.  
  85.   /* ———- fetch 劫持:样式替换 + tilejson/sprites 兜底 ———- */
  86.   var styleHits = 0;
  87.   var origFetch = window.fetch;
  88.   window.fetch = function (input, init) {
  89.     try {
  90.       var url = (typeof input === "string" || input instanceof String)
  91.         ? String(input) : (input && input.url) || "";
  92.       if (/\/static\/map\/(light|dark)\.json/.test(url)) {
  93.         log("样式已替换为天地图 DataServer 栅格 " + LAYER_BASE + "+" + LAYER_LABEL +
  94.           " #" + (++styleHits), url);
  95.         return Promise.resolve(jsonRes(STYLE_JSON));
  96.       }
  97.       if (url.indexOf("/api/map_tiles/tilejson") !== –1) {
  98.         return Promise.resolve(jsonRes(JSON.stringify({
  99.           tilejson: "3.0.0", name: "Tianditu", scheme: "xyz",
  100.           tiles: tileTemplate(LAYER_BASE), minzoom: 1, maxzoom: 18,
  101.           attribution: "\u00a9 \u5929\u5730\u56fe"
  102.         })));
  103.       }
  104.       if (url.indexOf("/api/map_tiles/sprites/") !== –1) {
  105.         if (/\.json(\?|#|$)/.test(url)) return Promise.resolve(jsonRes("{}"));
  106.       }
  107.     } catch (err) { /* 异常则原样透传 */ }
  108.     return origFetch.apply(this, arguments);
  109.   };
  110.  
  111.   /* ———- Leaflet 栅格回退路径(借鉴 gist:src setter 代理) ———-
  112.    * HA 无 WebGL2 时给 Leaflet 瓦片层设置 img.src=/api/map_tiles/raster/z/x/y.png。
  113.    * 这里代理 src setter:命中该模式先占位并暂存坐标,等 img 入 DOM 时
  114.    * backgroundImage 放底图、src 放注记(一个元素叠两层),并做 19/20 级降级。 */
  115.   var PLACEHOLDER = "data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=";
  116.   var RASTER_RE = /\/api\/map_tiles\/raster\/(\d+)\/(\d+)\/(\d+)\.png/;
  117.   var existsCoordSet = new Set();
  118.  
  119.   function handleImg(img) {
  120.     if (!img._tdt) return;
  121.     var z = img._tdt[0], x = img._tdt[1], y = img._tdt[2];
  122.     delete img._tdt;
  123.  
  124.     var scale = Math.pow(2, Math.max(0, z – 18));
  125.     var sx = Math.floor(x / scale), sy = Math.floor(y / scale), sz = Math.min(z, 18);
  126.  
  127.     if (scale > 1) {                       // 超出天地图 18 级:取父瓦放大
  128.       var dk = sx + "," + sy + "," + sz + "," + z;
  129.       if (existsCoordSet.has(dk)) {        // 同父瓦只需一份,其余隐藏
  130.         img.style.display = "none";
  131.         return;
  132.       }
  133.       img._downgradeKey = dk;
  134.       existsCoordSet.add(dk);
  135.       var m = img.style.transform && img.style.transform.match(
  136.         /translate3d\(([^,]+),\s*([^,]+),\s*([^)]+)\)/);
  137.       if (m) {
  138.         var tx = parseFloat(m[1]) – (x % scale) * 256;
  139.         var ty = parseFloat(m[2]) – (y % scale) * 256;
  140.         img.style.transform = img.style.transform.replace(
  141.           /translate3d\([^)]+\)/, "translate3d(" + tx + "px, " + ty + "px, 0)") +
  142.           " scale(" + scale + ")";
  143.       }
  144.       img.style.width = img.style.height = "256px";
  145.       img.style.backgroundSize = "256px 256px";
  146.       img.style.transformOrigin = "top left";
  147.     }
  148.  
  149.     img.style.backgroundImage = ‘url("’ + tileUrl(LAYER_BASE, sz, sx, sy) + ‘")’;
  150.     origSrcDesc.set.call(img, tileUrl(LAYER_LABEL, sz, sx, sy));
  151.     counters.domHits++;
  152.   }
  153.  
  154.   var origSrcDesc = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, "src");
  155.   if (FALLBACK_DOM && origSrcDesc && origSrcDesc.set) {
  156.     Object.defineProperty(HTMLImageElement.prototype, "src", {
  157.       configurable: true,
  158.       enumerable: origSrcDesc.enumerable,
  159.       get: origSrcDesc.get,
  160.       set: function (value) {
  161.         var m = String(value).match(RASTER_RE);
  162.         if (m) {
  163.           this._tdt = [+m[1], +m[2], +m[3]];
  164.           value = PLACEHOLDER;
  165.         }
  166.         origSrcDesc.set.call(this, value);
  167.       }
  168.     });
  169.   }
  170.  
  171.   var counters = { domHits: 0 };
  172.   var mo = new MutationObserver(function (list) {
  173.     list.forEach(function (rec) {
  174.       var i, n, imgs;
  175.       for (i = 0; i < rec.addedNodes.length; i++) {
  176.         n = rec.addedNodes[i];
  177.         if (n.nodeType !== 1) continue;
  178.         if (n.tagName === "IMG") handleImg(n);
  179.         else if (n.querySelectorAll) {
  180.           imgs = n.querySelectorAll("img");
  181.           for (var k = 0; k < imgs.length; k++) handleImg(imgs[k]);
  182.         }
  183.         if (n.shadowRoot) registerShadow(n.shadowRoot);
  184.       }
  185.       for (i = 0; i < rec.removedNodes.length; i++) {
  186.         n = rec.removedNodes[i];
  187.         if (n.nodeType !== 1) continue;
  188.         imgs = (n.tagName === "IMG") ? [n] : (n.querySelectorAll ? n.querySelectorAll("img") : []);
  189.         for (k = 0; k < imgs.length; k++) {
  190.           if (imgs[k]._downgradeKey) existsCoordSet.delete(imgs[k]._downgradeKey);
  191.         }
  192.       }
  193.     });
  194.   });
  195.  
  196.   function observeRoot(root) {
  197.     try { mo.observe(root, { childList: true, subtree: true }); } catch (e) {}
  198.   }
  199.   var shadowRoots = [];
  200.   function registerShadow(sh) {
  201.     if (!sh || shadowRoots.indexOf(sh) !== –1) return;
  202.     shadowRoots.push(sh);
  203.     observeRoot(sh);
  204.     // 暗色主题滤镜(借鉴 gist):ha-map 的 shadow 里给 MapLibre 画布反色
  205.     if (DARK_FILTER && sh.host && sh.host.localName === "ha-map") {
  206.       try {
  207.         var st = document.createElement("style");
  208.         st.textContent = "#map.dark .maplibregl-canvas{" +
  209.           "filter:invert(.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(.3)}";
  210.         sh.append(st);
  211.       } catch (e) {}
  212.     }
  213.   }
  214.   var origAttach = Element.prototype.attachShadow;
  215.   Element.prototype.attachShadow = function (opt) {
  216.     var sh = origAttach.call(this, opt);
  217.     registerShadow(sh);
  218.     return sh;
  219.   };
  220.   observeRoot(document);
  221.  
  222.   log("v2.0 启动完成 | 端点=DataServer 底图=" + LAYER_BASE + " 注记=" + LAYER_LABEL +
  223.     " | 坐标纠偏=不需要(CGCS2000≈WGS84) | 暗色滤镜=" + DARK_FILTER);
  224. })();
GeSHi © Codebox Plus Extension

统计信息: 发表于 由 BG6RSH — 周三 9月 23, 2026 10:47 am


作者

admin

关注我
其他文章
上一个

技术交流 • Traccar v6.16.0 火星坐标版Docker镜像发布

下一个

学习笔记 • Re: phpbb论坛语法高亮插件o0johntam0o产生空白行的解决方法

暂无评论!成为第一个。

发表回复 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理。

73,835条垃圾评论已被Akismet阻挡 (在新标签页中打开)

近期文章

  • 技术交流 • Re: traccar安卓客户端软件下载
  • 技术交流 • 更新docker版后有问题
  • 技术交流 • 在Windows IIS中部署Django项目
  • 学习笔记 • Re: phpbb论坛语法高亮插件o0johntam0o产生空白行的解决方法
  • 技术交流 • Re: 更换Home Assistant的地图为高德地图,并接入Traccar服务

近期评论

  • gg 发表在《Traccar服务软件高德地图适配版docker镜像安装教程》
  • admin 发表在《Traccar服务软件高德地图适配版docker镜像安装教程》
  • /api/positions/里addres 为什么是null啊? 发表在《Traccar服务软件高德地图适配版docker镜像安装教程》
  • gg 发表在《Traccar服务软件高德地图适配版docker镜像安装教程》
  • 9394 发表在《Traccar服务软件高德地图适配版docker镜像安装教程》

归档

  • 2026 年 10 月
  • 2025 年 6 月
  • 2023 年 6 月
  • 2022 年 10 月
  • 2021 年 5 月
  • 2021 年 4 月
  • 2020 年 11 月
  • 2020 年 10 月
  • 2020 年 6 月
  • 2019 年 11 月
  • 2019 年 8 月
  • 2019 年 6 月
  • 2019 年 1 月
  • 2018 年 12 月
  • 2018 年 11 月
  • 2017 年 9 月
  • 2016 年 7 月
  • 2016 年 5 月
  • 2015 年 12 月
  • 2015 年 4 月
  • 2015 年 1 月
  • 2014 年 12 月
  • 2014 年 11 月
  • 2014 年 10 月
  • 2014 年 9 月
  • 2014 年 8 月
  • 2014 年 7 月
  • 2014 年 6 月
  • 2014 年 5 月
  • 2013 年 10 月
  • 2013 年 9 月
  • 2013 年 8 月
  • 2013 年 7 月
  • 2013 年 6 月
  • 2013 年 5 月
  • 2013 年 4 月
  • 2013 年 3 月
  • 2013 年 2 月
  • 2013 年 1 月
  • 2012 年 12 月
  • 2012 年 11 月
  • 2012 年 10 月
  • 2012 年 6 月
  • 2012 年 3 月
  • 2011 年 8 月
  • 2011 年 5 月
  • 2011 年 4 月
  • 2011 年 3 月
  • 2011 年 1 月
  • 2010 年 8 月
  • 2010 年 6 月
  • 2010 年 4 月
  • 2010 年 3 月
  • 2010 年 2 月
  • 2010 年 1 月
  • 2009 年 12 月
  • 2009 年 11 月

分类

  • 学习笔记
  • 技术交流
  • 编程
  • 软件
  • 随记

其他操作

  • 注册
  • 登录
  • 条目 feed
  • 评论 feed
  • WordPress.org

标签

aliyun Android CRC DBGridEh ddns Delphi EhLib ESP8266 GP328D gp338plus GP3688 Modbus phpbb python QT raspberry S7-SCL sapi speech SpVoice Synology traccar UART Unicode virtualbox vmware WGS84 wordpress ZeroTier 写频 动态IP 图片尺寸 地图 外挂编程 天气预报 导航 打印 扩频 拼音首字母 支票快打 树莓派 爬虫 百度地图 西门子PLC 车联网API

友情链接

  • 私有云论坛
  • 百度开放云
  • 主机管理
  • My Cloud
  • 智能家居技术论坛
  • 精灵云
  • 雅苑私站
Copyright 2026 — BG6RSH's 随记.. All rights reserved. Blogsy WordPress Theme