找回密码
 立即注册
查看: 80|回复: 0

[UI界面] 【天地图】非侵入式地图服务替换脚本(无需修改卡片和使用第三方APP)

[复制链接]
nxy 手机认证

1

主题

4

回帖

81

积分

注册会员

积分
81
金钱
76
HASS币
0
发表于 前天 17:21 | 显示全部楼层 |阅读模式

dashboard-map_0.png

零、前言

由于 HomeAssistant 使用国外的地图服务器,导致地图瓦片加载缓慢甚至超时。

而百度地图、高德地图等第三方地图服务由于坐标系和API差异,通常需要使用第三方卡片或者APP。如果相关工具停止更新,可能需要花费大量时间进行迁移。

本教程选择天地图作为地图服务提供商,可以免费使用地图瓦片API(1万次/天),该方案不会导致坐标偏移,也无需修改任何现有的地图卡片。

完成设置后,将在网页、APP端中的所有原生地图组件中生效。

一、步骤

  1. 前往天地图官网,注册并获取服务许可(Key)。

    • 注册为个人开发者即可。

    • 谨慎填写应用名称,如果乱填可能会被删除应用(可以重新创建)。

  2. 打开 HomeAssistant 配置文件夹,在 /config/configuration.yaml 文件中添加以下代码。

    # Load frontend themes from the themes folder
    frontend:
     themes: !include_dir_merge_named themes
     extra_module_url:
       - /local/replace-map.js
    • 如果frontend.extra_module_url已存在,则仅需添加一项 /local/replace-map.js
  3. /config/www 中新建 replace-map.js ,添加以下代码。

    const KEY = '【你的KEY】';
    
    const MAX_Z = 18;
    const TILE_SIZE = 256;
    
    /**
    * 将超出最大支持级别的tile xyz数据,降级到maxZoom,并返回变换参数
    * @param {number} x 目标级别下的x
    * @param {number} y 目标级别下的y
    * @param {number} z 目标级别
    * @param {number} maxZoom 服务端最大支持的级别
    * @returns {object} { srcX, srcY, srcZ, scale, dx, dy }
    */
    function downgradeTile(x, y, z, maxZoom) {
           if (z <= maxZoom) {
                   // 不需要降级
                   return { srcX: x, srcY: y, srcZ: z, scale: 1, dx: 0, dy: 0 };
           }
           const scale = 2 ** (z - maxZoom);
           const srcX = Math.floor(x / scale);
           const srcY = Math.floor(y / scale);
           const srcZ = maxZoom;
    
           // 计算该瓦片在目标级别下的起始位置
           const tileSize = 256; // 假设瓦片宽高256px
           const offsetX = (x % scale) * tileSize / scale;
           const offsetY = (y % scale) * tileSize / scale;
           return {
                   srcX,
                   srcY,
                   srcZ,
                   scale,
                   dx: -offsetX * scale,
                   dy: -offsetY * scale
           };
    }
    
    function initDomObserver() {
    
           function transformCartoImg(img) {
                   const src = img.src;
                   if (!src.startsWith('https://basemaps.cartocdn.com/')) {
                           return;
                   }
    
                   const match = src.match(/rastertiles\/voyager\/(\d+)\/(\d+)\/(\d+)@2x\.png/);
                   if (!match) {
                           return;
                   }
    
                   let [_, zStr, xStr, yStr] = match;
                   let z = parseInt(zStr);
                   let x = parseInt(xStr);
                   let y = parseInt(yStr);
    
                   if (z <= MAX_Z) {
                           const vecSrc = `http://t4.tianditu.com/DataServer?T=vec_w&x=${x}&y=${y}&l=${z}&tk=${KEY}`;
                           const cvaSrc = `http://t4.tianditu.com/DataServer?T=cva_w&x=${x}&y=${y}&l=${z}&tk=75f0434f240669f4a2df6359275146d2`;
                           console.log('替换图片:', src, '→', cvaSrc);
                           img.style.backgroundImage = `url("${vecSrc}")`;
                           img.src = cvaSrc;
                           return;
                   }
    
                   // 使用降级算法获取变换参数
                   const { srcX, srcY, srcZ, scale, dx, dy } = downgradeTile(x, y, z, MAX_Z);
    
                   const vecSrc = `http://t4.tianditu.com/DataServer?T=vec_w&x=${srcX}&y=${srcY}&l=${srcZ}&tk=${KEY}`;
                   const cvaSrc = `http://t4.tianditu.com/DataServer?T=cva_w&x=${srcX}&y=${srcY}&l=${srcZ}&tk=75f0434f240669f4a2df6359275146d2`;
    
                   // 设置 transform,拼接已有样式
                   // 从transform中提取translate3d值,并加入dx和dy值
                   if (img.style.transform && img.style.transform.includes('translate3d(')) {
                           // 提取translate3d(-xxxpx, -xxxpx, 0px)格式中的数值
                           const translateMatch = img.style.transform.match(/translate3d\(([^,]+),\s*([^,]+),\s*([^)]+)\)/);
                           if (translateMatch) {
                                   // 从px单位中提取数值
                                   const translateX = parseFloat(translateMatch[1]);
                                   const translateY = parseFloat(translateMatch[2]);
                                   // 将dx和dy加入到提取的translate值中
                                   const newTranslateX = translateX + dx;
                                   const newTranslateY = translateY + dy;
                                   // 替换原始的translate3d值
                                   img.style.transform = img.style.transform.replace(
                                           /translate3d\([^)]+\)/,
                                           `translate3d(${newTranslateX}px, ${newTranslateY}px, 0px)`
                                   );
                           }
                   }
    
                   if (!img.style.transform.includes('scale(')) {
                           img.style.transform = (img.style.transform || '') + ` scale(${scale})`;
                   }
    
                   // 设置或覆盖必要样式
                   img.style.width = TILE_SIZE + 'px';
                   img.style.height = TILE_SIZE + 'px';
                   img.style.objectFit = 'none';
                   img.style.transformOrigin = 'top left';
                   img.style.mixBlendMode = 'unset';
                   img.style.backgroundImage = `url("${vecSrc}")`;
                   img.style.backgroundSize = `${TILE_SIZE}px ${TILE_SIZE}px`;
    
                   console.log(`降级 z=${z} → ${MAX_Z}, src:`, cvaSrc);
                   img.src = cvaSrc;
           }
    
           function handleNewNode(node) {
                   if (!(node instanceof Element)) {
                           return;
                   }
                   if (node.tagName === 'IMG') {
                           transformCartoImg(node);
                   } else if (node.tagName === 'HA-MAP') {
                           const imgs = node.querySelectorAll?.('div.leaflet-tile-container > img') || [];
                           for (const img of imgs) {
                                   transformCartoImg(img);
                           }
                   }
    
                   if (node.shadowRoot) {
                           observer.observe(node.shadowRoot, {
                                   childList: true,
                                   subtree: true
                           });
                   }
           }
    
           const observer = new MutationObserver(mutations => {
                   for (const mutation of mutations) {
                           for (const node of mutation.addedNodes) {
                                   handleNewNode(node);
                           }
                   }
           });
    
           observer.observe(document, {
                   childList: true,
                   subtree: true
           });
    
           function observeShadowRoots(root) {
                   const queue = [root];
                   while (queue.length > 0) {
                           const el = queue.shift();
                           if (el.shadowRoot) {
                                   observer.observe(el.shadowRoot, {
                                           childList: true,
                                           subtree: true
                                   });
                                   queue.push(...el.shadowRoot.querySelectorAll('home-assistant,home-assistant-main,ha-drawer,ha-panel-lovelace,hui-root,hui-view-container,hui-panel-view,hui-map-card,ha-card,ha-map'));
                           }
                           if (el.children) {
                                   queue.push(...el.children);
                           }
                   }
           }
           observeShadowRoots(document.body);
    
           const originalAttachShadow = Element.prototype.attachShadow;
           Element.prototype.attachShadow = function (init) {
                   const shadow = originalAttachShadow.call(this, init);
                   observer.observe(shadow, {
                           childList: true,
                           subtree: true
                   });
                   return shadow;
           };
    }
    
    initDomObserver();
  4. 重启 HomeAssistant 即可生效。

二、原理

通过监听DOM树的变化,自动将 Carto 地图的瓦片图片链接替换为天地图的底图和标注图层链接。

由于天地图最大缩放级别为18,而默认地图缩放最大级别为20。当遇到请求的地图级别超过天地图最大支持级别时,将根据算法将瓦片贴图降级并放大,以解决放大后地图白屏的问题。



回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|Hassbian ( 晋ICP备17001384号-1 )

GMT+8, 2025-6-17 14:08 , Processed in 1.450483 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表