diff --git a/src/model/generators/platform_templates/DeebotUniverse_Deebot-4-Home-Assistant.json b/src/model/generators/platform_templates/DeebotUniverse_Deebot-4-Home-Assistant.json index 69f965ce..2d06197e 100644 --- a/src/model/generators/platform_templates/DeebotUniverse_Deebot-4-Home-Assistant.json +++ b/src/model/generators/platform_templates/DeebotUniverse_Deebot-4-Home-Assistant.json @@ -149,29 +149,32 @@ "y": 0 }, "map": { - "x": 400, - "y": 400 + "x": 0, + "y": 0 } }, { "vacuum": { - "x": 6400, + "x": 50, "y": 0 }, "map": { - "x": 528, - "y": 400 + "x": 1, + "y": 0 } }, { "vacuum": { "x": 0, - "y": 6400 + "y": -50 }, "map": { - "x": 400, - "y": 528 + "x": 0, + "y": 1 } } - ] + ], + "internal_variables": { + "svgmap": "true" + } } diff --git a/src/model/map_objects/coordinates-converter.ts b/src/model/map_objects/coordinates-converter.ts index 132529d1..e77fde0e 100644 --- a/src/model/map_objects/coordinates-converter.ts +++ b/src/model/map_objects/coordinates-converter.ts @@ -1,7 +1,7 @@ -import { applyToPoint, fromTriangles, Matrix } from "transformation-matrix"; +import { applyToPoint, compose, fromTriangles, inverse, Matrix, translate } from "transformation-matrix"; import { default as transformer, QuadPoints } from "change-perspective"; -import { CalibrationPoint, PointType } from "../../types/types"; +import { CalibrationPoint, Point, PointType } from "../../types/types"; enum TransformMode { AFFINE, @@ -16,14 +16,17 @@ export class CoordinatesConverter { private readonly mapToVacuumTransformer: ((x: number, y: number) => [number, number]) | undefined; private readonly vacuumToMapTransformer: ((x: number, y: number) => [number, number]) | undefined; - constructor(calibrationPoints: CalibrationPoint[] | undefined) { + constructor(calibrationPoints: CalibrationPoint[] | undefined, offset?: Point) { const mapPoints = calibrationPoints?.map(cp => cp.map); const vacuumPoints = calibrationPoints?.map(cp => cp.vacuum); if (mapPoints && vacuumPoints) { if (mapPoints.length === 3) { this.transformMode = TransformMode.AFFINE; - this.mapToVacuumMatrix = fromTriangles(mapPoints, vacuumPoints); this.vacuumToMapMatrix = fromTriangles(vacuumPoints, mapPoints); + if (offset) { + this.vacuumToMapMatrix = compose(translate(offset.x, offset.y), this.vacuumToMapMatrix) + } + this.mapToVacuumMatrix = inverse(this.vacuumToMapMatrix) this.calibrated = !!(this.mapToVacuumMatrix && this.vacuumToMapMatrix); } else { this.transformMode = TransformMode.PERSPECTIVE; diff --git a/src/xiaomi-vacuum-map-card.ts b/src/xiaomi-vacuum-map-card.ts index 2b4209e7..d1005187 100644 --- a/src/xiaomi-vacuum-map-card.ts +++ b/src/xiaomi-vacuum-map-card.ts @@ -8,6 +8,7 @@ import type { ActionableObjectConfig, IconActionConfig, LovelaceDomEvent, + Point, PredefinedPointConfig, ReplacedKey, RoomConfig, @@ -121,6 +122,7 @@ export class XiaomiVacuumMapCard extends LitElement { @state() private configErrors: string[] = []; @state() private connected = false; @state() public internalVariables = {}; + @state() private mapViewBox?: DOMRect; @query(".modes-dropdown-menu") private _modesDropdownMenu?: HTMLElement; @queryAll(".icon-dropdown-menu") private _iconDropdownMenus?: NodeListOf; private currentPreset!: CardPresetConfig; @@ -283,12 +285,17 @@ export class XiaomiVacuumMapCard extends LitElement { margin-bottom: ${(preset.map_source.crop?.bottom ?? 0) * -1}px; margin-left: ${(preset.map_source.crop?.left ?? 0) * -1}px; margin-right: ${(preset.map_source.crop?.right ?? 0) * -1}px;"> - html`` + )} + camera_image + @load="${() => this._updateImageSizeAndScale()}" + />
(zoomerContent.style.transitionDuration = "0s")); } - private _calculateBasicScale(): void { + private _isSameViewbox(viewBox: DOMRect): boolean { + if (this.mapViewBox === undefined) return false; + for (const f in ["x", "y", "width", "height"]) { + if (viewBox[f] !== this.mapViewBox[f]) return false + } + return true; + } + + private _isSvgMapImage(): boolean { + return !!this.internalVariables['svgmap']; + } + + private _updateSvgImageSize(): void { + const mapObject = this._getMapObject(); + + if (mapObject.contentDocument?.documentElement.nodeName !== 'svg') { + this.mapViewBox = undefined; + return; + } + + const viewBoxRect = (mapObject.contentDocument.documentElement as unknown as SVGSVGElement).viewBox.baseVal; + if (this._isSameViewbox(viewBoxRect)) { + return; + } + this.mapViewBox = viewBoxRect; + + this.realImageHeight = viewBoxRect.height; + this.realImageWidth = viewBoxRect.width; + } + + private _updateSimpleImageSize(): void { const mapImage = this._getMapImage(); + if (mapImage && mapImage.naturalWidth > 0) { this.realImageWidth = mapImage.naturalWidth; this.realImageHeight = mapImage.naturalHeight; - this.realScale = mapImage.width / mapImage.naturalWidth; + } + } + + private _getOffset(): Point | undefined { + if (this.mapViewBox === undefined || (this.mapViewBox.x === 0 && this.mapViewBox.y === 0)) { + return undefined; + } + return {x: -this.mapViewBox.x, y: -this.mapViewBox.y} + } + + private _updateImageSizeAndScale(): void { + if (this._isSvgMapImage()) { + this._updateSvgImageSize(); + } else { + this._updateSimpleImageSize(); + } + + this._calculateBasicScale(); + } + + private _calculateBasicScale(): void { + const mapImage = this._getMapImage(); + + if (mapImage && mapImage.offsetWidth && this.realImageWidth) { + this.realScale = mapImage.offsetWidth / this.realImageWidth; + } else { + this.realScale = 1; } } @@ -1227,6 +1291,10 @@ export class XiaomiVacuumMapCard extends LitElement { return this.shadowRoot?.getElementById("map-zoomer") as PinchZoom; } + private _getMapObject(): HTMLObjectElement { + return this.shadowRoot?.getElementById("map-object") as HTMLObjectElement; + } + private _getMapImage(): HTMLImageElement { return this.shadowRoot?.getElementById("map-image") as HTMLImageElement; }