diff --git a/frontend/src/map/EditMap.tsx b/frontend/src/map/EditMap.tsx index a2c0bb0915..6d5267e0c9 100644 --- a/frontend/src/map/EditMap.tsx +++ b/frontend/src/map/EditMap.tsx @@ -68,8 +68,8 @@ class EditMap extends Map { this.drawableComponentsMutex.take(async () => { this.drawableComponents = []; - await this.mapLayerRenderer.draw(this.props.rawMap, this.props.theme); - this.drawableComponents.push(this.mapLayerRenderer.getCanvas()); + await this.mapLayerManager.draw(this.props.rawMap, this.props.theme); + this.drawableComponents.push(this.mapLayerManager.getCanvas()); this.updateStructures(this.props.mode); @@ -228,7 +228,7 @@ class EditMap extends Map { this.updateState(); - this.draw(); + this.redrawLayers(); } protected onMapUpdate() : void { @@ -244,7 +244,43 @@ class EditMap extends Map { protected onTap(evt: any): boolean | void { // Only allow map interaction while the robot is docked if (this.props.robotStatus.value === "docked") { - return super.onTap(evt); + if (super.onTap(evt)) { + return true; + } + + if ( + this.props.mode === "segments" && + this.state.cuttingLine === undefined + ) { + const {x, y} = this.relativeCoordinatesToCanvas(evt.x0, evt.y0); + const tappedPointInMapSpace = this.ctxWrapper.mapPointToCurrentTransform(x, y); + + const intersectingSegmentId = this.mapLayerManager.getIntersectingSegment(tappedPointInMapSpace.x, tappedPointInMapSpace.y); + + if (intersectingSegmentId) { + const segmentLabels = this.structureManager.getMapStructures().filter(s => { + return s.type === SegmentLabelMapStructure.TYPE; + }) as Array; + + const matchedSegmentLabel = segmentLabels.find(l => { + return l.id === intersectingSegmentId; + }); + + if ( + this.state.selectedSegmentIds.length < 2 || + this.state.selectedSegmentIds.includes(intersectingSegmentId) + ) { + if (matchedSegmentLabel) { + matchedSegmentLabel.onTap(); + + this.updateState(); + this.redrawLayers(); + + return true; + } + } + } + } } } diff --git a/frontend/src/map/LiveMap.tsx b/frontend/src/map/LiveMap.tsx index 84cb9d9760..905ab05766 100644 --- a/frontend/src/map/LiveMap.tsx +++ b/frontend/src/map/LiveMap.tsx @@ -51,25 +51,59 @@ class LiveMap extends Map { protected onTap(evt: TapTouchHandlerEvent): boolean | void { if (super.onTap(evt)) { - return; + return true; } const {x, y} = this.relativeCoordinatesToCanvas(evt.x0, evt.y0); const tappedPointInMapSpace = this.ctxWrapper.mapPointToCurrentTransform(x, y); - if (this.props.supportedCapabilities[Capability.GoToLocation]) { - this.structureManager.getClientStructures().forEach(s => { - if (s.type === GoToTargetClientStructure.TYPE) { - this.structureManager.removeClientStructure(s); - } - }); - - if (this.structureManager.getClientStructures().length === 0 && this.state.selectedSegmentIds.length === 0) { + if ( + this.structureManager.getClientStructures().filter(s => { + return s.type !== GoToTargetClientStructure.TYPE; + }).length === 0 && + this.state.selectedSegmentIds.length === 0 && + evt.duration >= TapTouchHandlerEvent.LONG_PRESS_DURATION + ) { + this.structureManager.getClientStructures().forEach(s => { + if (s.type === GoToTargetClientStructure.TYPE) { + this.structureManager.removeClientStructure(s); + } + }); this.structureManager.addClientStructure(new GoToTargetClientStructure(tappedPointInMapSpace.x, tappedPointInMapSpace.y)); + this.updateState(); this.draw(); + + return true; + } + } + + if ( + this.state.zones.length === 0 && + this.state.goToTarget === undefined + ) { + const intersectingSegmentId = this.mapLayerManager.getIntersectingSegment(tappedPointInMapSpace.x, tappedPointInMapSpace.y); + + if (intersectingSegmentId) { + const segmentLabels = this.structureManager.getMapStructures().filter(s => { + return s.type === SegmentLabelMapStructure.TYPE; + }) as Array; + + const matchedSegmentLabel = segmentLabels.find(l => { + return l.id === intersectingSegmentId; + }); + + + if (matchedSegmentLabel) { + matchedSegmentLabel.onTap(); + + this.updateState(); + this.redrawLayers(); + + return true; + } } } } @@ -113,7 +147,7 @@ class LiveMap extends Map { }); this.updateState(); - this.draw(); + this.redrawLayers(); }} /> } diff --git a/frontend/src/map/Map.tsx b/frontend/src/map/Map.tsx index b35470364b..d98e29208a 100644 --- a/frontend/src/map/Map.tsx +++ b/frontend/src/map/Map.tsx @@ -1,6 +1,6 @@ import React, {createRef} from "react"; import {RawMapData, RawMapEntityType} from "../api"; -import {MapLayerRenderer} from "./MapLayerRenderer"; +import {MapLayerManager} from "./MapLayerManager"; import {PathDrawer} from "./PathDrawer"; import {TouchHandler} from "./utils/touch_handling/TouchHandler"; import StructureManager from "./StructureManager"; @@ -52,7 +52,7 @@ const SCROLL_PARAMETERS = { class Map extends React.Component

{ protected readonly canvasRef: React.RefObject; protected structureManager: StructureManager; - protected mapLayerRenderer: MapLayerRenderer; + protected mapLayerManager: MapLayerManager; protected canvas!: HTMLCanvasElement; protected ctxWrapper!: Canvas2DContextTrackingWrapper; protected readonly resizeListener: () => void; @@ -82,7 +82,7 @@ class Map extends React.Component

{ this.structureManager = new StructureManager(); this.structureManager.setPixelSize(this.props.rawMap.pixelSize); - this.mapLayerRenderer = new MapLayerRenderer(); + this.mapLayerManager = new MapLayerManager(); this.state = { selectedSegmentIds: [] as Array @@ -214,6 +214,12 @@ class Map extends React.Component

{ }); } + protected redrawLayers() : void { + this.mapLayerManager.draw(this.props.rawMap, this.props.theme).then(() => { + this.draw(); + }); + } + render(): JSX.Element { return ( @@ -239,8 +245,8 @@ class Map extends React.Component

{ this.drawableComponentsMutex.take(async () => { this.drawableComponents = []; - await this.mapLayerRenderer.draw(this.props.rawMap, this.props.theme); - this.drawableComponents.push(this.mapLayerRenderer.getCanvas()); + await this.mapLayerManager.draw(this.props.rawMap, this.props.theme); + this.drawableComponents.push(this.mapLayerManager.getCanvas()); const pathsImage = await PathDrawer.drawPaths( { paths: this.props.rawMap.entities.filter(e => { @@ -299,6 +305,8 @@ class Map extends React.Component

{ }); } + this.mapLayerManager.setSelectedSegmentIds(updatedSelectedSegmentIds); + this.setState({ selectedSegmentIds: updatedSelectedSegmentIds, } as S & MapState); @@ -398,33 +406,6 @@ class Map extends React.Component

{ return true; } - const mapStructuresHandledTap = this.structureManager.getMapStructures().some(structure => { - const result = structure.tap(tappedPointInScreenSpace, currentTransform); - - if (result.requestDraw === true) { - drawRequested = true; - } - - if (result.stopPropagation) { - if (result.deleteMe === true) { - this.structureManager.removeMapStructure(structure); - } - - - this.updateState(); - - this.draw(); - - return true; - } else { - return false; - } - }); - - if (mapStructuresHandledTap) { - return true; - } - //only draw if any structure was changed let didUpdateStructures = false; this.structureManager.getClientStructures().forEach(s => { diff --git a/frontend/src/map/MapLayerManager.ts b/frontend/src/map/MapLayerManager.ts new file mode 100644 index 0000000000..5d5b6d9041 --- /dev/null +++ b/frontend/src/map/MapLayerManager.ts @@ -0,0 +1,260 @@ +import {RawMapData} from "../api"; +import {Theme} from "@mui/material"; +import {adjustColorBrightness} from "../utils"; +import {RGBColor, LayerColors, PROCESS_LAYERS} from "./MapLayerManagerUtils"; + +function hexToRgb(hex: string) : RGBColor { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim()); + + if (result === null) { + throw new Error(`Invalid color ${hex}`); + } + + return { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } ; +} + +export class MapLayerManager { + private readonly canvas: HTMLCanvasElement; + private readonly ctx: CanvasRenderingContext2D; + private width: number; + private height: number; + + private mapLayerManagerWorker: Worker; + private mapLayerManagerWorkerAvailable = false; + private pendingCallback: (() => void) | undefined; + private colors: { floor: string; wall: string; segments: string[] }; + private readonly darkColors: { floor: RGBColor; wall: RGBColor; segments: RGBColor[] }; + private readonly darkBackgroundColors: { floor: RGBColor; wall: RGBColor; segments: RGBColor[] }; + private readonly lightColors: { floor: RGBColor; wall: RGBColor; segments: RGBColor[] }; + private readonly lightBackgroundColors: { floor: RGBColor; wall: RGBColor; segments: RGBColor[] }; + + private segmentLookupInfo: { + data: Uint8ClampedArray, + width: number, + height: number, + top: number, + left: number, + idMapping: {[key: string]: string} + }; + private selectedSegmentIds: string[]; + + constructor() { + this.width = 1; + this.height = 1; + + this.canvas = document.createElement("canvas"); + this.canvas.width = this.width; + this.canvas.height = this.height; + + this.ctx = this.canvas.getContext("2d")!; + + this.colors = { + floor:"#0076ff", + wall: "#333333", + segments: [ + "#19A1A1", + "#7AC037", + "#DF5618", + "#F7C841", + "#9966CC" // "fallback" color + ] + }; + + this.darkColors = { + floor: hexToRgb(adjustColorBrightness(this.colors.floor, -20)), + wall: hexToRgb(this.colors.wall), + segments: this.colors.segments.map((e) => { + return hexToRgb(adjustColorBrightness(e, -20)); + }) + }; + this.darkBackgroundColors = { + floor: hexToRgb(adjustColorBrightness(this.colors.floor, -50)), + wall: hexToRgb(adjustColorBrightness(this.colors.wall, -20)), + segments: this.colors.segments.map((e) => { + return hexToRgb(adjustColorBrightness(e, -50)); + }) + }; + + this.lightColors = { + floor: hexToRgb(this.colors.floor), + wall: hexToRgb(this.colors.wall), + segments: this.colors.segments.map((e) => { + return hexToRgb(e); + }) + }; + this.lightBackgroundColors = { + floor: hexToRgb(adjustColorBrightness(this.colors.floor, -40)), + wall: hexToRgb(adjustColorBrightness(this.colors.wall, -15)), + segments: this.colors.segments.map((e) => { + return hexToRgb(adjustColorBrightness(e, -40)); + }) + }; + + this.mapLayerManagerWorker = new Worker(new URL("./MapLayerManager.worker", import.meta.url)); + + this.mapLayerManagerWorker.onerror = (ev => { + // eslint-disable-next-line no-console + console.warn("MapLayerManager.worker unavailable."); + + this.mapLayerManagerWorkerAvailable = false; + }); + + this.mapLayerManagerWorker.onmessage = (evt) => { + if (evt.data.pixelData !== undefined) { + const imageData = new ImageData( + new Uint8ClampedArray(evt.data.pixelData), + evt.data.width, + evt.data.height + ); + + this.segmentLookupInfo = { + data: new Uint8ClampedArray(evt.data.segmentLookupData), + width: evt.data.width, + height: evt.data.height, + top: evt.data.top, + left: evt.data.left, + idMapping: evt.data.segmentLookupIdMapping + }; + + this.ctx.putImageData(imageData, evt.data.left, evt.data.top); + + if (typeof this.pendingCallback === "function") { + this.pendingCallback(); + this.pendingCallback = undefined; + } + } else { + if (evt.data.ready === true) { + // eslint-disable-next-line no-console + console.info("MapLayerManager.worker available"); + + this.mapLayerManagerWorkerAvailable = true; + + return; + } + } + }; + + this.pendingCallback = undefined; + + this.segmentLookupInfo = { + data: new Uint8ClampedArray(), + width: 1, + height: 1, + top: 0, + left: 0, + idMapping: {} + }; + this.selectedSegmentIds = []; + } + + draw(data : RawMapData, theme: Theme): Promise { + let colors: LayerColors; + let backgroundColors: LayerColors; + + switch (theme.palette.mode) { + case "light": + colors = this.lightColors; + backgroundColors = this.lightBackgroundColors; + break; + case "dark": + colors = this.darkColors; + backgroundColors = this.darkBackgroundColors; + break; + } + + return new Promise((resolve, reject) => { + //As the map data might change dimensions, we need to keep track of that. + if ( + this.canvas.width !== Math.round(data.size.x / data.pixelSize) || + this.canvas.height !== Math.round(data.size.y / data.pixelSize) + ) { + this.width = Math.round(data.size.x / data.pixelSize); + this.height = Math.round(data.size.y / data.pixelSize); + + this.canvas.width = this.width; + this.canvas.height = this.height; + } + this.ctx.clearRect(0, 0, this.width, this.height); + + if (data.layers.length > 0) { + if (this.mapLayerManagerWorkerAvailable) { + this.mapLayerManagerWorker.postMessage( { + mapLayers: data.layers, + pixelSize: data.pixelSize, + colors: colors, + backgroundColors: backgroundColors, + selectedSegmentIds: this.selectedSegmentIds + }); + + //I'm not 100% sure if this cleanup is necessary, but it should prevent eternally stuck promises + if (typeof this.pendingCallback === "function") { + this.pendingCallback(); + this.pendingCallback = undefined; + } + + this.pendingCallback = () => { + resolve(); + }; + } else { //Fallback if there's no worker for some reason + const rendered = PROCESS_LAYERS( + data.layers, + data.pixelSize, + colors, + backgroundColors, + this.selectedSegmentIds + ); + + this.segmentLookupInfo = { + data: new Uint8ClampedArray(rendered.segmentLookupData), + width: rendered.width, + height: rendered.height, + top: rendered.top, + left: rendered.left, + idMapping: rendered.segmentLookupIdMapping + }; + + + this.ctx.putImageData( + new ImageData( + new Uint8ClampedArray(rendered.pixelData), + rendered.width, + rendered.height + ), + rendered.left, + rendered.top + ); + + resolve(); + } + } else { + resolve(); + } + }); + } + + /** + * + * @param {number} x - in cm coordinates + * @param {number} y - in cm coordinates + */ + getIntersectingSegment(x: number, y: number): string|null { + const offset = Math.round( + (Math.round(x) - this.segmentLookupInfo.left) + + ((Math.round(y) - this.segmentLookupInfo.top) * this.segmentLookupInfo.width) + ); + + return this.segmentLookupInfo.idMapping[this.segmentLookupInfo.data[offset]] ?? null; + } + + setSelectedSegmentIds(selectedSegmentIds: string[]) { + this.selectedSegmentIds = selectedSegmentIds; + } + + getCanvas(): HTMLCanvasElement { + return this.canvas; + } +} diff --git a/frontend/src/map/MapLayerRenderer.worker.ts b/frontend/src/map/MapLayerManager.worker.ts similarity index 58% rename from frontend/src/map/MapLayerRenderer.worker.ts rename to frontend/src/map/MapLayerManager.worker.ts index 1b2a10b702..9dd63b65f4 100644 --- a/frontend/src/map/MapLayerRenderer.worker.ts +++ b/frontend/src/map/MapLayerManager.worker.ts @@ -1,4 +1,4 @@ -import { RENDER_LAYERS_TO_IMAGEDATA } from "./MapLayerRenderUtils"; +import { PROCESS_LAYERS } from "./MapLayerManagerUtils"; self.postMessage({ ready: true @@ -14,19 +14,27 @@ self.addEventListener( "message", ( evt ) => { return; } - const rendered = RENDER_LAYERS_TO_IMAGEDATA( + const rendered = PROCESS_LAYERS( evt.data.mapLayers, evt.data.pixelSize, - evt.data.colorsToUse + evt.data.colors, + evt.data.backgroundColors, + evt.data.selectedSegmentIds ); self.postMessage( { - pixels: rendered.imageData.data.buffer, + pixelData: rendered.pixelData.buffer, width: rendered.width, height: rendered.height, left: rendered.left, top: rendered.top, + + segmentLookupData: rendered.segmentLookupData.buffer, + segmentLookupIdMapping: rendered.segmentLookupIdMapping }, { - transfer: [rendered.imageData.data.buffer] + transfer: [ + rendered.pixelData.buffer, + rendered.segmentLookupData.buffer + ] }); } ); diff --git a/frontend/src/map/MapLayerRenderUtils.ts b/frontend/src/map/MapLayerManagerUtils.ts similarity index 54% rename from frontend/src/map/MapLayerRenderUtils.ts rename to frontend/src/map/MapLayerManagerUtils.ts index 1c4736aba8..7604013bd3 100644 --- a/frontend/src/map/MapLayerRenderUtils.ts +++ b/frontend/src/map/MapLayerManagerUtils.ts @@ -13,19 +13,20 @@ export type LayerColors = { segments: RGBColor[]; }; -export function RENDER_LAYERS_TO_IMAGEDATA(layers: Array, pixelSize: number, colorsToUse: LayerColors) { +export function PROCESS_LAYERS(layers: Array, pixelSize: number, colors: LayerColors, backgroundColors: LayerColors, selectedSegmentIds: string[]) { const dimensions = CALCULATE_REQUIRED_DIMENSIONS(layers); const width = dimensions.x.sum; const height = dimensions.y.sum; - const imageData = new ImageData( - new Uint8ClampedArray( width * height * 4 ), - width, - height - ); + const pixelData = new Uint8ClampedArray( width * height * 4 ); + const segmentLookupData = new Uint8ClampedArray( width * height); + const segmentLookupIdMapping = new Map(); //Because segment IDs are arbitrary strings, we need this mapping to an int for the lookup data const colorFinder = new FourColorTheoremSolver(layers, pixelSize); + + const hasSelectedSegments = selectedSegmentIds.length === 0; + [...layers].sort((a,b) => { return TYPE_SORT_MAPPING[a.type] - TYPE_SORT_MAPPING[b.type]; }).forEach(layer => { @@ -33,14 +34,27 @@ export function RENDER_LAYERS_TO_IMAGEDATA(layers: Array, pixelSize switch (layer.type) { case "floor": - color = colorsToUse.floor; + if (hasSelectedSegments) { + color = colors.floor; + } else { + color = backgroundColors.floor; + } break; case "wall": - color = colorsToUse.wall; + if (hasSelectedSegments) { + color = colors.wall; + } else { + color = backgroundColors.wall; + } break; - case "segment": - color = colorsToUse.segments[colorFinder.getColor((layer.metaData.segmentId ?? ""))]; + case "segment": { + if (hasSelectedSegments || selectedSegmentIds.includes(layer.metaData.segmentId ?? "")) { + color = colors.segments[colorFinder.getColor((layer.metaData.segmentId ?? ""))]; + } else { + color = backgroundColors.segments[colorFinder.getColor((layer.metaData.segmentId ?? ""))]; + } break; + } } if (!color) { @@ -49,27 +63,37 @@ export function RENDER_LAYERS_TO_IMAGEDATA(layers: Array, pixelSize color = {r: 128, g: 128, b: 128}; } + let segmentLookupId = 0; + if (layer.metaData.segmentId) { + segmentLookupId = segmentLookupIdMapping.size + 1; + segmentLookupIdMapping.set(segmentLookupId, layer.metaData.segmentId); + } + for (let i = 0; i < layer.pixels.length; i = i + 2) { - const imgDataOffset = ( - ( - (layer.pixels[i] - dimensions.x.min) + - ((layer.pixels[i+1] - dimensions.y.min) * width) - ) * 4 + const offset = ( + (layer.pixels[i] - dimensions.x.min) + + ((layer.pixels[i+1] - dimensions.y.min) * width) ); + const imgDataOffset = offset * 4; + + pixelData[imgDataOffset] = color.r; + pixelData[imgDataOffset + 1] = color.g; + pixelData[imgDataOffset + 2] = color.b; + pixelData[imgDataOffset + 3] = 255; - imageData.data[imgDataOffset] = color.r; - imageData.data[imgDataOffset + 1] = color.g; - imageData.data[imgDataOffset + 2] = color.b; - imageData.data[imgDataOffset + 3] = 255; + segmentLookupData[offset] = segmentLookupId; } }); return { - imageData: imageData, + pixelData: pixelData, width: dimensions.x.sum, height: dimensions.y.sum, left: dimensions.x.min, top: dimensions.y.min, + + segmentLookupData: segmentLookupData, + segmentLookupIdMapping: Object.fromEntries(segmentLookupIdMapping) }; } diff --git a/frontend/src/map/MapLayerRenderer.ts b/frontend/src/map/MapLayerRenderer.ts deleted file mode 100644 index 5de7bbea03..0000000000 --- a/frontend/src/map/MapLayerRenderer.ts +++ /dev/null @@ -1,174 +0,0 @@ -import {RawMapData} from "../api"; -import {Theme} from "@mui/material"; -import {adjustColorBrightness} from "../utils"; -import {RGBColor, LayerColors, RENDER_LAYERS_TO_IMAGEDATA} from "./MapLayerRenderUtils"; - - - -function hexToRgb(hex: string) : RGBColor { - const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim()); - - if (result === null) { - throw new Error(`Invalid color ${hex}`); - } - - return { - r: parseInt(result[1], 16), - g: parseInt(result[2], 16), - b: parseInt(result[3], 16) - } ; -} - -export class MapLayerRenderer { - private readonly canvas: HTMLCanvasElement; - private readonly ctx: CanvasRenderingContext2D; - private width: number; - private height: number; - - private mapLayerRenderWebWorker: Worker; - private mapLayerRenderWebWorkerAvailable = false; - private pendingCallback: (() => void) | undefined; - private colors: { floor: string; wall: string; segments: string[] }; - private readonly darkColors: { floor: RGBColor; wall: RGBColor; segments: RGBColor[] }; - private readonly lightColors: { floor: RGBColor; wall: RGBColor; segments: RGBColor[] }; - - constructor() { - this.width = 1; - this.height = 1; - - this.canvas = document.createElement("canvas"); - this.canvas.width = this.width; - this.canvas.height = this.height; - - this.ctx = this.canvas.getContext("2d")!; - - this.colors = { - floor:"#0076ff", - wall: "#333333", - segments: [ - "#19A1A1", - "#7AC037", - "#DF5618", - "#F7C841", - "#9966CC" // "fallback" color - ] - }; - - this.darkColors = { - floor: hexToRgb(adjustColorBrightness(this.colors.floor, -20)), - wall: hexToRgb(this.colors.wall), - segments: this.colors.segments.map((e) => { - return hexToRgb(adjustColorBrightness(e, -20)); - }) - }; - - this.lightColors = { - floor: hexToRgb(this.colors.floor), - wall: hexToRgb(this.colors.wall), - segments: this.colors.segments.map((e) => { - return hexToRgb(e); - }) - }; - - this.mapLayerRenderWebWorker = new Worker(new URL("./MapLayerRenderer.worker", import.meta.url)); - - this.mapLayerRenderWebWorker.onerror = (ev => { - // eslint-disable-next-line no-console - console.warn("MapLayerRenderWebWorker unavailable."); - - this.mapLayerRenderWebWorkerAvailable = false; - }); - - this.mapLayerRenderWebWorker.onmessage = (evt) => { - if (evt.data.pixels !== undefined) { - const imageData = new ImageData( - new Uint8ClampedArray(evt.data.pixels), - evt.data.width, - evt.data.height - ); - - this.ctx.putImageData(imageData, evt.data.left, evt.data.top); - - if (typeof this.pendingCallback === "function") { - this.pendingCallback(); - this.pendingCallback = undefined; - } - } else { - if (evt.data.ready === true) { - // eslint-disable-next-line no-console - console.info("MapLayerRenderer.worker available"); - - this.mapLayerRenderWebWorkerAvailable = true; - - return; - } - } - }; - - this.pendingCallback = undefined; - } - - draw(data : RawMapData, theme: Theme): Promise { - let colorsToUse: LayerColors; - - switch (theme.palette.mode) { - case "light": - colorsToUse = this.lightColors; - break; - case "dark": - colorsToUse = this.darkColors; - break; - } - - return new Promise((resolve, reject) => { - //As the map data might change dimensions, we need to keep track of that. - if ( - this.canvas.width !== Math.round(data.size.x / data.pixelSize) || - this.canvas.height !== Math.round(data.size.y / data.pixelSize) - ) { - this.width = Math.round(data.size.x / data.pixelSize); - this.height = Math.round(data.size.y / data.pixelSize); - - this.canvas.width = this.width; - this.canvas.height = this.height; - } - this.ctx.clearRect(0, 0, this.width, this.height); - - if (data.layers.length > 0) { - if (this.mapLayerRenderWebWorkerAvailable) { - this.mapLayerRenderWebWorker.postMessage( { - mapLayers: data.layers, - pixelSize: data.pixelSize, - colorsToUse: colorsToUse - }); - - //I'm not 100% sure if this cleanup is necessary, but it should prevent eternally stuck promises - if (typeof this.pendingCallback === "function") { - this.pendingCallback(); - this.pendingCallback = undefined; - } - - this.pendingCallback = () => { - resolve(); - }; - } else { //Fallback if there's no worker for some reason - const rendered = RENDER_LAYERS_TO_IMAGEDATA( - data.layers, - data.pixelSize, - colorsToUse - ); - - this.ctx.putImageData(rendered.imageData, rendered.left, rendered.top); - resolve(); - } - } else { - resolve(); - } - }); - } - - getCanvas(): HTMLCanvasElement { - return this.canvas; - } -} - diff --git a/frontend/src/map/RobotCoverageMap.tsx b/frontend/src/map/RobotCoverageMap.tsx index 77c86daefc..a2fe13f7f9 100644 --- a/frontend/src/map/RobotCoverageMap.tsx +++ b/frontend/src/map/RobotCoverageMap.tsx @@ -48,8 +48,8 @@ class RobotCoverageMap extends Map { this.drawableComponents = []; - await this.mapLayerRenderer.draw(this.props.rawMap, this.props.theme); - this.drawableComponents.push(this.mapLayerRenderer.getCanvas()); + await this.mapLayerManager.draw(this.props.rawMap, this.props.theme); + this.drawableComponents.push(this.mapLayerManager.getCanvas()); const coveragePathImage = await PathDrawer.drawPaths( { paths: this.props.rawMap.entities.filter(e => { diff --git a/frontend/src/map/res/SegmentEditHelp.ts b/frontend/src/map/res/SegmentEditHelp.ts index 82d783c659..f190b0e1fd 100644 --- a/frontend/src/map/res/SegmentEditHelp.ts +++ b/frontend/src/map/res/SegmentEditHelp.ts @@ -10,8 +10,8 @@ There could for example be a segment, which is just the area around your dining On most firmwares, the robot uses the segment data to optimize its navigation and drive the most efficient path. -You can select a segment by clicking on the triangle. Depending on your firmware, you can then split it into two or -give it a name. If you select another segment, you can also join the two to form one bigger segment if the firmware allows that. +You can select a segment by clicking on it. Depending on your firmware, you can then split it into two or give it a name. +If you select another segment, you can also join the two to form one bigger segment if the firmware allows that. Segment colors are determined on-the-fly by the map renderer and don't mean anything. They're simply different so that you diff --git a/frontend/src/map/structures/map_structures/SegmentLabelMapStructure.ts b/frontend/src/map/structures/map_structures/SegmentLabelMapStructure.ts index eaa85791af..3aca0dbba7 100644 --- a/frontend/src/map/structures/map_structures/SegmentLabelMapStructure.ts +++ b/frontend/src/map/structures/map_structures/SegmentLabelMapStructure.ts @@ -1,10 +1,7 @@ import MapStructure from "./MapStructure"; import segmentIconSVG from "../icons/segment.svg"; import segmentSelectedIconSVG from "../icons/segment_selected.svg"; -import {StructureInterceptionHandlerResult} from "../Structure"; import {Canvas2DContextTrackingWrapper} from "../../utils/Canvas2DContextTrackingWrapper"; -import {PointCoordinates} from "../../utils/types"; -import {calculateBoxAroundPoint, isInsideBox} from "../../utils/helpers"; const img = new Image(); img.src = segmentIconSVG; @@ -12,7 +9,6 @@ img.src = segmentIconSVG; const img_selected = new Image(); img_selected.src = segmentSelectedIconSVG; -const hitboxPadding = 5; class SegmentLabelMapStructure extends MapStructure { public static TYPE = "SegmentLabelMapStructure"; @@ -126,22 +122,8 @@ class SegmentLabelMapStructure extends MapStructure { } } - tap(tappedPoint : PointCoordinates, transformationMatrixToScreenSpace: DOMMatrixInit) : StructureInterceptionHandlerResult { - const p0 = new DOMPoint(this.x0, this.y0).matrixTransform(transformationMatrixToScreenSpace); - - const iconHitbox = calculateBoxAroundPoint(p0, (this.scaledIconSize.width / 2) + hitboxPadding); - - if (isInsideBox(tappedPoint, iconHitbox)) { - this.selected = !this.selected; - - return { - stopPropagation: true - }; - } - - return { - stopPropagation: false - }; + onTap() { + this.selected = !this.selected; } getType(): string { diff --git a/frontend/src/map/utils/touch_handling/MapCanvasEvent.ts b/frontend/src/map/utils/touch_handling/MapCanvasEvent.ts index 5ad4943d98..aaa11cb664 100644 --- a/frontend/src/map/utils/touch_handling/MapCanvasEvent.ts +++ b/frontend/src/map/utils/touch_handling/MapCanvasEvent.ts @@ -4,11 +4,13 @@ export class MapCanvasEvent { x: number; y: number; pointerId: number; + timestamp: number; constructor(x: number, y : number, pointerId : number) { this.x = x; this.y = y; this.pointerId = pointerId; + this.timestamp = Date.now(); } static CREATE_EVENTS_FROM_MOUSE_EVENT(evt : MouseEvent) : Array { diff --git a/frontend/src/map/utils/touch_handling/events/TapTouchHandlerEvent.ts b/frontend/src/map/utils/touch_handling/events/TapTouchHandlerEvent.ts index f372fd954e..2aa3507af4 100644 --- a/frontend/src/map/utils/touch_handling/events/TapTouchHandlerEvent.ts +++ b/frontend/src/map/utils/touch_handling/events/TapTouchHandlerEvent.ts @@ -2,15 +2,17 @@ import {TouchHandlerEvent} from "./TouchHandlerEvent"; export class TapTouchHandlerEvent extends TouchHandlerEvent { static TYPE = "tap"; + static LONG_PRESS_DURATION = 150; x0: number; y0: number; + duration: number; //ms - constructor(x0: number, y0: number) { + constructor(x0: number, y0: number, duration: number) { super(TapTouchHandlerEvent.TYPE); this.x0 = x0; this.y0 = y0; + this.duration = duration; } - } diff --git a/frontend/src/map/utils/touch_handling/gestures/PossibleTapGesture.ts b/frontend/src/map/utils/touch_handling/gestures/PossibleTapGesture.ts index 633d75ce15..fb307d2e08 100644 --- a/frontend/src/map/utils/touch_handling/gestures/PossibleTapGesture.ts +++ b/frontend/src/map/utils/touch_handling/gestures/PossibleTapGesture.ts @@ -59,7 +59,11 @@ export class PossibleTapGesture extends Gesture { const event = evts[0]; if (event.pointerId === this.pointerId) { - return new TapTouchHandlerEvent(this.initialPosition.x, this.initialPosition.y); + return new TapTouchHandlerEvent( + this.initialPosition.x, + this.initialPosition.y, + event.timestamp - this.initialEvent.timestamp + ); } else { return; } diff --git a/frontend/src/valetudo/res/HelpText.ts b/frontend/src/valetudo/res/HelpText.ts index e0b73da2da..fc0e3765e7 100644 --- a/frontend/src/valetudo/res/HelpText.ts +++ b/frontend/src/valetudo/res/HelpText.ts @@ -5,12 +5,16 @@ Look out for question mark buttons for more context-specific help. ### What are those triangles on the map? -Those are segment labels. You can click on it to select the segment. +Those are segment labels. If a segment is considered active by the robot - meaning that it is part of the robots current task - the triangle will point down. Otherwise, it will point up. If you zoom in on a segment label, it will display the segment ID, its name and also its size in m². +### How do I set a go-to marker? + +If your robots' firmware supports it, you can set a go-to marker by long-pressing on the map. + ### How do I open the controls of the home page on mobile? Just tap on the bar at the bottom of the screen.