更换Home Assistant的地图为高德地图,并接入Traccar服务

家用宽带架设服务器的技术交流,编程技术交流。
回复
BG6RSH
帖子: 180
注册时间: 周日 6月 23, 2019 12:00 pm

更换Home Assistant的地图为高德地图,并接入Traccar服务

帖子 BG6RSH »

看了https://bbs.hassbian.com/thread-29564-1-3.html这个帖子,这是一个非常巧妙的“偷梁换柱”方案,用于在 Home Assistant 的地图卡片中将 CartoCDN 替换为天地图,并支持高缩放级别的降级模拟。它通过 MutationObserver 实现了对 Shadow DOM 的完整支持,适用于前端无法直接修改地图配置的场景。
a.jpeg
a.jpeg (156.6 KiB) 查看 280 次
在它的方案原理基础上,我改写为高德瓦片地图的代码,在HA的配置目录config下新建www目录,将hass_gaode.js复制到www目录下。
修改configuration.yaml文件
  1. # Loads default set of integrations. Do not remove.
  2. default_config:
  3.  
  4. # Load frontend themes from the themes folder
  5. frontend:
  6.   themes: !include_dir_merge_named themes
  7. # *** 添加下面2行代码 ***
  8.   extra_module_url:
  9.    - /local/hass_gaode.js
hass_gaode.js 文件内容:
  1. // ==========  config  ==========
  2. const GAODE_KEY = '21b3a11e940d284eecb6b66bdd090fcc';          // ←←← 换成自己的
  3. const MAX_Z     = 18;                       // 高德最大支持 18 级
  4. const TILE_SIZE = 256;                      // 标准 256px 瓦片
  5. // =================================
  6.  
  7. /* 降级算法:把高 z 映射到 maxZoom,并返回缩放/偏移量 */
  8. function downgradeTile(x, y, z, maxZoom) {
  9.   if (z <= maxZoom) {
  10.     return { srcX: x, srcY: y, srcZ: z, scale: 1, dx: 0, dy: 0 };
  11.   }
  12.   const scale = 2 ** (z - maxZoom);
  13.   const srcX  = Math.floor(x / scale);
  14.   const srcY  = Math.floor(y / scale);
  15.   const offsetX = (x % scale) * TILE_SIZE / scale;
  16.   const offsetY = (y % scale) * TILE_SIZE / scale;
  17.   return {
  18.     srcX, srcY, srcZ: maxZoom,
  19.     scale,
  20.     dx: -offsetX * scale,
  21.     dy: -offsetY * scale
  22.   };
  23. }
  24.  
  25. /* 高德使用 4 个域名做负载均衡 */
  26. let subDomain = 0;
  27. function nextSub() { subDomain = (subDomain + 1) % 4; return `wprd0${subDomain + 1}`; }
  28.  
  29. /* 生成高德瓦片地址
  30.    layer: vec 矢量底图  |  cva 矢量注记  |  img 影像底图  |  cia 影像注记
  31. */
  32. function buildGaodeUrl(x, y, z, layer) {
  33.   return `https://${nextSub()}.is.autonavi.com/appmaptile?style=7&tiletype=${layer}&x=${x}&y=${y}&z=${z}&lang=zh_cn&size=1&scale=1&key=${GAODE_KEY}`;
  34. }
  35.  
  36. /* 记录已降级过的 key,防止重复绘制 */
  37. const doneSet = new Set();
  38.  
  39. /* 创建注记层 <img> */
  40. function createCvaImg(cvaSrc, templateImg) {
  41.   const img       = new Image();
  42.   img.src         = cvaSrc;
  43.   img.className   = 'leaflet-tile';
  44.   img.style.cssText = templateImg.style.cssText;   // 复制位置/变换
  45.   img.style.mixBlendMode = 'unset';
  46.   img.onload = () => img.classList.add('leaflet-tile-loaded');
  47.   return img;
  48. }
  49.  
  50. /* 核心替换函数 */
  51. function transformCartoImg(img, extraImgs) {
  52.   const src = img.src;
  53.   if (!src.includes('basemaps.cartocdn.com')) return;
  54.  
  55.   const m = src.match(/\/(\d+)\/(\d+)\/(\d+)(?:@2x)?\.png/);
  56.   if (!m) return;
  57.   let [, z, x, y] = m.map(Number);
  58.  
  59.   /* 1. 需要降级的情况 */
  60.   if (z > MAX_Z) {
  61.     const { srcX, srcY, srcZ, scale, dx, dy } = downgradeTile(x, y, z, MAX_Z);
  62.     const key = `${srcX},${srcY},${srcZ},${z}`;
  63.     if (doneSet.has(key)) {          // 已处理过,直接隐藏
  64.       img.src = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';
  65.       img.style.display = 'none';
  66.       return;
  67.     }
  68.     doneSet.add(key);
  69.     img.downgradeKey = key;
  70.     x = srcX; y = srcY; z = srcZ;
  71.  
  72.     /* 叠加缩放+偏移到原有 transform 上 */
  73.     let t = img.style.transform || '';
  74.     if (t.includes('translate3d(')) {
  75.       const [, tx, ty] = t.match(/translate3d\(([^,]+),\s*([^,]+)/) || [];
  76.       if (tx !== undefined) {
  77.         const nx = parseFloat(tx) + dx;
  78.         const ny = parseFloat(ty) + dy;
  79.         t = t.replace(/translate3d\([^)]+\)/, `translate3d(${nx}px, ${ny}px, 0px)`);
  80.       }
  81.     }
  82.     if (!t.includes('scale(')) t += ` scale(${scale})`;
  83.     img.style.transform = t;
  84.     img.style.transformOrigin = 'top left';
  85.     img.style.width  = TILE_SIZE + 'px';
  86.     img.style.height = TILE_SIZE + 'px';
  87.   }
  88.  
  89.   /* 2. 组装高德地址 */
  90.   const vecSrc = buildGaodeUrl(x, y, z, 'vec');
  91.   const cvaSrc = buildGaodeUrl(x, y, z, 'cva');
  92.  
  93.   /* 3. 注入 DOM */
  94.   if (extraImgs) {                       // 来自 appendChild 拦截
  95.     const cvaImg = createCvaImg(cvaSrc, img);
  96.     cvaImg.style.transformOrigin = 'top left';
  97.     extraImgs.push(cvaImg);
  98.     img.src = vecSrc;
  99.   } else {                               // 直接修改
  100.     img.style.backgroundImage = `url("${vecSrc}")`;
  101.     img.style.backgroundSize  = `${TILE_SIZE}px ${TILE_SIZE}px`;
  102.     img.src = cvaSrc;
  103.   }
  104.   console.debug('[高德替换]', src, '→', vecSrc, '+注记');
  105. }
  106.  
  107. /* ==========  DOM 监听部分(与原脚本一致,仅函数名替换) ========== */
  108. function initDomObserver() {
  109.   const _appendChild = Element.prototype.appendChild;
  110.  
  111.   function onAdd(node) {
  112.     if (node.nodeType !== 1) return;
  113.     if (node.classList.contains('leaflet-layer')) {
  114.       node.appendChild = function (child) {
  115.         if (child.classList?.contains('leaflet-tile-container')) {
  116.           child.querySelectorAll('img').forEach(transformCartoImg);
  117.           child.appendChild = function (frag) {
  118.             const addArr = [];
  119.             [...frag.children].forEach(n => n.tagName === 'IMG' && transformCartoImg(n, addArr));
  120.             addArr.forEach(i => frag.appendChild(i));
  121.             [...frag.children].forEach(n => n.style.display === 'none' && n.remove());
  122.             return _appendChild.call(this, frag);
  123.           };
  124.         }
  125.         return _appendChild.call(this, child);
  126.       };
  127.       const tc = node.querySelector('.leaflet-tile-container');
  128.       if (tc) tc.appendChild = function (f) { [...f.children].forEach(n => n.tagName === 'IMG' && transformCartoImg(n)); return _appendChild.call(this, f); };
  129.     } else if (node.classList?.contains('leaflet-control-attribution')) {
  130.       node.remove();                       // 隐藏原版权
  131.     }
  132.     if (node.shadowRoot) initShadow(node.shadowRoot);
  133.   }
  134.   function onRemove(node) {
  135.     if (node.nodeType !== 1) return;
  136.     if (node.tagName === 'IMG') {
  137.       if (node.downgradeKey) doneSet.delete(node.downgradeKey);
  138.       node.cvaImgRef?.remove();
  139.     }
  140.   }
  141.   const ob = new MutationObserver(list => list.forEach(m => {
  142.     m.addedNodes.forEach(onAdd);
  143.     m.removedNodes.forEach(onRemove);
  144.   }));
  145.   function initShadow(root) {
  146.     ob.observe(root, { childList: true, subtree: true });
  147.     root.querySelectorAll?.('.leaflet-layer').forEach(onAdd);
  148.   }
  149.   ob.observe(document, { childList: true, subtree: true });
  150.   const orig = Element.prototype.attachShadow;
  151.   Element.prototype.attachShadow = function (opt) {
  152.     const sh = orig.call(this, opt);
  153.     initShadow(sh);
  154.     return sh;
  155.   };
  156.   initShadow(document.body);
  157. }
  158.  
  159. initDomObserver();
回复