看了https://bbs.hassbian.com/thread-29564-1-3.html这个帖子,这是一个非常巧妙的“偷梁换柱”方案,用于在 Home Assistant 的地图卡片中将 CartoCDN 替换为天地图,并支持高缩放级别的降级模拟。它通过 MutationObserver 实现了对 Shadow DOM 的完整支持,适用于前端无法直接修改地图配置的场景。
在它的方案原理基础上,我改写为高德瓦片地图的代码,在HA的配置目录config下新建www目录,将hass_gaode.js复制到www目录下。
修改configuration.yaml文件
Code: [全选] [Expand/Collapse]
-
# Loads default set of integrations. Do not remove.
-
default_config:
-
-
# Load frontend themes from the themes folder
-
frontend:
-
themes: !include_dir_merge_named themes
-
# *** 添加下面2行代码 ***
-
extra_module_url:
-
– /local/hass_gaode.js
hass_gaode.js 文件内容:
Code: [全选] [Expand/Collapse]
-
// ========== config ==========
-
const GAODE_KEY = ’21b3a11e940d284eecb6b66bdd090fcc’; // ←←← 换成自己的
-
const MAX_Z = 18; // 高德最大支持 18 级
-
const TILE_SIZE = 256; // 标准 256px 瓦片
-
// =================================
-
-
/* 降级算法:把高 z 映射到 maxZoom,并返回缩放/偏移量 */
-
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 offsetX = (x % scale) * TILE_SIZE / scale;
-
const offsetY = (y % scale) * TILE_SIZE / scale;
-
return {
-
srcX, srcY, srcZ: maxZoom,
-
scale,
-
dx: –offsetX * scale,
-
dy: –offsetY * scale
-
};
-
}
-
-
/* 高德使用 4 个域名做负载均衡 */
-
let subDomain = 0;
-
function nextSub() { subDomain = (subDomain + 1) % 4; return
wprd0${subDomain + 1}; } -
-
/* 生成高德瓦片地址
-
layer: vec 矢量底图 | cva 矢量注记 | img 影像底图 | cia 影像注记
-
*/
-
function buildGaodeUrl(x, y, z, layer) {
-
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}; -
}
-
-
/* 记录已降级过的 key,防止重复绘制 */
-
const doneSet = new Set();
-
-
/* 创建注记层 <img> */
-
function createCvaImg(cvaSrc, templateImg) {
-
const img = new Image();
-
img.src = cvaSrc;
-
img.className = ‘leaflet-tile’;
-
img.style.cssText = templateImg.style.cssText; // 复制位置/变换
-
img.style.mixBlendMode = ‘unset’;
-
img.onload = () => img.classList.add(‘leaflet-tile-loaded’);
-
return img;
-
}
-
-
/* 核心替换函数 */
-
function transformCartoImg(img, extraImgs) {
-
const src = img.src;
-
if (!src.includes(‘basemaps.cartocdn.com’)) return;
-
-
const m = src.match(/\/(\d+)\/(\d+)\/(\d+)(?:@2x)?\.png/);
-
if (!m) return;
-
let [, z, x, y] = m.map(Number);
-
-
/* 1. 需要降级的情况 */
-
if (z > MAX_Z) {
-
const { srcX, srcY, srcZ, scale, dx, dy } = downgradeTile(x, y, z, MAX_Z);
-
const key =
${srcX},${srcY},${srcZ},${z}; -
if (doneSet.has(key)) { // 已处理过,直接隐藏
-
img.src = ‘data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=’;
-
img.style.display = ‘none’;
-
return;
-
}
-
doneSet.add(key);
-
img.downgradeKey = key;
-
x = srcX; y = srcY; z = srcZ;
-
-
/* 叠加缩放+偏移到原有 transform 上 */
-
let t = img.style.transform || ”;
-
if (t.includes(‘translate3d(‘)) {
-
const [, tx, ty] = t.match(/translate3d\(([^,]+),\s*([^,]+)/) || [];
-
if (tx !== undefined) {
-
const nx = parseFloat(tx) + dx;
-
const ny = parseFloat(ty) + dy;
-
t = t.replace(/translate3d\([^)]+\)/,
translate3d(${nx}px, ${ny}px, 0px)); -
}
-
}
-
if (!t.includes(‘scale(‘)) t +=
scale(${scale}); -
img.style.transform = t;
-
img.style.transformOrigin = ‘top left’;
-
img.style.width = TILE_SIZE + ‘px’;
-
img.style.height = TILE_SIZE + ‘px’;
-
}
-
-
/* 2. 组装高德地址 */
-
const vecSrc = buildGaodeUrl(x, y, z, ‘vec’);
-
const cvaSrc = buildGaodeUrl(x, y, z, ‘cva’);
-
-
/* 3. 注入 DOM */
-
if (extraImgs) { // 来自 appendChild 拦截
-
const cvaImg = createCvaImg(cvaSrc, img);
-
cvaImg.style.transformOrigin = ‘top left’;
-
extraImgs.push(cvaImg);
-
img.src = vecSrc;
-
} else { // 直接修改
-
img.style.backgroundImage =
url("${vecSrc}"); -
img.style.backgroundSize =
${TILE_SIZE}px ${TILE_SIZE}px; -
img.src = cvaSrc;
-
}
-
console.debug(‘[高德替换]’, src, ‘→’, vecSrc, ‘+注记’);
-
}
-
-
/* ========== DOM 监听部分(与原脚本一致,仅函数名替换) ========== */
-
function initDomObserver() {
-
const _appendChild = Element.prototype.appendChild;
-
-
function onAdd(node) {
-
if (node.nodeType !== 1) return;
-
if (node.classList.contains(‘leaflet-layer’)) {
-
node.appendChild = function (child) {
-
if (child.classList?.contains(‘leaflet-tile-container’)) {
-
child.querySelectorAll(‘img’).forEach(transformCartoImg);
-
child.appendChild = function (frag) {
-
const addArr = [];
-
[…frag.children].forEach(n => n.tagName === ‘IMG’ && transformCartoImg(n, addArr));
-
addArr.forEach(i => frag.appendChild(i));
-
[…frag.children].forEach(n => n.style.display === ‘none’ && n.remove());
-
return _appendChild.call(this, frag);
-
};
-
}
-
return _appendChild.call(this, child);
-
};
-
const tc = node.querySelector(‘.leaflet-tile-container’);
-
if (tc) tc.appendChild = function (f) { […f.children].forEach(n => n.tagName === ‘IMG’ && transformCartoImg(n)); return _appendChild.call(this, f); };
-
} else if (node.classList?.contains(‘leaflet-control-attribution’)) {
-
node.remove(); // 隐藏原版权
-
}
-
if (node.shadowRoot) initShadow(node.shadowRoot);
-
}
-
function onRemove(node) {
-
if (node.nodeType !== 1) return;
-
if (node.tagName === ‘IMG’) {
-
if (node.downgradeKey) doneSet.delete(node.downgradeKey);
-
node.cvaImgRef?.remove();
-
}
-
}
-
const ob = new MutationObserver(list => list.forEach(m => {
-
m.addedNodes.forEach(onAdd);
-
m.removedNodes.forEach(onRemove);
-
}));
-
function initShadow(root) {
-
ob.observe(root, { childList: true, subtree: true });
-
root.querySelectorAll?.(‘.leaflet-layer’).forEach(onAdd);
-
}
-
ob.observe(document, { childList: true, subtree: true });
-
const orig = Element.prototype.attachShadow;
-
Element.prototype.attachShadow = function (opt) {
-
const sh = orig.call(this, opt);
-
initShadow(sh);
-
return sh;
-
};
-
initShadow(document.body);
-
}
-
-
initDomObserver();
原文作者: BG6RSH | 查看原帖