mirror of
https://github.com/Hypfer/Valetudo.git
synced 2025-10-26 11:27:27 +00:00
feat(ui): ObstacleImagesCapability
This commit is contained in:
parent
f9fa9d0b60
commit
30b5fc8a3d
75
assets/misc/confused_valetudog.svg
Normal file
75
assets/misc/confused_valetudog.svg
Normal file
@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
shape-rendering="crispEdges"
|
||||
version="1.1"
|
||||
viewBox="0 -0.5 50 48"
|
||||
id="svg3"
|
||||
sodipodi:docname="confused_valetudog.svg"
|
||||
width="50"
|
||||
height="48"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs3" />
|
||||
<sodipodi:namedview
|
||||
id="namedview3"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showguides="true"
|
||||
inkscape:zoom="8.7159622"
|
||||
inkscape:cx="14.857797"
|
||||
inkscape:cy="30.117157"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="1537"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3">
|
||||
<sodipodi:guide
|
||||
position="21,21.872356"
|
||||
orientation="-1,0"
|
||||
id="guide3"
|
||||
inkscape:locked="false"
|
||||
inkscape:label=""
|
||||
inkscape:color="rgb(0,134,229)" />
|
||||
</sodipodi:namedview>
|
||||
<g
|
||||
id="g1">
|
||||
<g
|
||||
id="g3"
|
||||
transform="translate(-25,-3)">
|
||||
<path
|
||||
d="m 31,20 h 32 m -32,1 h 1 m 30,0 h 1 m -32,1 h 1 m 30,0 h 2 m -37,1 h 5 m 31,0 h 2 m -39,1 h 2 m 36,0 h 2 m -40,1 h 1 m 38,0 h 2 m -41,1 h 1 m 39,0 h 2 m -42,1 h 2 m 39,0 h 4 m -44,1 h 2 m 41,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m 1,0 h 3 m -48,1 h 1 m 42,0 h 3 m 1,0 h 1 m -48,1 h 1 m 42,0 h 2 m 1,0 h 2 m -48,1 h 1 m 42,0 h 1 m 1,0 h 2 m -47,1 h 1 m 43,0 h 2 m -46,1 h 1 m 42,0 h 2 m -45,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -44,1 h 1 m 42,0 h 1 m -46,1 h 3 m 42,0 h 3 m -48,1 h 1 m 46,0 h 1 m -48,1 h 1 m 5,0 h 36 m 5,0 h 1 m -48,1 h 1 m 5,0 h 1 m 34,0 h 1 m 5,0 h 1 m -48,1 h 7 m 34,0 h 7"
|
||||
stroke="#ffffff"
|
||||
id="path1" />
|
||||
<path
|
||||
d="m 32,21 h 30 m -30,1 h 1 m 28,0 h 1 m -30,1 h 1 m 12,0 h 1 m 16,0 h 1 m -35,1 h 5 m 30,0 h 1 m -37,1 h 1 m 36,0 h 1 m -38,1 h 18 m 20,0 h 1 m -38,1 h 1 m 37,0 h 1 m 0,1 h 3 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m 3,0 h 1 m -46,1 h 1 m 40,0 h 1 m 2,0 h 1 m -45,1 h 1 m 40,0 h 1 m 1,0 h 1 m -44,1 h 1 m 40,0 h 2 m -43,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -42,1 h 1 m 40,0 h 1 m -44,1 h 3 m 1,0 h 38 m 1,0 h 3 m -46,1 h 1 m 3,0 h 1 m 36,0 h 1 m 3,0 h 1 m -46,1 h 5 m 36,0 h 5"
|
||||
stroke="#000000"
|
||||
id="path2" />
|
||||
<path
|
||||
d="m 33,22 h 28 m -28,1 h 12 m 1,0 h 16 m -29,1 h 30 m -35,1 h 36 m -19,1 h 20 m -36,1 h 37 m -37,1 h 38 m -38,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 40 m -40,1 h 1 m 38,0 h 1 m -42,1 h 3 m 38,0 h 3"
|
||||
stroke="#da7446"
|
||||
id="path3" />
|
||||
</g>
|
||||
<g
|
||||
id="g4"
|
||||
transform="translate(-10.5,-9)">
|
||||
<path
|
||||
stroke="#ffffff"
|
||||
d="m 27,9 h 8 m -9,1 h 2 m 6,0 h 2 m -10,1 h 1 m 8,0 h 1 m -10,1 h 1 m 2,0 h 4 m 2,0 h 1 m -10,1 h 1 m 2,0 h 1 m 1,0 h 2 m 2,0 h 1 m -10,1 h 6 m 3,0 h 1 m -7,1 h 2 m 3,0 h 2 m -7,1 h 1 m 3,0 h 2 m -6,1 h 1 m 2,0 h 2 m -5,1 h 1 m 2,0 h 1 m -4,1 h 4 m -4,1 h 1 m 2,0 h 1 m -4,1 h 1 m 2,0 h 1 m -4,1 h 4"
|
||||
id="path1-5" />
|
||||
<path
|
||||
stroke="#000000"
|
||||
d="m 28,10 h 6 m -7,1 h 8 m -8,1 h 2 m 4,0 h 2 m -8,1 h 2 m 4,0 h 2 m -3,1 h 3 m -4,1 h 3 m -4,1 h 3 m -3,1 h 2 m -2,1 h 2 m -2,2 h 2 m -2,1 h 2"
|
||||
id="path2-0" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@ -18,6 +18,7 @@ export interface RawMapEntity {
|
||||
export interface RawMapEntityMetaData {
|
||||
angle?: number;
|
||||
label?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface RawMapLayer {
|
||||
|
||||
@ -3,7 +3,8 @@ import { RawMapData } from "./RawMapData";
|
||||
import {PresetSelectionState, PresetValue, RobotAttribute} from "./RawRobotState";
|
||||
import {
|
||||
AutoEmptyDockAutoEmptyInterval,
|
||||
AutoEmptyDockAutoEmptyIntervalPayload, AutoEmptyDockAutoEmptyIntervalProperties,
|
||||
AutoEmptyDockAutoEmptyIntervalPayload,
|
||||
AutoEmptyDockAutoEmptyIntervalProperties,
|
||||
Capability,
|
||||
CarpetSensorMode,
|
||||
CarpetSensorModeControlProperties,
|
||||
@ -30,6 +31,7 @@ import {
|
||||
NetworkAdvertisementProperties,
|
||||
NTPClientConfiguration,
|
||||
NTPClientStatus,
|
||||
ObstacleImagesProperties,
|
||||
Point,
|
||||
Quirk,
|
||||
RobotInformation,
|
||||
@ -66,8 +68,9 @@ import { floorObject } from "./utils";
|
||||
import {preprocessMap} from "./mapUtils";
|
||||
import ReconnectingEventSource from "reconnecting-eventsource";
|
||||
|
||||
export const valetudoAPIBaseURL = "./api/v2";
|
||||
export const valetudoAPI = axios.create({
|
||||
baseURL: "./api/v2",
|
||||
baseURL: valetudoAPIBaseURL,
|
||||
});
|
||||
|
||||
let currentCommitId = "unknown";
|
||||
@ -1120,3 +1123,23 @@ export const fetchAutoEmptyDockAutoEmptyIntervalProperties = async (): Promise<A
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchObstacleImagesState = async (): Promise<SimpleToggleState> => {
|
||||
return valetudoAPI
|
||||
.get<SimpleToggleState>(`/robot/capabilities/${Capability.ObstacleImages}`)
|
||||
.then(({ data }) => {
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
export const sendObstacleImagesState = async (enable: boolean): Promise<void> => {
|
||||
await sendToggleMutation(Capability.ObstacleImages, enable);
|
||||
};
|
||||
|
||||
export const fetchObstacleImagesProperties = async (): Promise<ObstacleImagesProperties> => {
|
||||
return valetudoAPI
|
||||
.get<ObstacleImagesProperties>(`/robot/capabilities/${Capability.ObstacleImages}/properties`)
|
||||
.then(({ data }) => {
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,11 +2,12 @@
|
||||
import { useSnackbar } from "notistack";
|
||||
import React from "react";
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
UseMutationOptions,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
UseQueryResult,
|
||||
UseQueryResult
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
BasicControlCommand,
|
||||
@ -118,6 +119,9 @@ import {
|
||||
fetchAutoEmptyDockAutoEmptyInterval,
|
||||
sendAutoEmptyDockAutoEmptyInterval,
|
||||
fetchAutoEmptyDockAutoEmptyIntervalProperties,
|
||||
fetchObstacleImagesProperties,
|
||||
fetchObstacleImagesState,
|
||||
sendObstacleImagesState,
|
||||
} from "./client";
|
||||
import {
|
||||
PresetSelectionState,
|
||||
@ -213,6 +217,8 @@ enum QueryKey {
|
||||
CollisionAvoidantNavigation = "collision_avoidant_navigation",
|
||||
CarpetSensorMode = "carpet_sensor_mode",
|
||||
CarpetSensorModeProperties = "carpet_sensor_mode_properties",
|
||||
ObstacleImages = "obstacle_image",
|
||||
ObstacleImagesProperties = "obstacle_image_properties"
|
||||
}
|
||||
|
||||
const useOnCommandError = (capability: Capability | string): ((error: unknown) => void) => {
|
||||
@ -1470,3 +1476,43 @@ export const useAutoEmptyDockAutoEmptyIntervalPropertiesQuery = () => {
|
||||
staleTime: Infinity
|
||||
});
|
||||
};
|
||||
|
||||
export const useObstacleImagesQuery = () => {
|
||||
return useQuery( {
|
||||
queryKey: [QueryKey.ObstacleImages],
|
||||
queryFn: fetchObstacleImagesState,
|
||||
|
||||
staleTime: Infinity
|
||||
});
|
||||
};
|
||||
|
||||
export const useObstacleImagesMutation = () => {
|
||||
return useValetudoFetchingMutation({
|
||||
queryKey: [QueryKey.ObstacleImages],
|
||||
mutationFn: (enable: boolean) => {
|
||||
return sendObstacleImagesState(enable).then(fetchObstacleImagesState);
|
||||
},
|
||||
onError: useOnCommandError(Capability.ObstacleImages)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const useObstacleImagesPropertiesQuery = () => {
|
||||
return useQuery( {
|
||||
queryKey: [QueryKey.ObstacleImagesProperties],
|
||||
queryFn: fetchObstacleImagesProperties,
|
||||
|
||||
staleTime: Infinity,
|
||||
});
|
||||
};
|
||||
|
||||
export const prefetchObstacleImagesProperties = async (queryClient : QueryClient) => {
|
||||
const queryKey = [QueryKey.ObstacleImagesProperties];
|
||||
|
||||
if (!queryClient.getQueryData(queryKey)) {
|
||||
return queryClient.prefetchQuery({
|
||||
queryKey: [QueryKey.ObstacleImagesProperties],
|
||||
queryFn: fetchObstacleImagesProperties,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -38,6 +38,7 @@ export enum Capability {
|
||||
WifiScan = "WifiScanCapability",
|
||||
ZoneCleaning = "ZoneCleaningCapability",
|
||||
Quirks = "QuirksCapability",
|
||||
ObstacleImages = "ObstacleImagesCapability"
|
||||
}
|
||||
|
||||
export type Point = {
|
||||
@ -574,3 +575,10 @@ export interface AutoEmptyDockAutoEmptyIntervalPayload {
|
||||
export interface AutoEmptyDockAutoEmptyIntervalProperties {
|
||||
supportedIntervals: Array<AutoEmptyDockAutoEmptyInterval>
|
||||
}
|
||||
|
||||
export interface ObstacleImagesProperties {
|
||||
dimensions: {
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
}
|
||||
|
||||
107
frontend/src/components/ObstacleImage.tsx
Normal file
107
frontend/src/components/ObstacleImage.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React from "react";
|
||||
import {Skeleton} from "@mui/material";
|
||||
import {Capability, useObstacleImagesPropertiesQuery, valetudoAPIBaseURL} from "../api";
|
||||
import {useCapabilitiesSupported} from "../CapabilitiesProvider";
|
||||
|
||||
function getScaledConfusedPlaceholderDog(newWidth: number, newHeight: number) {
|
||||
const oldWidth = 56;
|
||||
const oldHeight = 50;
|
||||
|
||||
const viewBoxWidth = newWidth / 4;
|
||||
const viewBoxHeight = newHeight / 4;
|
||||
|
||||
const viewBoxX = (viewBoxWidth - oldWidth) / 2;
|
||||
const viewBoxY = (viewBoxHeight - oldHeight) / 2;
|
||||
|
||||
const svgDog = (`
|
||||
<svg width="${newWidth}" height="${newHeight}" viewBox="-${viewBoxX} -${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}" shape-rendering="crispEdges" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="filter6" x="0" y="-.010638" width="1" height="1.0213" color-interpolation-filters="sRGB">
|
||||
<feColorMatrix values="0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0 0 0 1 0 "/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(5,1)" filter="url(#filter6)" opacity=".1">
|
||||
<g transform="translate(-25,-3)">
|
||||
<path d="m31 20h32m-32 1h1m30 0h1m-32 1h1m30 0h2m-37 1h5m31 0h2m-39 1h2m36 0h2m-40 1h1m38 0h2m-41 1h1m39 0h2m-42 1h2m39 0h4m-44 1h2m41 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m1 0h3m-48 1h1m42 0h3m1 0h1m-48 1h1m42 0h2m1 0h2m-48 1h1m42 0h1m1 0h2m-47 1h1m43 0h2m-46 1h1m42 0h2m-45 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-44 1h1m42 0h1m-46 1h3m42 0h3m-48 1h1m46 0h1m-48 1h1m5 0h36m5 0h1m-48 1h1m5 0h1m34 0h1m5 0h1m-48 1h7m34 0h7" stroke="#fff"/>
|
||||
<path d="m32 21h30m-30 1h1m28 0h1m-30 1h1m12 0h1m16 0h1m-35 1h5m30 0h1m-37 1h1m36 0h1m-38 1h18m20 0h1m-38 1h1m37 0h1m0 1h3m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m3 0h1m-46 1h1m40 0h1m2 0h1m-45 1h1m40 0h1m1 0h1m-44 1h1m40 0h2m-43 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-42 1h1m40 0h1m-44 1h3m1 0h38m1 0h3m-46 1h1m3 0h1m36 0h1m3 0h1m-46 1h5m36 0h5" stroke="#000"/>
|
||||
<path d="m33 22h28m-28 1h12m1 0h16m-29 1h30m-35 1h36m-19 1h20m-36 1h37m-37 1h38m-38 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h40m-40 1h1m38 0h1m-42 1h3m38 0h3" stroke="#da7446"/>
|
||||
</g>
|
||||
<g transform="translate(-10.5,-9)">
|
||||
<path d="m27 9h8m-9 1h2m6 0h2m-10 1h1m8 0h1m-10 1h1m2 0h4m2 0h1m-10 1h1m2 0h1m1 0h2m2 0h1m-10 1h6m3 0h1m-7 1h2m3 0h2m-7 1h1m3 0h2m-6 1h1m2 0h2m-5 1h1m2 0h1m-4 1h4m-4 1h1m2 0h1m-4 1h1m2 0h1m-4 1h4" stroke="#fff"/>
|
||||
<path d="m28 10h6m-7 1h8m-8 1h2m4 0h2m-8 1h2m4 0h2m-3 1h3m-4 1h3m-4 1h3m-3 1h2m-2 1h2m-2 2h2m-2 1h2" stroke="#000"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`);
|
||||
|
||||
return `data:image/svg+xml,${encodeURIComponent(svgDog)}`;
|
||||
}
|
||||
|
||||
const ActualObstacleImage = (props: { id: string }): React.ReactElement => {
|
||||
const [imageLoadFailed, setImageLoadFailed] = React.useState(false);
|
||||
const [imageLoaded, setImageLoaded] = React.useState(false);
|
||||
const [imageSrc, setImageSrc] = React.useState(`${valetudoAPIBaseURL}/robot/capabilities/${Capability.ObstacleImages}/img/${props.id}`);
|
||||
|
||||
const {
|
||||
data: obstacleImagesCapabilityProperties,
|
||||
} = useObstacleImagesPropertiesQuery();
|
||||
|
||||
// Since the LiveMapPage prefetches the properties, this should never be displayed
|
||||
if (!obstacleImagesCapabilityProperties) {
|
||||
return (
|
||||
<div style={{textAlign: "center"}}>
|
||||
<Skeleton height={"3rem"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const style: Record<string, any> = {
|
||||
maxWidth: "100%",
|
||||
maxHeight: "85%",
|
||||
height: "auto",
|
||||
borderRadius: "4px",
|
||||
display: "block",
|
||||
objectFit: "contain",
|
||||
border: !imageLoaded ? "1px inset black" : undefined, // Imitate the style browsers use for a broken image
|
||||
};
|
||||
|
||||
return (
|
||||
<img
|
||||
style={style}
|
||||
src={imageSrc}
|
||||
width={obstacleImagesCapabilityProperties.dimensions.width}
|
||||
height={obstacleImagesCapabilityProperties.dimensions.height}
|
||||
onLoad={() => {
|
||||
if (!imageLoadFailed) {
|
||||
setImageLoaded(true);
|
||||
}
|
||||
}}
|
||||
onError={() => {
|
||||
if (!imageLoadFailed) {
|
||||
setImageLoadFailed(true);
|
||||
|
||||
setImageSrc(getScaledConfusedPlaceholderDog(
|
||||
obstacleImagesCapabilityProperties.dimensions.width,
|
||||
obstacleImagesCapabilityProperties.dimensions.height
|
||||
));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ObstacleImage = (props: { id: string}): React.ReactElement => {
|
||||
const [
|
||||
obstacleImagesSupported,
|
||||
] = useCapabilitiesSupported(
|
||||
Capability.ObstacleImages,
|
||||
);
|
||||
|
||||
if (!obstacleImagesSupported) {
|
||||
return <></>;
|
||||
} else {
|
||||
return <ActualObstacleImage id={props.id}/>;
|
||||
}
|
||||
};
|
||||
|
||||
export default ObstacleImage;
|
||||
@ -52,6 +52,10 @@ class EditMap extends Map<EditMapProps, EditMapState> {
|
||||
|
||||
this.state = {
|
||||
selectedSegmentIds: [],
|
||||
dialogOpen: false,
|
||||
dialogTitle: "Hello World",
|
||||
dialogBody: "This should never be visible",
|
||||
|
||||
segmentNames: {},
|
||||
cuttingLine: undefined,
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import GoToActions from "./actions/live_map_actions/GoToActions";
|
||||
import {TapTouchHandlerEvent} from "./utils/touch_handling/events/TapTouchHandlerEvent";
|
||||
import React from "react";
|
||||
import {LiveMapModeSwitcher} from "./LiveMapModeSwitcher";
|
||||
import {Button, Dialog, DialogActions, DialogContent, DialogTitle} from "@mui/material";
|
||||
|
||||
|
||||
export type LiveMapMode = "segments" | "zones" | "goto" | "none";
|
||||
@ -62,6 +63,11 @@ class LiveMap extends Map<LiveMapProps, LiveMapState> {
|
||||
this.state = {
|
||||
mode: this.supportedModes[modeIdxToUse] ?? "none",
|
||||
selectedSegmentIds: [],
|
||||
|
||||
dialogOpen: false,
|
||||
dialogTitle: "Hello World",
|
||||
dialogBody: "This should never be visible",
|
||||
|
||||
zones: [],
|
||||
goToTarget: undefined
|
||||
};
|
||||
@ -291,6 +297,26 @@ class LiveMap extends Map<LiveMapProps, LiveMapState> {
|
||||
/>
|
||||
}
|
||||
</ActionsContainer>
|
||||
<Dialog
|
||||
open={this.state.dialogOpen}
|
||||
onClose={() =>{
|
||||
this.setState({dialogOpen: false});
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
{this.state.dialogTitle}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{this.state.dialogBody}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => {
|
||||
this.setState({dialogOpen: false});
|
||||
}} autoFocus>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</MapContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import {Box, Button, CircularProgress, styled, Typography, useTheme} from "@mui/material";
|
||||
import {Capability, useMapSegmentationPropertiesQuery, useRobotMapQuery} from "../api";
|
||||
import {Capability, prefetchObstacleImagesProperties, useMapSegmentationPropertiesQuery, useRobotMapQuery} from "../api";
|
||||
import LiveMap from "./LiveMap";
|
||||
import {useCapabilitiesSupported} from "../CapabilitiesProvider";
|
||||
import React from "react";
|
||||
import {useQueryClient} from "@tanstack/react-query";
|
||||
|
||||
|
||||
const Container = styled(Box)({
|
||||
@ -15,6 +16,7 @@ const Container = styled(Box)({
|
||||
});
|
||||
|
||||
const LiveMapPage = (props: Record<string, never> ): React.ReactElement => {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
data: mapData,
|
||||
isPending: mapIsPending,
|
||||
@ -25,15 +27,27 @@ const LiveMapPage = (props: Record<string, never> ): React.ReactElement => {
|
||||
const [
|
||||
goToLocationCapabilitySupported,
|
||||
mapSegmentationCapabilitySupported,
|
||||
zoneCleaningCapabilitySupported
|
||||
zoneCleaningCapabilitySupported,
|
||||
|
||||
obstacleImagesSupported,
|
||||
] = useCapabilitiesSupported(
|
||||
Capability.GoToLocation,
|
||||
Capability.MapSegmentation,
|
||||
Capability.ZoneCleaning,
|
||||
|
||||
Capability.Locate
|
||||
Capability.ObstacleImages
|
||||
);
|
||||
|
||||
// If the capability is supported, we prefetch the properties now, so that the image size
|
||||
// is already available once the user opens a dialog
|
||||
// => This prevents the content from jumping around
|
||||
if (obstacleImagesSupported) {
|
||||
prefetchObstacleImagesProperties(queryClient).catch(err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Prefetching obstacle image properties failed", err);
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: mapSegmentationProperties,
|
||||
isPending: mapSegmentationPropertiesPending
|
||||
|
||||
@ -27,7 +27,10 @@ export interface MapProps {
|
||||
}
|
||||
|
||||
export interface MapState {
|
||||
selectedSegmentIds: Array<string>
|
||||
selectedSegmentIds: Array<string>,
|
||||
dialogOpen: boolean,
|
||||
dialogTitle: string,
|
||||
dialogBody: string | React.ReactElement,
|
||||
}
|
||||
|
||||
export const usePendingMapAction = create<{
|
||||
@ -86,7 +89,10 @@ abstract class Map<P, S> extends React.Component<P & MapProps, S & MapState > {
|
||||
this.mapLayerManager = new MapLayerManager();
|
||||
|
||||
this.state = {
|
||||
selectedSegmentIds: [] as Array<string>
|
||||
selectedSegmentIds: [] as Array<string>,
|
||||
dialogOpen: false,
|
||||
dialogTitle: "Hello World",
|
||||
dialogBody: "This should never be visible",
|
||||
} as Readonly<S & MapState>;
|
||||
|
||||
|
||||
@ -371,6 +377,16 @@ abstract class Map<P, S> extends React.Component<P & MapProps, S & MapState > {
|
||||
drawRequested = true;
|
||||
}
|
||||
|
||||
if (result.openDialog) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
|
||||
dialogOpen: true,
|
||||
dialogTitle: result.openDialog.title,
|
||||
dialogBody: result.openDialog.body
|
||||
});
|
||||
}
|
||||
|
||||
if (result.stopPropagation) {
|
||||
if (result.deleteMe === true) {
|
||||
this.structureManager.removeClientStructure(structure);
|
||||
@ -391,6 +407,43 @@ abstract class Map<P, S> extends React.Component<P & MapProps, S & MapState > {
|
||||
return true;
|
||||
}
|
||||
|
||||
const mapStructuresHandledTap = this.structureManager.getMapStructures().some(structure => {
|
||||
const result = structure.tap(tappedPointInScreenSpace, currentTransform);
|
||||
|
||||
if (result.requestDraw === true) {
|
||||
drawRequested = true;
|
||||
}
|
||||
|
||||
if (result.openDialog) {
|
||||
this.setState({
|
||||
...this.state,
|
||||
|
||||
dialogOpen: true,
|
||||
dialogTitle: result.openDialog.title,
|
||||
dialogBody: result.openDialog.body
|
||||
});
|
||||
}
|
||||
|
||||
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 => {
|
||||
|
||||
@ -21,6 +21,10 @@ class RobotCoverageMap extends Map<CleanupCoverageMapProps, CleanupCoverageMapSt
|
||||
|
||||
this.state = {
|
||||
selectedSegmentIds: [],
|
||||
dialogOpen: false,
|
||||
dialogTitle: "Hello World",
|
||||
dialogBody: "This should never be visible",
|
||||
|
||||
helpDialogOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
@ -72,7 +72,8 @@ class StructureManager {
|
||||
mapStructures.push(new ObstacleMapStructure(
|
||||
p0.x,
|
||||
p0.y,
|
||||
e.metaData.label
|
||||
e.metaData.label,
|
||||
e.metaData.id
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import {Canvas2DContextTrackingWrapper} from "../utils/Canvas2DContextTrackingWrapper";
|
||||
import {PointCoordinates} from "../utils/types";
|
||||
import React from "react";
|
||||
|
||||
export type StructureInterceptionHandlerResult = {
|
||||
stopPropagation: boolean; //Will always redraw
|
||||
deleteMe?: boolean;
|
||||
requestDraw?: boolean; //Optional if things should be redrawn without stopping the event propagation
|
||||
openDialog?: {
|
||||
title: string;
|
||||
body: string | React.ReactElement;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Structure {
|
||||
|
||||
@ -1,60 +0,0 @@
|
||||
import MapStructure from "./MapStructure";
|
||||
import obstacleIconSVG from "../icons/obstacle.svg";
|
||||
import {Canvas2DContextTrackingWrapper} from "../../utils/Canvas2DContextTrackingWrapper";
|
||||
import {considerHiDPI} from "../../utils/helpers";
|
||||
|
||||
const img = new Image();
|
||||
img.src = obstacleIconSVG;
|
||||
|
||||
class ObstacleMapStructure extends MapStructure {
|
||||
public static TYPE = "ObstacleMapStructure";
|
||||
private label: string | undefined;
|
||||
|
||||
constructor(x0: number, y0: number, label?: string) {
|
||||
super(x0, y0);
|
||||
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
draw(ctxWrapper: Canvas2DContextTrackingWrapper, transformationMatrixToScreenSpace: DOMMatrixInit, scaleFactor: number): void {
|
||||
const ctx = ctxWrapper.getContext();
|
||||
const p0 = new DOMPoint(this.x0, this.y0).matrixTransform(transformationMatrixToScreenSpace);
|
||||
|
||||
|
||||
const scaledSize = {
|
||||
width: Math.max(considerHiDPI(img.width) / (considerHiDPI(8) / scaleFactor), considerHiDPI(img.width) * 0.3),
|
||||
height: Math.max(considerHiDPI(img.height) / (considerHiDPI(8) / scaleFactor), considerHiDPI(img.height) * 0.3)
|
||||
};
|
||||
|
||||
ctx.drawImage(
|
||||
img,
|
||||
p0.x - scaledSize.width / 2,
|
||||
p0.y - scaledSize.height / 2,
|
||||
scaledSize.width,
|
||||
scaledSize.height
|
||||
);
|
||||
|
||||
if (this.label && scaleFactor >= considerHiDPI(28)) {
|
||||
ctxWrapper.save();
|
||||
|
||||
ctx.textAlign = "center";
|
||||
ctx.font = `${considerHiDPI(32)}px sans-serif`;
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 1)";
|
||||
ctx.strokeStyle = "rgba(18, 18, 18, 1)";
|
||||
|
||||
ctx.lineWidth = considerHiDPI(2.5);
|
||||
ctx.strokeText(this.label, p0.x , p0.y + (scaledSize.height/2) + considerHiDPI(32));
|
||||
|
||||
ctx.lineWidth = considerHiDPI(1);
|
||||
ctx.fillText(this.label, p0.x , p0.y + (scaledSize.height/2) + considerHiDPI(32));
|
||||
|
||||
ctxWrapper.restore();
|
||||
}
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return ObstacleMapStructure.TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
export default ObstacleMapStructure;
|
||||
@ -0,0 +1,102 @@
|
||||
import MapStructure from "./MapStructure";
|
||||
import obstacleIconSVG from "../icons/obstacle.svg";
|
||||
import {Canvas2DContextTrackingWrapper} from "../../utils/Canvas2DContextTrackingWrapper";
|
||||
import {calculateBoxAroundPoint, considerHiDPI, isInsideBox} from "../../utils/helpers";
|
||||
import {PointCoordinates} from "../../utils/types";
|
||||
import {StructureInterceptionHandlerResult} from "../Structure";
|
||||
import ObstacleImage from "../../../components/ObstacleImage";
|
||||
import {Typography} from "@mui/material";
|
||||
|
||||
const img = new Image();
|
||||
img.src = obstacleIconSVG;
|
||||
|
||||
const hitboxPadding = 2.5;
|
||||
|
||||
class ObstacleMapStructure extends MapStructure {
|
||||
public static TYPE = "ObstacleMapStructure";
|
||||
private label: string | undefined;
|
||||
private id: string | undefined;
|
||||
private scaledIconSize: { width: number; height: number } = {width: 1, height: 1};
|
||||
private lastScaleFactor: number = 1;
|
||||
|
||||
constructor(x0: number, y0: number, label?: string, id?: string) {
|
||||
super(x0, y0);
|
||||
|
||||
this.label = label;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
draw(ctxWrapper: Canvas2DContextTrackingWrapper, transformationMatrixToScreenSpace: DOMMatrixInit, scaleFactor: number): void {
|
||||
this.lastScaleFactor = scaleFactor;
|
||||
|
||||
const ctx = ctxWrapper.getContext();
|
||||
const p0 = new DOMPoint(this.x0, this.y0).matrixTransform(transformationMatrixToScreenSpace);
|
||||
|
||||
|
||||
this.scaledIconSize = {
|
||||
width: Math.max(considerHiDPI(img.width) / (considerHiDPI(8) / scaleFactor), considerHiDPI(img.width) * 0.3),
|
||||
height: Math.max(considerHiDPI(img.height) / (considerHiDPI(8) / scaleFactor), considerHiDPI(img.height) * 0.3)
|
||||
};
|
||||
|
||||
ctx.drawImage(
|
||||
img,
|
||||
p0.x - this.scaledIconSize.width / 2,
|
||||
p0.y - this.scaledIconSize.height / 2,
|
||||
this.scaledIconSize.width,
|
||||
this.scaledIconSize.height
|
||||
);
|
||||
|
||||
if (this.label && scaleFactor >= considerHiDPI(28)) {
|
||||
ctxWrapper.save();
|
||||
|
||||
ctx.textAlign = "center";
|
||||
ctx.font = `${considerHiDPI(32)}px sans-serif`;
|
||||
ctx.fillStyle = "rgba(255, 255, 255, 1)";
|
||||
ctx.strokeStyle = "rgba(18, 18, 18, 1)";
|
||||
|
||||
ctx.lineWidth = considerHiDPI(2.5);
|
||||
ctx.strokeText(this.label, p0.x , p0.y + (this.scaledIconSize.height/2) + considerHiDPI(32));
|
||||
|
||||
ctx.lineWidth = considerHiDPI(1);
|
||||
ctx.fillText(this.label, p0.x , p0.y + (this.scaledIconSize.height/2) + considerHiDPI(32));
|
||||
|
||||
ctxWrapper.restore();
|
||||
}
|
||||
}
|
||||
|
||||
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.lastScaleFactor >= considerHiDPI(6)) {
|
||||
return {
|
||||
stopPropagation: true,
|
||||
openDialog: {
|
||||
title: "Obstacle Information",
|
||||
body: (
|
||||
<>
|
||||
{
|
||||
this.id &&
|
||||
<ObstacleImage id={this.id} />
|
||||
}
|
||||
<Typography sx={{marginTop: this.id ? "0.5rem" : undefined}}>
|
||||
{this.label}
|
||||
</Typography>
|
||||
</>
|
||||
)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
stopPropagation: false
|
||||
};
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
return ObstacleMapStructure.TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
export default ObstacleMapStructure;
|
||||
@ -22,6 +22,8 @@ import {
|
||||
useLocateMutation,
|
||||
useObstacleAvoidanceControlMutation,
|
||||
useObstacleAvoidanceControlQuery,
|
||||
useObstacleImagesMutation,
|
||||
useObstacleImagesQuery,
|
||||
usePetObstacleAvoidanceControlMutation,
|
||||
usePetObstacleAvoidanceControlQuery,
|
||||
} from "../api";
|
||||
@ -36,6 +38,7 @@ import {
|
||||
MiscellaneousServices as MiscIcon,
|
||||
NotListedLocation as LocateIcon,
|
||||
Pets as PetObstacleAvoidanceControlIcon,
|
||||
Photo as ObstacleImagesIcon,
|
||||
RoundaboutRight as CollisionAvoidantNavigationControlIcon,
|
||||
Sensors as CarpetModeIcon,
|
||||
Star as QuirksIcon,
|
||||
@ -364,6 +367,32 @@ const PetObstacleAvoidanceControlCapabilitySwitchListMenuItem = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ObstacleImagesCapabilitySwitchListMenuItem = () => {
|
||||
const {
|
||||
data: data,
|
||||
isFetching: isFetching,
|
||||
isError: isError,
|
||||
} = useObstacleImagesQuery();
|
||||
|
||||
const {mutate: mutate, isPending: isChanging} = useObstacleImagesMutation();
|
||||
const loading = isFetching || isChanging;
|
||||
const disabled = loading || isChanging || isError;
|
||||
|
||||
return (
|
||||
<ToggleSwitchListMenuItem
|
||||
value={data?.enabled ?? false}
|
||||
setValue={(value) => {
|
||||
mutate(value);
|
||||
}}
|
||||
disabled={disabled}
|
||||
loadError={isError}
|
||||
primaryLabel={"Obstacle Images"}
|
||||
secondaryLabel={"Take pictures of all encountered obstacles."}
|
||||
icon={<ObstacleImagesIcon/>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CollisionAvoidantNavigationControlCapabilitySwitchListMenuItem = () => {
|
||||
const {
|
||||
data: data,
|
||||
@ -397,6 +426,7 @@ const RobotOptions = (): React.ReactElement => {
|
||||
|
||||
obstacleAvoidanceControlCapabilitySupported,
|
||||
petObstacleAvoidanceControlCapabilitySupported,
|
||||
obstacleImagesSupported,
|
||||
collisionAvoidantNavigationControlCapabilitySupported,
|
||||
carpetModeControlCapabilitySupported,
|
||||
carpetSensorModeControlCapabilitySupported,
|
||||
@ -417,6 +447,7 @@ const RobotOptions = (): React.ReactElement => {
|
||||
|
||||
Capability.ObstacleAvoidanceControl,
|
||||
Capability.PetObstacleAvoidanceControl,
|
||||
Capability.ObstacleImages,
|
||||
Capability.CollisionAvoidantNavigation,
|
||||
Capability.CarpetModeControl,
|
||||
Capability.CarpetSensorModeControl,
|
||||
@ -462,6 +493,12 @@ const RobotOptions = (): React.ReactElement => {
|
||||
);
|
||||
}
|
||||
|
||||
if (obstacleImagesSupported) {
|
||||
items.push(
|
||||
<ObstacleImagesCapabilitySwitchListMenuItem key={"obstacleImages"}/>
|
||||
);
|
||||
}
|
||||
|
||||
if (collisionAvoidantNavigationControlCapabilitySupported) {
|
||||
items.push(
|
||||
<CollisionAvoidantNavigationControlCapabilitySwitchListMenuItem key={"collisionAvoidantNavigationControl"}/>
|
||||
@ -483,6 +520,7 @@ const RobotOptions = (): React.ReactElement => {
|
||||
}, [
|
||||
obstacleAvoidanceControlCapabilitySupported,
|
||||
petObstacleAvoidanceControlCapabilitySupported,
|
||||
obstacleImagesSupported,
|
||||
collisionAvoidantNavigationControlCapabilitySupported,
|
||||
carpetModeControlCapabilitySupported,
|
||||
carpetSensorModeControlCapabilitySupported
|
||||
|
||||
Loading…
Reference in New Issue
Block a user