From 3f4393f99636199f65f05cd058f25452b6398387 Mon Sep 17 00:00:00 2001 From: Marti Kaljuve Date: Tue, 26 Nov 2024 14:51:31 +0200 Subject: [PATCH 1/6] add "npm run dev" for development. --- README.md | 15 +++++++++++++ package-lock.json | 54 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 4 +++- rollup.config.js | 16 +++++++++++++- 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e74caca..24b3439 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,21 @@ Custom buttons can be added to this card when vacuum_entity is set. Each custom | icon | string | mdi:radiobox-blank | The icon that will represent the custom button | text | string | "" | Optional text to display next to the icon +## Development + +1. Run Rollup in watch mode + + ```sh + npm run dev + ``` + +2. Enable **Advanced Mode** in your Home Assistant profile + +3. Add the bundle as a Lovelace resource in Home Assistant (**Settings > Dashboards > ⋮ > Resources > + Add Resource**) + ``` + http://localhost:5000/valetudo-map-card.js + ``` + ## License Lovelace Valetudo Map Card is licensed under the MIT license. diff --git a/package-lock.json b/package-lock.json index cce05ba..35c5478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "rollup-plugin-babel": "4.4.0", "rollup-plugin-commonjs": "10.1.0", "rollup-plugin-node-resolve": "5.2.0", + "rollup-plugin-serve": "1.1.1", "rollup-plugin-terser": "7.0.2", "rollup-plugin-typescript2": "0.33.0", "typescript": "4.8.2" @@ -2362,6 +2363,18 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2402,6 +2415,15 @@ "wrappy": "1" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -2748,6 +2770,16 @@ "rollup": ">=1.11.0" } }, + "node_modules/rollup-plugin-serve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-serve/-/rollup-plugin-serve-1.1.1.tgz", + "integrity": "sha512-H0VarZRtFR0lfiiC9/P8jzCDvtFf1liOX4oSdIeeYqUCKrmFA7vNiQ0rg2D+TuoP7leaa/LBR8XBts5viF6lnw==", + "dev": true, + "dependencies": { + "mime": "^2", + "opener": "1" + } + }, "node_modules/rollup-plugin-terser": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", @@ -4952,6 +4984,12 @@ "picomatch": "^2.3.1" } }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4989,6 +5027,12 @@ "wrappy": "1" } }, + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -5229,6 +5273,16 @@ "rollup-pluginutils": "^2.8.1" } }, + "rollup-plugin-serve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-serve/-/rollup-plugin-serve-1.1.1.tgz", + "integrity": "sha512-H0VarZRtFR0lfiiC9/P8jzCDvtFf1liOX4oSdIeeYqUCKrmFA7vNiQ0rg2D+TuoP7leaa/LBR8XBts5viF6lnw==", + "dev": true, + "requires": { + "mime": "^2", + "opener": "1" + } + }, "rollup-plugin-terser": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", diff --git a/package.json b/package.json index 85a9d35..bce6495 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "lint": "eslint -c .automated.eslintrc.json .", "lint_fix": "eslint -c .automated.eslintrc.json . --fix", - "build": "rollup -c" + "build": "rollup -c", + "dev": "rollup -c -w" }, "repository": { "type": "git", @@ -34,6 +35,7 @@ "rollup-plugin-node-resolve": "5.2.0", "rollup-plugin-terser": "7.0.2", "rollup-plugin-typescript2": "0.33.0", + "rollup-plugin-serve": "1.1.1", "typescript": "4.8.2", "@rollup/plugin-json": "4.1.0", "babel-plugin-inline-json-import": "0.3.2", diff --git a/rollup.config.js b/rollup.config.js index 68da660..76d65de 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,6 +4,19 @@ import nodeResolve from "rollup-plugin-node-resolve"; import babel from "rollup-plugin-babel"; import { terser } from "rollup-plugin-terser"; import json from "@rollup/plugin-json"; +import serve from 'rollup-plugin-serve'; + +const dev = process.env.ROLLUP_WATCH; + +/** @type {import("rollup-plugin-serve").ServeOptions} */ +const serveOpts = { + contentBase: ['./dist'], + host: '0.0.0.0', + port: 5000, + headers: { + 'Access-Control-Allow-Origin': '*' + } +}; const plugins = [ nodeResolve({}), @@ -16,7 +29,8 @@ const plugins = [ ["inline-json-import", {}] ] }), - terser(), + dev && serve(serveOpts), + !dev && terser(), ]; export default [ From 2bbfe4b7e23785014a5edda64074072f1a3f810b Mon Sep 17 00:00:00 2001 From: Marti Kaljuve Date: Wed, 27 Nov 2024 00:20:04 +0200 Subject: [PATCH 2/6] Use local config variable in drawMap() --- src/valetudo-map-card.js | 157 ++++++++++++++++++++------------------- 1 file changed, 79 insertions(+), 78 deletions(-) diff --git a/src/valetudo-map-card.js b/src/valetudo-map-card.js index f198837..02ba022 100644 --- a/src/valetudo-map-card.js +++ b/src/valetudo-map-card.js @@ -218,31 +218,32 @@ class ValetudoMapCard extends HTMLElement { } drawMap(attributes, mapHeight, mapWidth, boundingBox) { + const config = this._config; const pixelSize = attributes.pixelSize; - const widthScale = pixelSize / this._config.map_scale; - const heightScale = pixelSize / this._config.map_scale; + const widthScale = pixelSize / config.map_scale; + const heightScale = pixelSize / config.map_scale; let objectLeftOffset = 0; let objectTopOffset = 0; let mapLeftOffset = 0; let mapTopOffset = 0; - mapLeftOffset = ((boundingBox.minX) - 1) * this._config.map_scale; - mapTopOffset = ((boundingBox.minY) - 1) * this._config.map_scale; + mapLeftOffset = ((boundingBox.minX) - 1) * config.map_scale; + mapTopOffset = ((boundingBox.minY) - 1) * config.map_scale; // Calculate colours const homeAssistant = document.getElementsByTagName("home-assistant")[0]; - const floorColor = this.calculateColor(homeAssistant, this._config.floor_color, "--valetudo-map-floor-color", "--secondary-background-color"); - const wallColor = this.calculateColor(homeAssistant, this._config.wall_color, "--valetudo-map-wall-color", "--accent-color"); - const currentlyCleanedZoneColor = this.calculateColor(homeAssistant, this._config.currently_cleaned_zone_color, "--valetudo-currently_cleaned_zone_color", "--secondary-text-color"); - const noGoAreaColor = this.calculateColor(homeAssistant, this._config.no_go_area_color, "--valetudo-no-go-area-color", "--accent-color"); - const noMopAreaColor = this.calculateColor(homeAssistant, this._config.no_mop_area_color, "--valetudo-no-mop-area-color", "--secondary-text-color"); - const virtualWallColor = this.calculateColor(homeAssistant, this._config.virtual_wall_color, "--valetudo-virtual-wall-color", "--accent-color"); - const pathColor = this.calculateColor(homeAssistant, this._config.path_color, "--valetudo-map-path-color", "--primary-text-color"); - const chargerColor = this.calculateColor(homeAssistant, this._config.dock_color, "green"); - const vacuumColor = this.calculateColor(homeAssistant, this._config.vacuum_color, "--primary-text-color"); - const gotoTargetColor = this.calculateColor(homeAssistant, this._config.goto_target_color, "blue"); + const floorColor = this.calculateColor(homeAssistant, config.floor_color, "--valetudo-map-floor-color", "--secondary-background-color"); + const wallColor = this.calculateColor(homeAssistant, config.wall_color, "--valetudo-map-wall-color", "--accent-color"); + const currentlyCleanedZoneColor = this.calculateColor(homeAssistant, config.currently_cleaned_zone_color, "--valetudo-currently_cleaned_zone_color", "--secondary-text-color"); + const noGoAreaColor = this.calculateColor(homeAssistant, config.no_go_area_color, "--valetudo-no-go-area-color", "--accent-color"); + const noMopAreaColor = this.calculateColor(homeAssistant, config.no_mop_area_color, "--valetudo-no-mop-area-color", "--secondary-text-color"); + const virtualWallColor = this.calculateColor(homeAssistant, config.virtual_wall_color, "--valetudo-virtual-wall-color", "--accent-color"); + const pathColor = this.calculateColor(homeAssistant, config.path_color, "--valetudo-map-path-color", "--primary-text-color"); + const chargerColor = this.calculateColor(homeAssistant, config.dock_color, "green"); + const vacuumColor = this.calculateColor(homeAssistant, config.vacuum_color, "--primary-text-color"); + const gotoTargetColor = this.calculateColor(homeAssistant, config.goto_target_color, "blue"); // Create all objects const containerContainer = document.createElement("div"); @@ -250,29 +251,29 @@ class ValetudoMapCard extends HTMLElement { const drawnMapContainer = document.createElement("div"); const drawnMapCanvas = document.createElement("canvas"); - drawnMapCanvas.width = mapWidth * this._config.map_scale; - drawnMapCanvas.height = mapHeight * this._config.map_scale; + drawnMapCanvas.width = mapWidth * config.map_scale; + drawnMapCanvas.height = mapHeight * config.map_scale; drawnMapContainer.style.zIndex = 1; drawnMapContainer.appendChild(drawnMapCanvas); const chargerContainer = document.createElement("div"); const chargerHTML = document.createElement("ha-icon"); let chargerInfo = this.getChargerInfo(attributes); - if (this._config.show_dock && chargerInfo) { + if (config.show_dock && chargerInfo) { chargerHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up - chargerHTML.icon = this._config.dock_icon || "mdi:flash"; - chargerHTML.style.left = `${Math.floor(chargerInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * this._config.icon_scale)}px`; - chargerHTML.style.top = `${Math.floor(chargerInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * this._config.icon_scale)}px`; + chargerHTML.icon = config.dock_icon || "mdi:flash"; + chargerHTML.style.left = `${Math.floor(chargerInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; + chargerHTML.style.top = `${Math.floor(chargerInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; chargerHTML.style.color = chargerColor; - chargerHTML.style.transform = `scale(${this._config.icon_scale}, ${this._config.icon_scale}) rotate(-${this._config.rotate})`; + chargerHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(-${config.rotate})`; } chargerContainer.style.zIndex = 2; chargerContainer.appendChild(chargerHTML); const pathContainer = document.createElement("div"); const pathCanvas = document.createElement("canvas"); - pathCanvas.width = mapWidth * this._config.map_scale; - pathCanvas.height = mapHeight * this._config.map_scale; + pathCanvas.width = mapWidth * config.map_scale; + pathCanvas.height = mapHeight * config.map_scale; pathContainer.style.zIndex = 3; pathContainer.appendChild(pathCanvas); @@ -284,14 +285,14 @@ class ValetudoMapCard extends HTMLElement { robotInfo = this.lastValidRobotInfo; } - if (this._config.show_vacuum && robotInfo) { + if (config.show_vacuum && robotInfo) { this.lastValidRobotInfo = robotInfo; vacuumHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up - vacuumHTML.icon = this._config.vacuum_icon || "mdi:robot-vacuum"; + vacuumHTML.icon = config.vacuum_icon || "mdi:robot-vacuum"; vacuumHTML.style.color = vacuumColor; - vacuumHTML.style.left = `${Math.floor(robotInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * this._config.icon_scale)}px`; - vacuumHTML.style.top = `${Math.floor(robotInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * this._config.icon_scale)}px`; - vacuumHTML.style.transform = `scale(${this._config.icon_scale}, ${this._config.icon_scale})`; + vacuumHTML.style.left = `${Math.floor(robotInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; + vacuumHTML.style.top = `${Math.floor(robotInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; + vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; } vacuumContainer.style.zIndex = 4; vacuumContainer.appendChild(vacuumHTML); @@ -299,13 +300,13 @@ class ValetudoMapCard extends HTMLElement { const goToTargetContainer = document.createElement("div"); const goToTargetHTML = document.createElement("ha-icon"); let goToInfo = this.getGoToInfo(attributes); - if (this._config.show_goto_target && goToInfo) { + if (config.show_goto_target && goToInfo) { goToTargetHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up - goToTargetHTML.icon = this._config.goto_target_icon || "mdi:pin"; - goToTargetHTML.style.left = `${Math.floor(goToInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * this._config.icon_scale)}px`; - goToTargetHTML.style.top = `${Math.floor(goToInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (22 * this._config.icon_scale)}px`; + goToTargetHTML.icon = config.goto_target_icon || "mdi:pin"; + goToTargetHTML.style.left = `${Math.floor(goToInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; + goToTargetHTML.style.top = `${Math.floor(goToInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (22 * config.icon_scale)}px`; goToTargetHTML.style.color = gotoTargetColor; - goToTargetHTML.style.transform = `scale(${this._config.icon_scale}, ${this._config.icon_scale})`; + goToTargetHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; } goToTargetContainer.style.zIndex = 5; goToTargetContainer.appendChild(goToTargetHTML); @@ -318,8 +319,8 @@ class ValetudoMapCard extends HTMLElement { containerContainer.appendChild(goToTargetContainer); const mapCtx = drawnMapCanvas.getContext("2d"); - if (this._config.show_floor) { - mapCtx.globalAlpha = this._config.floor_opacity; + if (config.show_floor) { + mapCtx.globalAlpha = config.floor_opacity; mapCtx.strokeStyle = floorColor; mapCtx.lineWidth = 1; @@ -328,12 +329,12 @@ class ValetudoMapCard extends HTMLElement { let floorPoints = this.getFloorPoints(attributes); if (floorPoints) { for (let i = 0; i < floorPoints.length; i+=2) { - let x = (floorPoints[i] * this._config.map_scale) - mapLeftOffset; - let y = (floorPoints[i + 1] * this._config.map_scale) - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + let x = (floorPoints[i] * config.map_scale) - mapLeftOffset; + let y = (floorPoints[i + 1] * config.map_scale) - mapTopOffset; + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { continue; } - mapCtx.fillRect(x, y, this._config.map_scale, this._config.map_scale); + mapCtx.fillRect(x, y, config.map_scale, config.map_scale); } } @@ -341,24 +342,24 @@ class ValetudoMapCard extends HTMLElement { } let segmentAreas = this.getSegments(attributes); - if (segmentAreas && this._config.show_segments) { + if (segmentAreas && config.show_segments) { const colorFinder = new FourColorTheoremSolver(segmentAreas, 6); - mapCtx.globalAlpha = this._config.segment_opacity; + mapCtx.globalAlpha = config.segment_opacity; for (let item of segmentAreas) { - mapCtx.strokeStyle = this._config.segment_colors[colorFinder.getColor(item.metaData.segmentId)]; + mapCtx.strokeStyle = config.segment_colors[colorFinder.getColor(item.metaData.segmentId)]; mapCtx.lineWidth = 1; - mapCtx.fillStyle = this._config.segment_colors[colorFinder.getColor(item.metaData.segmentId)]; + mapCtx.fillStyle = config.segment_colors[colorFinder.getColor(item.metaData.segmentId)]; mapCtx.beginPath(); let segmentPoints = item["pixels"]; if (segmentPoints) { for (let i = 0; i < segmentPoints.length; i+=2) { - let x = (segmentPoints[i] * this._config.map_scale) - mapLeftOffset; - let y = (segmentPoints[i + 1] * this._config.map_scale) - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + let x = (segmentPoints[i] * config.map_scale) - mapLeftOffset; + let y = (segmentPoints[i + 1] * config.map_scale) - mapTopOffset; + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { continue; } - mapCtx.fillRect(x, y, this._config.map_scale, this._config.map_scale); + mapCtx.fillRect(x, y, config.map_scale, config.map_scale); } } } @@ -366,8 +367,8 @@ class ValetudoMapCard extends HTMLElement { mapCtx.globalAlpha = 1; } - if (this._config.show_walls) { - mapCtx.globalAlpha = this._config.wall_opacity; + if (config.show_walls) { + mapCtx.globalAlpha = config.wall_opacity; mapCtx.strokeStyle = wallColor; mapCtx.lineWidth = 1; @@ -376,12 +377,12 @@ class ValetudoMapCard extends HTMLElement { let wallPoints = this.getWallPoints(attributes); if (wallPoints) { for (let i = 0; i < wallPoints.length; i+=2) { - let x = (wallPoints[i] * this._config.map_scale) - mapLeftOffset; - let y = (wallPoints[i + 1] * this._config.map_scale) - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + let x = (wallPoints[i] * config.map_scale) - mapLeftOffset; + let y = (wallPoints[i + 1] * config.map_scale) - mapTopOffset; + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { continue; } - mapCtx.fillRect(x, y, this._config.map_scale, this._config.map_scale); + mapCtx.fillRect(x, y, config.map_scale, config.map_scale); } } @@ -389,14 +390,14 @@ class ValetudoMapCard extends HTMLElement { } let activeZones = this.getActiveZones(attributes); - if (Array.isArray(activeZones) && activeZones.length > 0 && this._config.show_currently_cleaned_zones) { - mapCtx.globalAlpha = this._config.currently_cleaned_zone_opacity; + if (Array.isArray(activeZones) && activeZones.length > 0 && config.show_currently_cleaned_zones) { + mapCtx.globalAlpha = config.currently_cleaned_zone_opacity; mapCtx.strokeStyle = currentlyCleanedZoneColor; mapCtx.lineWidth = 2; mapCtx.fillStyle = currentlyCleanedZoneColor; for (let item of activeZones) { - mapCtx.globalAlpha = this._config.currently_cleaned_zone_opacity; + mapCtx.globalAlpha = config.currently_cleaned_zone_opacity; mapCtx.beginPath(); let points = item["points"]; for (let i = 0; i < points.length; i+=2) { @@ -407,14 +408,14 @@ class ValetudoMapCard extends HTMLElement { } else { mapCtx.lineTo(x, y); } - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { // noinspection UnnecessaryContinueJS continue; } } mapCtx.fill(); - if (this._config.show_currently_cleaned_zones_border) { + if (config.show_currently_cleaned_zones_border) { mapCtx.closePath(); mapCtx.globalAlpha = 1.0; mapCtx.stroke(); @@ -424,12 +425,12 @@ class ValetudoMapCard extends HTMLElement { } let noGoAreas = this.getNoGoAreas(attributes); - if (noGoAreas && this._config.show_no_go_areas) { + if (noGoAreas && config.show_no_go_areas) { mapCtx.strokeStyle = noGoAreaColor; mapCtx.lineWidth = 2; mapCtx.fillStyle = noGoAreaColor; for (let item of noGoAreas) { - mapCtx.globalAlpha = this._config.no_go_area_opacity; + mapCtx.globalAlpha = config.no_go_area_opacity; mapCtx.beginPath(); let points = item["points"]; for (let i = 0; i < points.length; i+=2) { @@ -440,14 +441,14 @@ class ValetudoMapCard extends HTMLElement { } else { mapCtx.lineTo(x, y); } - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { // noinspection UnnecessaryContinueJS continue; } } mapCtx.fill(); - if (this._config.show_no_go_area_border) { + if (config.show_no_go_area_border) { mapCtx.closePath(); mapCtx.globalAlpha = 1.0; mapCtx.stroke(); @@ -457,12 +458,12 @@ class ValetudoMapCard extends HTMLElement { } let noMopAreas = this.getNoMopAreas(attributes); - if (noMopAreas && this._config.show_no_mop_areas) { + if (noMopAreas && config.show_no_mop_areas) { mapCtx.strokeStyle = noMopAreaColor; mapCtx.lineWidth = 2; mapCtx.fillStyle = noMopAreaColor; for (let item of noMopAreas) { - mapCtx.globalAlpha = this._config.no_mop_area_opacity; + mapCtx.globalAlpha = config.no_mop_area_opacity; mapCtx.beginPath(); let points = item["points"]; for (let i = 0; i < points.length; i+=2) { @@ -473,14 +474,14 @@ class ValetudoMapCard extends HTMLElement { } else { mapCtx.lineTo(x, y); } - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { // noinspection UnnecessaryContinueJS continue; } } mapCtx.fill(); - if (this._config.show_no_mop_area_border) { + if (config.show_no_mop_area_border) { mapCtx.closePath(); mapCtx.globalAlpha = 1.0; mapCtx.stroke(); @@ -490,21 +491,21 @@ class ValetudoMapCard extends HTMLElement { } let virtualWallPoints = this.getVirtualWallPoints(attributes); - if (virtualWallPoints && this._config.show_virtual_walls && this._config.virtual_wall_width > 0) { - mapCtx.globalAlpha = this._config.virtual_wall_opacity; + if (virtualWallPoints && config.show_virtual_walls && config.virtual_wall_width > 0) { + mapCtx.globalAlpha = config.virtual_wall_opacity; mapCtx.strokeStyle = virtualWallColor; - mapCtx.lineWidth = this._config.virtual_wall_width; + mapCtx.lineWidth = config.virtual_wall_width; mapCtx.beginPath(); for (let item of virtualWallPoints) { let fromX = Math.floor(item["points"][0] / widthScale) - objectLeftOffset - mapLeftOffset; let fromY = Math.floor(item["points"][1] / heightScale) - objectTopOffset - mapTopOffset; let toX = Math.floor(item["points"][2] / widthScale) - objectLeftOffset - mapLeftOffset; let toY = Math.floor(item["points"][3] / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(fromX, fromY, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(fromX, fromY, drawnMapCanvas, config)) { continue; } - if (this.isOutsideBounds(toX, toY, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(toX, toY, drawnMapCanvas, config)) { continue; } mapCtx.moveTo(fromX, fromY); @@ -516,12 +517,12 @@ class ValetudoMapCard extends HTMLElement { } const pathCtx = pathCanvas.getContext("2d"); - pathCtx.globalAlpha = this._config.path_opacity; + pathCtx.globalAlpha = config.path_opacity; pathCtx.strokeStyle = pathColor; - pathCtx.lineWidth = this._config.path_width; + pathCtx.lineWidth = config.path_width; let pathPoints = this.getPathPoints(attributes); - if (Array.isArray(pathPoints) && pathPoints.length > 0 && (this._config.show_path && this._config.path_width > 0)) { + if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { for (let item of pathPoints) { let x = 0; let y = 0; @@ -530,7 +531,7 @@ class ValetudoMapCard extends HTMLElement { for (let i = 0; i < item.points.length; i+=2) { x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { continue; } if (first) { @@ -544,13 +545,13 @@ class ValetudoMapCard extends HTMLElement { } // Update vacuum angle - vacuumHTML.style.transform = `scale(${this._config.icon_scale}, ${this._config.icon_scale}) rotate(${robotInfo[2]}deg)`; + vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(${robotInfo[2]}deg)`; pathCtx.globalAlpha = 1; } let predictedPathPoints = this.getPredictedPathPoints(attributes); - if (Array.isArray(predictedPathPoints) && predictedPathPoints.length > 0 && (this._config.show_predicted_path && this._config.path_width > 0)) { + if (Array.isArray(predictedPathPoints) && predictedPathPoints.length > 0 && (config.show_predicted_path && config.path_width > 0)) { pathCtx.setLineDash([5,3]); for (let item of predictedPathPoints) { let x = 0; @@ -560,7 +561,7 @@ class ValetudoMapCard extends HTMLElement { for (let i = 0; i < item.points.length; i+=2) { x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, this._config)) { + if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { continue; } if (first) { From 58dbacd461b75c51f138d769ae1e24261f47582f Mon Sep 17 00:00:00 2001 From: Marti Kaljuve Date: Wed, 27 Nov 2024 00:27:36 +0200 Subject: [PATCH 3/6] Move path drawing from drawMap() into separate drawPathCanvas() function. --- src/valetudo-map-card.js | 119 +++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 55 deletions(-) diff --git a/src/valetudo-map-card.js b/src/valetudo-map-card.js index 02ba022..82b21c2 100644 --- a/src/valetudo-map-card.js +++ b/src/valetudo-map-card.js @@ -217,6 +217,63 @@ class ValetudoMapCard extends HTMLElement { return this.getEntities(attributes, "no_mop_area"); } + drawPathCanvas(attributes, pathCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }) { + const config = this._config; + + let pathPoints = this.getPathPoints(attributes); + if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { + for (let item of pathPoints) { + let x = 0; + let y = 0; + let first = true; + pathCtx.beginPath(); + for (let i = 0; i < item.points.length; i+=2) { + x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; + y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; + if (this.isOutsideBounds(x, y, pathCtx.canvas, config)) { + continue; + } + if (first) { + pathCtx.moveTo(x, y); + first = false; + } else { + pathCtx.lineTo(x, y); + } + } + pathCtx.stroke(); + } + + pathCtx.globalAlpha = 1; + } + + let predictedPathPoints = this.getPredictedPathPoints(attributes); + if (Array.isArray(predictedPathPoints) && predictedPathPoints.length > 0 && (config.show_predicted_path && config.path_width > 0)) { + pathCtx.setLineDash([5,3]); + for (let item of predictedPathPoints) { + let x = 0; + let y = 0; + let first = true; + pathCtx.beginPath(); + for (let i = 0; i < item.points.length; i+=2) { + x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; + y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; + if (this.isOutsideBounds(x, y, pathCtx.canvas, config)) { + continue; + } + if (first) { + pathCtx.moveTo(x, y); + first = false; + } else { + pathCtx.lineTo(x, y); + } + } + pathCtx.stroke(); + } + + pathCtx.globalAlpha = 1; + } + } + drawMap(attributes, mapHeight, mapWidth, boundingBox) { const config = this._config; const pixelSize = attributes.pixelSize; @@ -293,6 +350,12 @@ class ValetudoMapCard extends HTMLElement { vacuumHTML.style.left = `${Math.floor(robotInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; vacuumHTML.style.top = `${Math.floor(robotInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; + + let pathPoints = this.getPathPoints(attributes); + if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { + // Update vacuum angle + vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(${robotInfo[2]}deg)`; + } } vacuumContainer.style.zIndex = 4; vacuumContainer.appendChild(vacuumHTML); @@ -521,61 +584,7 @@ class ValetudoMapCard extends HTMLElement { pathCtx.strokeStyle = pathColor; pathCtx.lineWidth = config.path_width; - let pathPoints = this.getPathPoints(attributes); - if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { - for (let item of pathPoints) { - let x = 0; - let y = 0; - let first = true; - pathCtx.beginPath(); - for (let i = 0; i < item.points.length; i+=2) { - x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; - y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { - continue; - } - if (first) { - pathCtx.moveTo(x, y); - first = false; - } else { - pathCtx.lineTo(x, y); - } - } - pathCtx.stroke(); - } - - // Update vacuum angle - vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(${robotInfo[2]}deg)`; - - pathCtx.globalAlpha = 1; - } - - let predictedPathPoints = this.getPredictedPathPoints(attributes); - if (Array.isArray(predictedPathPoints) && predictedPathPoints.length > 0 && (config.show_predicted_path && config.path_width > 0)) { - pathCtx.setLineDash([5,3]); - for (let item of predictedPathPoints) { - let x = 0; - let y = 0; - let first = true; - pathCtx.beginPath(); - for (let i = 0; i < item.points.length; i+=2) { - x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; - y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { - continue; - } - if (first) { - pathCtx.moveTo(x, y); - first = false; - } else { - pathCtx.lineTo(x, y); - } - } - pathCtx.stroke(); - } - - pathCtx.globalAlpha = 1; - } + this.drawPathCanvas(attributes, pathCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }); // Put our newly generated map in there this.clearContainer(this.mapContainer); From 46462f6ec53c13e7cf52fed420381552ac5aae91 Mon Sep 17 00:00:00 2001 From: Marti Kaljuve Date: Wed, 27 Nov 2024 00:32:02 +0200 Subject: [PATCH 4/6] Move map drawing from drawMap() into separate drawMapCanvas() function. --- src/valetudo-map-card.js | 337 ++++++++++++++++++++------------------- 1 file changed, 173 insertions(+), 164 deletions(-) diff --git a/src/valetudo-map-card.js b/src/valetudo-map-card.js index 82b21c2..af59e0c 100644 --- a/src/valetudo-map-card.js +++ b/src/valetudo-map-card.js @@ -217,78 +217,9 @@ class ValetudoMapCard extends HTMLElement { return this.getEntities(attributes, "no_mop_area"); } - drawPathCanvas(attributes, pathCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }) { + drawMapCanvas(attributes, mapCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }) { const config = this._config; - let pathPoints = this.getPathPoints(attributes); - if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { - for (let item of pathPoints) { - let x = 0; - let y = 0; - let first = true; - pathCtx.beginPath(); - for (let i = 0; i < item.points.length; i+=2) { - x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; - y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(x, y, pathCtx.canvas, config)) { - continue; - } - if (first) { - pathCtx.moveTo(x, y); - first = false; - } else { - pathCtx.lineTo(x, y); - } - } - pathCtx.stroke(); - } - - pathCtx.globalAlpha = 1; - } - - let predictedPathPoints = this.getPredictedPathPoints(attributes); - if (Array.isArray(predictedPathPoints) && predictedPathPoints.length > 0 && (config.show_predicted_path && config.path_width > 0)) { - pathCtx.setLineDash([5,3]); - for (let item of predictedPathPoints) { - let x = 0; - let y = 0; - let first = true; - pathCtx.beginPath(); - for (let i = 0; i < item.points.length; i+=2) { - x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; - y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(x, y, pathCtx.canvas, config)) { - continue; - } - if (first) { - pathCtx.moveTo(x, y); - first = false; - } else { - pathCtx.lineTo(x, y); - } - } - pathCtx.stroke(); - } - - pathCtx.globalAlpha = 1; - } - } - - drawMap(attributes, mapHeight, mapWidth, boundingBox) { - const config = this._config; - const pixelSize = attributes.pixelSize; - - const widthScale = pixelSize / config.map_scale; - const heightScale = pixelSize / config.map_scale; - - let objectLeftOffset = 0; - let objectTopOffset = 0; - let mapLeftOffset = 0; - let mapTopOffset = 0; - - mapLeftOffset = ((boundingBox.minX) - 1) * config.map_scale; - mapTopOffset = ((boundingBox.minY) - 1) * config.map_scale; - // Calculate colours const homeAssistant = document.getElementsByTagName("home-assistant")[0]; const floorColor = this.calculateColor(homeAssistant, config.floor_color, "--valetudo-map-floor-color", "--secondary-background-color"); @@ -297,91 +228,7 @@ class ValetudoMapCard extends HTMLElement { const noGoAreaColor = this.calculateColor(homeAssistant, config.no_go_area_color, "--valetudo-no-go-area-color", "--accent-color"); const noMopAreaColor = this.calculateColor(homeAssistant, config.no_mop_area_color, "--valetudo-no-mop-area-color", "--secondary-text-color"); const virtualWallColor = this.calculateColor(homeAssistant, config.virtual_wall_color, "--valetudo-virtual-wall-color", "--accent-color"); - const pathColor = this.calculateColor(homeAssistant, config.path_color, "--valetudo-map-path-color", "--primary-text-color"); - const chargerColor = this.calculateColor(homeAssistant, config.dock_color, "green"); - const vacuumColor = this.calculateColor(homeAssistant, config.vacuum_color, "--primary-text-color"); - const gotoTargetColor = this.calculateColor(homeAssistant, config.goto_target_color, "blue"); - // Create all objects - const containerContainer = document.createElement("div"); - containerContainer.id = "lovelaceValetudoCard"; - - const drawnMapContainer = document.createElement("div"); - const drawnMapCanvas = document.createElement("canvas"); - drawnMapCanvas.width = mapWidth * config.map_scale; - drawnMapCanvas.height = mapHeight * config.map_scale; - drawnMapContainer.style.zIndex = 1; - drawnMapContainer.appendChild(drawnMapCanvas); - - const chargerContainer = document.createElement("div"); - const chargerHTML = document.createElement("ha-icon"); - let chargerInfo = this.getChargerInfo(attributes); - if (config.show_dock && chargerInfo) { - chargerHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up - chargerHTML.icon = config.dock_icon || "mdi:flash"; - chargerHTML.style.left = `${Math.floor(chargerInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; - chargerHTML.style.top = `${Math.floor(chargerInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; - chargerHTML.style.color = chargerColor; - chargerHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(-${config.rotate})`; - } - chargerContainer.style.zIndex = 2; - chargerContainer.appendChild(chargerHTML); - - const pathContainer = document.createElement("div"); - const pathCanvas = document.createElement("canvas"); - pathCanvas.width = mapWidth * config.map_scale; - pathCanvas.height = mapHeight * config.map_scale; - pathContainer.style.zIndex = 3; - pathContainer.appendChild(pathCanvas); - - const vacuumContainer = document.createElement("div"); - const vacuumHTML = document.createElement("ha-icon"); - - let robotInfo = this.getRobotInfo(attributes); - if (!robotInfo) { - robotInfo = this.lastValidRobotInfo; - } - - if (config.show_vacuum && robotInfo) { - this.lastValidRobotInfo = robotInfo; - vacuumHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up - vacuumHTML.icon = config.vacuum_icon || "mdi:robot-vacuum"; - vacuumHTML.style.color = vacuumColor; - vacuumHTML.style.left = `${Math.floor(robotInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; - vacuumHTML.style.top = `${Math.floor(robotInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; - vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; - - let pathPoints = this.getPathPoints(attributes); - if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { - // Update vacuum angle - vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(${robotInfo[2]}deg)`; - } - } - vacuumContainer.style.zIndex = 4; - vacuumContainer.appendChild(vacuumHTML); - - const goToTargetContainer = document.createElement("div"); - const goToTargetHTML = document.createElement("ha-icon"); - let goToInfo = this.getGoToInfo(attributes); - if (config.show_goto_target && goToInfo) { - goToTargetHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up - goToTargetHTML.icon = config.goto_target_icon || "mdi:pin"; - goToTargetHTML.style.left = `${Math.floor(goToInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; - goToTargetHTML.style.top = `${Math.floor(goToInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (22 * config.icon_scale)}px`; - goToTargetHTML.style.color = gotoTargetColor; - goToTargetHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; - } - goToTargetContainer.style.zIndex = 5; - goToTargetContainer.appendChild(goToTargetHTML); - - // Put objects in container - containerContainer.appendChild(drawnMapContainer); - containerContainer.appendChild(chargerContainer); - containerContainer.appendChild(pathContainer); - containerContainer.appendChild(vacuumContainer); - containerContainer.appendChild(goToTargetContainer); - - const mapCtx = drawnMapCanvas.getContext("2d"); if (config.show_floor) { mapCtx.globalAlpha = config.floor_opacity; @@ -394,7 +241,7 @@ class ValetudoMapCard extends HTMLElement { for (let i = 0; i < floorPoints.length; i+=2) { let x = (floorPoints[i] * config.map_scale) - mapLeftOffset; let y = (floorPoints[i + 1] * config.map_scale) - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { + if (this.isOutsideBounds(x, y, mapCtx.canvas, config)) { continue; } mapCtx.fillRect(x, y, config.map_scale, config.map_scale); @@ -419,7 +266,7 @@ class ValetudoMapCard extends HTMLElement { for (let i = 0; i < segmentPoints.length; i+=2) { let x = (segmentPoints[i] * config.map_scale) - mapLeftOffset; let y = (segmentPoints[i + 1] * config.map_scale) - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { + if (this.isOutsideBounds(x, y, mapCtx.canvas, config)) { continue; } mapCtx.fillRect(x, y, config.map_scale, config.map_scale); @@ -442,7 +289,7 @@ class ValetudoMapCard extends HTMLElement { for (let i = 0; i < wallPoints.length; i+=2) { let x = (wallPoints[i] * config.map_scale) - mapLeftOffset; let y = (wallPoints[i + 1] * config.map_scale) - mapTopOffset; - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { + if (this.isOutsideBounds(x, y, mapCtx.canvas, config)) { continue; } mapCtx.fillRect(x, y, config.map_scale, config.map_scale); @@ -471,7 +318,7 @@ class ValetudoMapCard extends HTMLElement { } else { mapCtx.lineTo(x, y); } - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { + if (this.isOutsideBounds(x, y, mapCtx.canvas, config)) { // noinspection UnnecessaryContinueJS continue; } @@ -504,7 +351,7 @@ class ValetudoMapCard extends HTMLElement { } else { mapCtx.lineTo(x, y); } - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { + if (this.isOutsideBounds(x, y, mapCtx.canvas, config)) { // noinspection UnnecessaryContinueJS continue; } @@ -537,7 +384,7 @@ class ValetudoMapCard extends HTMLElement { } else { mapCtx.lineTo(x, y); } - if (this.isOutsideBounds(x, y, drawnMapCanvas, config)) { + if (this.isOutsideBounds(x, y, mapCtx.canvas, config)) { // noinspection UnnecessaryContinueJS continue; } @@ -565,10 +412,10 @@ class ValetudoMapCard extends HTMLElement { let fromY = Math.floor(item["points"][1] / heightScale) - objectTopOffset - mapTopOffset; let toX = Math.floor(item["points"][2] / widthScale) - objectLeftOffset - mapLeftOffset; let toY = Math.floor(item["points"][3] / heightScale) - objectTopOffset - mapTopOffset; - if (this.isOutsideBounds(fromX, fromY, drawnMapCanvas, config)) { + if (this.isOutsideBounds(fromX, fromY, mapCtx.canvas, config)) { continue; } - if (this.isOutsideBounds(toX, toY, drawnMapCanvas, config)) { + if (this.isOutsideBounds(toX, toY, mapCtx.canvas, config)) { continue; } mapCtx.moveTo(fromX, fromY); @@ -578,13 +425,175 @@ class ValetudoMapCard extends HTMLElement { mapCtx.globalAlpha = 1; } + } + + drawPathCanvas(attributes, pathCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }) { + const config = this._config; + + let pathPoints = this.getPathPoints(attributes); + if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { + for (let item of pathPoints) { + let x = 0; + let y = 0; + let first = true; + pathCtx.beginPath(); + for (let i = 0; i < item.points.length; i+=2) { + x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; + y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; + if (this.isOutsideBounds(x, y, pathCtx.canvas, config)) { + continue; + } + if (first) { + pathCtx.moveTo(x, y); + first = false; + } else { + pathCtx.lineTo(x, y); + } + } + pathCtx.stroke(); + } + + pathCtx.globalAlpha = 1; + } + let predictedPathPoints = this.getPredictedPathPoints(attributes); + if (Array.isArray(predictedPathPoints) && predictedPathPoints.length > 0 && (config.show_predicted_path && config.path_width > 0)) { + pathCtx.setLineDash([5,3]); + for (let item of predictedPathPoints) { + let x = 0; + let y = 0; + let first = true; + pathCtx.beginPath(); + for (let i = 0; i < item.points.length; i+=2) { + x = Math.floor((item.points[i]) / widthScale) - objectLeftOffset - mapLeftOffset; + y = Math.floor((item.points[i + 1]) / heightScale) - objectTopOffset - mapTopOffset; + if (this.isOutsideBounds(x, y, pathCtx.canvas, config)) { + continue; + } + if (first) { + pathCtx.moveTo(x, y); + first = false; + } else { + pathCtx.lineTo(x, y); + } + } + pathCtx.stroke(); + } + + pathCtx.globalAlpha = 1; + } + } + + drawMap(attributes, mapHeight, mapWidth, boundingBox) { + const config = this._config; + const pixelSize = attributes.pixelSize; + + const widthScale = pixelSize / config.map_scale; + const heightScale = pixelSize / config.map_scale; + + let objectLeftOffset = 0; + let objectTopOffset = 0; + let mapLeftOffset = 0; + let mapTopOffset = 0; + + mapLeftOffset = ((boundingBox.minX) - 1) * config.map_scale; + mapTopOffset = ((boundingBox.minY) - 1) * config.map_scale; + + // Calculate colours + const homeAssistant = document.getElementsByTagName("home-assistant")[0]; + const pathColor = this.calculateColor(homeAssistant, config.path_color, "--valetudo-map-path-color", "--primary-text-color"); + const chargerColor = this.calculateColor(homeAssistant, config.dock_color, "green"); + const vacuumColor = this.calculateColor(homeAssistant, config.vacuum_color, "--primary-text-color"); + const gotoTargetColor = this.calculateColor(homeAssistant, config.goto_target_color, "blue"); + + // Create all objects + const containerContainer = document.createElement("div"); + containerContainer.id = "lovelaceValetudoCard"; + + const drawnMapContainer = document.createElement("div"); + const drawnMapCanvas = document.createElement("canvas"); + drawnMapCanvas.width = mapWidth * config.map_scale; + drawnMapCanvas.height = mapHeight * config.map_scale; + drawnMapContainer.style.zIndex = 1; + drawnMapContainer.appendChild(drawnMapCanvas); + + const chargerContainer = document.createElement("div"); + const chargerHTML = document.createElement("ha-icon"); + let chargerInfo = this.getChargerInfo(attributes); + if (config.show_dock && chargerInfo) { + chargerHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up + chargerHTML.icon = config.dock_icon || "mdi:flash"; + chargerHTML.style.left = `${Math.floor(chargerInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; + chargerHTML.style.top = `${Math.floor(chargerInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; + chargerHTML.style.color = chargerColor; + chargerHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(-${config.rotate})`; + } + chargerContainer.style.zIndex = 2; + chargerContainer.appendChild(chargerHTML); + + const pathContainer = document.createElement("div"); + const pathCanvas = document.createElement("canvas"); + pathCanvas.width = mapWidth * config.map_scale; + pathCanvas.height = mapHeight * config.map_scale; + pathContainer.style.zIndex = 3; + pathContainer.appendChild(pathCanvas); + + const vacuumContainer = document.createElement("div"); + const vacuumHTML = document.createElement("ha-icon"); + + let robotInfo = this.getRobotInfo(attributes); + if (!robotInfo) { + robotInfo = this.lastValidRobotInfo; + } + + if (config.show_vacuum && robotInfo) { + this.lastValidRobotInfo = robotInfo; + vacuumHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up + vacuumHTML.icon = config.vacuum_icon || "mdi:robot-vacuum"; + vacuumHTML.style.color = vacuumColor; + vacuumHTML.style.left = `${Math.floor(robotInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; + vacuumHTML.style.top = `${Math.floor(robotInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (12 * config.icon_scale)}px`; + vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; + + let pathPoints = this.getPathPoints(attributes); + if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { + // Update vacuum angle + vacuumHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale}) rotate(${robotInfo[2]}deg)`; + } + } + vacuumContainer.style.zIndex = 4; + vacuumContainer.appendChild(vacuumHTML); + + const goToTargetContainer = document.createElement("div"); + const goToTargetHTML = document.createElement("ha-icon"); + let goToInfo = this.getGoToInfo(attributes); + if (config.show_goto_target && goToInfo) { + goToTargetHTML.style.position = "absolute"; // Needed in Home Assistant 0.110.0 and up + goToTargetHTML.icon = config.goto_target_icon || "mdi:pin"; + goToTargetHTML.style.left = `${Math.floor(goToInfo[0] / widthScale) - objectLeftOffset - mapLeftOffset - (12 * config.icon_scale)}px`; + goToTargetHTML.style.top = `${Math.floor(goToInfo[1] / heightScale) - objectTopOffset - mapTopOffset - (22 * config.icon_scale)}px`; + goToTargetHTML.style.color = gotoTargetColor; + goToTargetHTML.style.transform = `scale(${config.icon_scale}, ${config.icon_scale})`; + } + goToTargetContainer.style.zIndex = 5; + goToTargetContainer.appendChild(goToTargetHTML); + + // Put objects in container + containerContainer.appendChild(drawnMapContainer); + containerContainer.appendChild(chargerContainer); + containerContainer.appendChild(pathContainer); + containerContainer.appendChild(vacuumContainer); + containerContainer.appendChild(goToTargetContainer); + + const mapCtx = drawnMapCanvas.getContext("2d"); const pathCtx = pathCanvas.getContext("2d"); pathCtx.globalAlpha = config.path_opacity; pathCtx.strokeStyle = pathColor; pathCtx.lineWidth = config.path_width; - this.drawPathCanvas(attributes, pathCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }); + const dimensions = { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }; + this.drawMapCanvas(attributes, mapCtx, dimensions); + this.drawPathCanvas(attributes, pathCtx, dimensions); // Put our newly generated map in there this.clearContainer(this.mapContainer); @@ -1059,7 +1068,7 @@ class ValetudoMapCard extends HTMLElement { } } -let componentName = "valetudo-map-card"; +let componentName = "valetudo-map-card2"; if (!customElements.get(componentName)) { customElements.define(componentName, ValetudoMapCard); From 4ee9b3aabc4444d9af44b309f4559743cf4afe93 Mon Sep 17 00:00:00 2001 From: Marti Kaljuve Date: Wed, 27 Nov 2024 00:39:48 +0200 Subject: [PATCH 5/6] Move more path related code to drawPathCanvas(). --- src/valetudo-map-card.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/valetudo-map-card.js b/src/valetudo-map-card.js index af59e0c..a9fb6c3 100644 --- a/src/valetudo-map-card.js +++ b/src/valetudo-map-card.js @@ -430,6 +430,13 @@ class ValetudoMapCard extends HTMLElement { drawPathCanvas(attributes, pathCtx, { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }) { const config = this._config; + const homeAssistant = document.getElementsByTagName("home-assistant")[0]; + const pathColor = this.calculateColor(homeAssistant, config.path_color, "--valetudo-map-path-color", "--primary-text-color"); + + pathCtx.globalAlpha = config.path_opacity; + pathCtx.strokeStyle = pathColor; + pathCtx.lineWidth = config.path_width; + let pathPoints = this.getPathPoints(attributes); if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { for (let item of pathPoints) { @@ -501,7 +508,6 @@ class ValetudoMapCard extends HTMLElement { // Calculate colours const homeAssistant = document.getElementsByTagName("home-assistant")[0]; - const pathColor = this.calculateColor(homeAssistant, config.path_color, "--valetudo-map-path-color", "--primary-text-color"); const chargerColor = this.calculateColor(homeAssistant, config.dock_color, "green"); const vacuumColor = this.calculateColor(homeAssistant, config.vacuum_color, "--primary-text-color"); const gotoTargetColor = this.calculateColor(homeAssistant, config.goto_target_color, "blue"); @@ -587,9 +593,6 @@ class ValetudoMapCard extends HTMLElement { const mapCtx = drawnMapCanvas.getContext("2d"); const pathCtx = pathCanvas.getContext("2d"); - pathCtx.globalAlpha = config.path_opacity; - pathCtx.strokeStyle = pathColor; - pathCtx.lineWidth = config.path_width; const dimensions = { widthScale, heightScale, mapLeftOffset, mapTopOffset, objectLeftOffset, objectTopOffset }; this.drawMapCanvas(attributes, mapCtx, dimensions); From a0aeaded081931f42b1844286d2e2f66813ecbc7 Mon Sep 17 00:00:00 2001 From: Marti Kaljuve Date: Wed, 27 Nov 2024 00:57:29 +0200 Subject: [PATCH 6/6] Added initial support for a responsive map. Using ResizeObserver to redraw the map+path canvases when the container size changes. Work in progress. --- README.md | 1 + src/valetudo-map-card.js | 69 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 24b3439..8fda25f 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ Then use map_scale and crop to make it fit. | vacuum_color | string | '--primary-text-color' | The color to use for the vacuum icon | map_scale | number | 1 | Scale the map by this value | icon_scale | number | 1 | Scale the icons (vacuum & dock) by this value +| full_width | boolean | false | Scale the map to fit the whole card | rotate | number | 0 | Value to rotate the map by (default is in deg, but a value like `2rad` is valid too) | left_padding | number | 0 | Value that moves the map `number` pixels from left to right | crop | Object | {top: 0, bottom: 0, left: 0, right: 0} | Crop the map diff --git a/src/valetudo-map-card.js b/src/valetudo-map-card.js index a9fb6c3..a0c88bc 100644 --- a/src/valetudo-map-card.js +++ b/src/valetudo-map-card.js @@ -601,6 +601,73 @@ class ValetudoMapCard extends HTMLElement { // Put our newly generated map in there this.clearContainer(this.mapContainer); this.mapContainer.appendChild(containerContainer); + + if (config.full_width) { + containerContainer.style.width = 'auto'; + containerContainer.style.height = 'auto'; + containerContainer.style.aspectRatio = `${mapWidth} / ${mapHeight}`; + + drawnMapCanvas.style.width = "100%"; + pathCanvas.style.width = "100%"; + + const resizeObserver = new ResizeObserver((entries) => { + const entry = entries.length && entries[0]; + if (!entry) { + return; + } + const { width } = entry.contentRect; + + const ratio = width / mapWidth; + const iconScale = (ratio / 2) * config.icon_scale; + + if (config.show_dock && chargerInfo) { + const dockLeft = Math.floor(chargerInfo[0] / pixelSize) - dimensions.mapLeftOffset; + const dockTop = Math.floor(chargerInfo[1] / pixelSize) - dimensions.mapTopOffset; + chargerHTML.style.left = `${(dockLeft / mapWidth) * 100}%`; + chargerHTML.style.top = `${(dockTop / mapHeight) * 100}%`; + chargerHTML.style.transform = `translate(-12px, -12px) scale(${iconScale}, ${iconScale}) rotate(-${config.rotate})`; + } + + if (config.show_vacuum && robotInfo) { + const robotLeft = Math.floor(robotInfo[0] / pixelSize) - dimensions.mapLeftOffset; + const robotTop = Math.floor(robotInfo[1] / pixelSize) - dimensions.mapTopOffset; + vacuumHTML.style.left = `${(robotLeft / mapWidth) * 100}%`; + vacuumHTML.style.top = `${(robotTop / mapHeight) * 100}%`; + + vacuumHTML.style.transform = `translate(-12px, -12px) scale(${iconScale}, ${iconScale})`; + + const pathPoints = this.getPathPoints(attributes); + if (Array.isArray(pathPoints) && pathPoints.length > 0 && (config.show_path && config.path_width > 0)) { + // Update vacuum angle + vacuumHTML.style.transform += ` rotate(${robotInfo[2]}deg)`; + } + } + + if (config.show_goto_target && goToInfo) { + const targetLeft = Math.floor(goToInfo[0] / pixelSize) - dimensions.mapLeftOffset; + const targetTop = Math.floor(goToInfo[1] / pixelSize) - dimensions.mapTopOffset; + goToTargetHTML.style.left = `${(targetLeft / mapWidth) * 100}%`; + goToTargetHTML.style.top = `${(targetTop / mapWidth) * 100}%`; + goToTargetHTML.style.transform = `translate(-12px, -22px) scale(${iconScale}, ${iconScale})`; + } + + // Natural number to avoid antialiasing in the canvas + const scale = Math.max(Math.ceil(width / mapWidth), 1); + + drawnMapCanvas.width = scale * mapWidth; + drawnMapCanvas.height = scale * mapHeight; + pathCanvas.width = scale * mapWidth; + pathCanvas.height = scale * mapHeight; + + mapCtx.scale(scale, scale); + pathCtx.scale(scale, scale); + + this.drawMapCanvas(attributes, mapCtx, dimensions); + this.drawPathCanvas(attributes, pathCtx, dimensions); + }); + + resizeObserver.observe(drawnMapContainer); + } } clearContainer(container) { @@ -1071,7 +1138,7 @@ class ValetudoMapCard extends HTMLElement { } } -let componentName = "valetudo-map-card2"; +let componentName = "valetudo-map-card"; if (!customElements.get(componentName)) { customElements.define(componentName, ValetudoMapCard);