|
零、前言
由于 HomeAssistant 使用国外的地图服务器,导致地图瓦片加载缓慢甚至超时。
而百度地图、高德地图等第三方地图服务由于坐标系和API差异,通常需要使用第三方卡片或者APP。如果相关工具停止更新,可能需要花费大量时间进行迁移。
本教程选择天地图作为地图服务提供商,可以免费使用地图瓦片API(1万次/天),该方案不会导致坐标偏移,也无需修改任何现有的地图卡片。
完成设置后,将在网页、APP端中的所有原生地图组件中生效。
一、步骤
-
前往天地图官网,注册并获取服务许可(Key)。
-
打开 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 。
-
在 /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();
-
重启 HomeAssistant 即可生效。
二、原理
通过监听DOM树的变化,自动将 Carto 地图的瓦片图片链接替换为天地图的底图和标注图层链接。
由于天地图最大缩放级别为18,而默认地图缩放最大级别为20。当遇到请求的地图级别超过天地图最大支持级别时,将根据算法将瓦片贴图降级并放大,以解决放大后地图白屏的问题。
|
|