diff --git a/frontend/__test_support__/three_d_mocks.tsx b/frontend/__test_support__/three_d_mocks.tsx index 9860b0ea78..522ce8797b 100644 --- a/frontend/__test_support__/three_d_mocks.tsx +++ b/frontend/__test_support__/three_d_mocks.tsx @@ -10,7 +10,9 @@ import * as THREE from "three"; import React, { ReactNode } from "react"; import { TransitionFn, UseSpringProps } from "@react-spring/three"; import { ThreeElements, ThreeEvent } from "@react-three/fiber"; -import { Cloud, Clouds, Image, Plane, Trail, Tube } from "@react-three/drei"; +import { + Cloud, Clouds, Image, Instance, Instances, Plane, Trail, Tube, +} from "@react-three/drei"; const GroupForTests = (props: ThreeElements["group"]) => // @ts-expect-error Property does not exist on type JSX.IntrinsicElements @@ -47,6 +49,13 @@ jest.mock("../three_d_garden/components", () => ({ props.visible === false ? <> : , + MeshBasicMaterial: (props: THREE.MeshBasicMaterial) => { + // eslint-disable-next-line max-len + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + props.onBeforeCompile?.({} as any, {} as any); + // @ts-expect-error Property does not exist on type JSX.IntrinsicElements + return
; + }, })); jest.mock("three/examples/jsm/Addons.js", () => ({ @@ -64,6 +73,7 @@ jest.mock("@react-three/fiber", () => ({ pointer: { x: 0, y: 0 }, camera: new THREE.PerspectiveCamera(), })), + extend: jest.fn(), })); jest.mock("@react-spring/three", () => ({ @@ -559,6 +569,11 @@ jest.mock("@react-three/drei", () => { return { ...jest.requireActual("@react-three/drei"), useGLTF, + shaderMaterial: jest.fn(), + Instances: (props: React.ComponentProps) => +
{props.children}
, + Instance: (props: React.ComponentProps) => +
{props.name}
, RoundedBox: ({ name }: { name: string }) =>
{name}
, Plane: (props: React.ComponentProps) => diff --git a/frontend/api/__tests__/api_test.ts b/frontend/api/__tests__/api_test.ts index 5edfcce360..8d4c28c2f7 100644 --- a/frontend/api/__tests__/api_test.ts +++ b/frontend/api/__tests__/api_test.ts @@ -10,7 +10,7 @@ describe("API", () => { [ [API.current.pointSearchPath, BASE + "/api/points/search"], [API.current.allPointsPath, BASE + "/api/points/?filter=all"], - [API.current.sensorReadingPath, BASE + "/api/sensor_readings"], + [API.current.sensorReadingPath, BASE + "/api/sensor_readings/"], [API.current.farmwareEnvPath, BASE + "/api/farmware_envs/"], [API.current.plantTemplatePath, BASE + "/api/plant_templates/"], [API.current.farmwareInstallationPath, BASE + "/api/farmware_installations/"], diff --git a/frontend/api/api.ts b/frontend/api/api.ts index edb1f9c44b..45b66b96ec 100644 --- a/frontend/api/api.ts +++ b/frontend/api/api.ts @@ -139,7 +139,7 @@ export class API { /** /api/firmware_config */ get firmwareConfigPath() { return `${this.baseUrl}/api/firmware_config/`; } /** /api/sensor_readings */ - get sensorReadingPath() { return `${this.baseUrl}/api/sensor_readings`; } + get sensorReadingPath() { return `${this.baseUrl}/api/sensor_readings/`; } /** /api/sensors/ */ get sensorPath() { return `${this.baseUrl}/api/sensors/`; } /** /api/farmware_envs/:id */ diff --git a/frontend/css/panels/sensors.scss b/frontend/css/panels/sensors.scss index f6c63c499e..73f3544cd4 100644 --- a/frontend/css/panels/sensors.scss +++ b/frontend/css/panels/sensors.scss @@ -20,11 +20,21 @@ font-size: 1.2rem; background: $translucent1_white; border-radius: 0.5rem; + max-height: 50rem; + overflow: scroll; th, td { width: 1%; + padding: 0.25rem; + &:first-of-type { + padding-left: 0.5rem; + } + &:last-of-type { + padding: 0; + } } tr { + height: 3rem; &.previous { color: $medium_gray; } @@ -32,6 +42,15 @@ background: $translucent1_white; } } + label { + font-size: 1.1rem; + } + .fa-trash { + color: $transparent; + &:hover { + color: unset; // sass-lint:disable-line variable-for-property + } + } .sensor-history-table-contents { max-height: 20rem; overflow-y: auto; diff --git a/frontend/demo/lua_runner/__tests__/calculate_move_test.ts b/frontend/demo/lua_runner/__tests__/calculate_move_test.ts index a7020bc92a..600265d29d 100644 --- a/frontend/demo/lua_runner/__tests__/calculate_move_test.ts +++ b/frontend/demo/lua_runner/__tests__/calculate_move_test.ts @@ -420,7 +420,7 @@ describe("calculateMove()", () => { }); it("handles soil height z axis overwrite: triangle data", () => { - sessionStorage.setItem("triangles", "[\"foo\"]"); + sessionStorage.setItem("soilSurfaceTriangles", "[\"foo\"]"); const command: Move = { kind: "move", args: {}, diff --git a/frontend/demo/lua_runner/__tests__/index_test.ts b/frontend/demo/lua_runner/__tests__/index_test.ts index 5ca0472039..cab9577da1 100644 --- a/frontend/demo/lua_runner/__tests__/index_test.ts +++ b/frontend/demo/lua_runner/__tests__/index_test.ts @@ -1410,6 +1410,15 @@ describe("runDemoLuaCode()", () => { expect(error).not.toHaveBeenCalled(); expect(console.log).toHaveBeenCalledWith("0"); expect(info).not.toHaveBeenCalled(); + expect(initSave).toHaveBeenCalledWith("SensorReading", { + pin: 5, + mode: 1, + x: 1, + y: 2, + z: 0, + value: 0, + read_at: expect.any(String), + }); }); it("runs move_relative", () => { diff --git a/frontend/demo/lua_runner/actions.ts b/frontend/demo/lua_runner/actions.ts index b93fa773e5..d18df84861 100644 --- a/frontend/demo/lua_runner/actions.ts +++ b/frontend/demo/lua_runner/actions.ts @@ -235,6 +235,18 @@ export const expandActions = ( setCurrent(homeTarget); }); break; + case "read_pin": + const pin = action.args[0] as number; + expanded.push({ + type: "sensor_reading", + args: [ + pin, + current.x, + current.y, + current.z, + ], + }); + break; default: expanded.push(action); break; @@ -361,6 +373,18 @@ export const runActions = ( payload: action.args[0] as number, }); }; + case "sensor_reading": + return () => { + store.dispatch(initSave("SensorReading", { + pin: action.args[0] as number, + mode: 1, + x: action.args[1] as number, + y: action.args[2] as number, + z: action.args[3] as number, + value: random(0, 1024), + read_at: (new Date()).toISOString(), + }) as unknown as UnknownAction); + }; case "write_pin": const pin = action.args[0] as number; const mode = action.args[1] as string; diff --git a/frontend/demo/lua_runner/interfaces.ts b/frontend/demo/lua_runner/interfaces.ts index a533ab0616..da080dddf4 100644 --- a/frontend/demo/lua_runner/interfaces.ts +++ b/frontend/demo/lua_runner/interfaces.ts @@ -8,6 +8,8 @@ export interface Action { | "move" | "_move" | "toggle_pin" + | "read_pin" + | "sensor_reading" | "emergency_lock" | "emergency_unlock" | "find_home" diff --git a/frontend/demo/lua_runner/run.ts b/frontend/demo/lua_runner/run.ts index 53ce5827c2..7c5f27de2a 100644 --- a/frontend/demo/lua_runner/run.ts +++ b/frontend/demo/lua_runner/run.ts @@ -536,6 +536,7 @@ export const runLua = jsToLua(L, toolMounted ? 0 : 1); return 1; } + actions.push({ type: "read_pin", args: [pin] }); jsToLua(L, 0); return 1; }); diff --git a/frontend/demo/lua_runner/stubs.ts b/frontend/demo/lua_runner/stubs.ts index 23f2ade38d..0d3251b2ef 100644 --- a/frontend/demo/lua_runner/stubs.ts +++ b/frontend/demo/lua_runner/stubs.ts @@ -59,7 +59,7 @@ export const getSafeZ = (): number => { export const getSoilHeight = (x: number, y: number): number => { const triangles = JSON.parse( - sessionStorage.getItem("triangles") || "[]") as TriangleData[]; + sessionStorage.getItem("soilSurfaceTriangles") || "[]") as TriangleData[]; const getZ = getZFunc(triangles, -500); return getZ(x, y); }; diff --git a/frontend/devices/__tests__/actions_test.ts b/frontend/devices/__tests__/actions_test.ts index a74f50f095..096af4c2d0 100644 --- a/frontend/devices/__tests__/actions_test.ts +++ b/frontend/devices/__tests__/actions_test.ts @@ -610,6 +610,10 @@ describe("pinToggle()", () => { }); describe("readPin()", () => { + afterEach(() => { + localStorage.removeItem("myBotIs"); + }); + it("calls readPin", async () => { await actions.readPin(1, "label", 0); expect(mockDevice.current.readPin).toHaveBeenCalledWith({ @@ -617,6 +621,13 @@ describe("readPin()", () => { }); expect(success).not.toHaveBeenCalled(); }); + + it("reads demo account pin", async () => { + localStorage.setItem("myBotIs", "online"); + await actions.readPin(1, "label", 0); + expect(mockDevice.current.readPin).not.toHaveBeenCalled(); + expect(runDemoLuaCode).toHaveBeenCalledWith("read_pin(1)"); + }); }); describe("writePin()", () => { diff --git a/frontend/devices/actions.ts b/frontend/devices/actions.ts index a16539179f..6e9045aed0 100644 --- a/frontend/devices/actions.ts +++ b/frontend/devices/actions.ts @@ -447,7 +447,10 @@ export function readPin( pin_number: number, label: string, pin_mode: ALLOWED_PIN_MODES, ) { const noun = t("Read pin"); - maybeNoop(); + if (forceOnline()) { + runDemoLuaCode(`read_pin(${pin_number})`); + return; + } return getDevice() .readPin({ pin_number, label, pin_mode }) .then(maybeNoop, commandErr(noun)); diff --git a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx index 1f59c99af4..1f1c0b12e9 100644 --- a/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx +++ b/frontend/farm_designer/__tests__/three_d_garden_map_test.tsx @@ -20,7 +20,7 @@ import { fakePlant } from "../../__test_support__/fake_state/resources"; import { render } from "@testing-library/react"; import { ThreeDGarden } from "../../three_d_garden"; import { clone } from "lodash"; -import { INITIAL } from "../../three_d_garden/config"; +import { INITIAL, SurfaceDebugOption } from "../../three_d_garden/config"; import { FirmwareHardware } from "farmbot"; import { CROPS } from "../../crops/constants"; import { fakeDevice } from "../../__test_support__/resource_index_builder"; @@ -32,6 +32,8 @@ const EMPTY_PROPS = { allPoints: [], groups: [], images: [], + sensors: [], + sensorReadings: [], }; describe("", () => { @@ -56,7 +58,10 @@ describe("", () => { allPoints: [], groups: [], images: [], + sensors: [], + sensorReadings: [], cameraCalibrationData: fakeCameraCalibrationData(), + farmwareEnvs: [], }); it("converts props", () => { @@ -97,7 +102,8 @@ describe("", () => { expectedConfig.cableDebug = true; expectedConfig.eventDebug = true; expectedConfig.lightsDebug = true; - expectedConfig.surfaceDebug = true; + expectedConfig.moistureDebug = true; + expectedConfig.surfaceDebug = SurfaceDebugOption.normals; expectedConfig.lowDetail = true; expectedConfig.solar = true; expectedConfig.stats = true; diff --git a/frontend/farm_designer/index.tsx b/frontend/farm_designer/index.tsx index 7ad6c3a254..d80ce14ca8 100755 --- a/frontend/farm_designer/index.tsx +++ b/frontend/farm_designer/index.tsx @@ -188,7 +188,6 @@ export class RawFarmDesigner showZones={show_zones} showSensorReadings={show_sensor_readings} showMoistureInterpolationMap={show_moisture_interpolation_map} - hasSensorReadings={this.props.sensorReadings.length > 0} dispatch={this.props.dispatch} timeSettings={this.props.timeSettings} getConfigValue={this.props.getConfigValue} @@ -236,6 +235,9 @@ export class RawFarmDesigner allPoints={this.props.allPoints} groups={this.props.groups} images={this.props.latestImages} + sensorReadings={this.props.sensorReadings} + sensors={this.props.sensors} + farmwareEnvs={this.props.farmwareEnvs} cameraCalibrationData={this.props.cameraCalibrationData} getWebAppConfigValue={this.props.getConfigValue} /> :
, @@ -397,6 +397,7 @@ const ReadingsListItem = (props: ReadingsListItemProps) => const sensorName = `${props.sensorNameByPinLookup[pin]} (pin ${pin})`; return ", () => { cropPhotos: false, showUncroppedArea: false, soilHeightLabels: false, - getSoilHeightColor: () => "rgb(128, 128, 128)", + getSoilHeightColor: () => ({ rgb: "rgb(128, 128, 128)", a: 1 }), current: false, animate: false, }); @@ -125,7 +125,7 @@ describe("", () => { const wrapper = svgMount(); expect(wrapper.text()).toContain("-100"); expect(wrapper.find("text").first().props().fill) - .toEqual(p.getSoilHeightColor(-100)); + .toEqual(p.getSoilHeightColor(-100).rgb); expect(wrapper.find("text").first().props().stroke).toEqual(Color.black); }); diff --git a/frontend/farm_designer/map/layers/points/garden_point.tsx b/frontend/farm_designer/map/layers/points/garden_point.tsx index 1e5a28b4fc..f56c16c972 100644 --- a/frontend/farm_designer/map/layers/points/garden_point.tsx +++ b/frontend/farm_designer/map/layers/points/garden_point.tsx @@ -46,7 +46,7 @@ export const GardenPoint = (props: GardenPointProps) => { {props.soilHeightLabels && soilHeightPoint(point) && { rgb: string, a: number }; + export enum InterpolationKey { data = "interpolationData", hash = "interpolationHash", @@ -80,8 +82,8 @@ export const getZAtLocation = interface GenerateInterpolationMapDataProps { kind: "Point" | "SensorReading"; points: (TaggedGenericPointer | TaggedSensorReading)[]; - mapTransformProps: MapTransformProps; - getColor(z: number): string; + gridSize: AxisNumberProperty; + getColor: GetColor; options: InterpolationOptions; } @@ -104,10 +106,10 @@ const convertToPointObject = export const generateData = (props: GenerateInterpolationMapDataProps) => { const points = selectMostRecentPoints(props.points); - const { gridSize } = props.mapTransformProps; + const { gridSize } = props; const { stepSize } = props.options; const hash = [ - JSON.stringify(points), + JSON.stringify(points.map(p => p.uuid)), JSON.stringify(gridSize), JSON.stringify(props.options), ].join(""); @@ -160,7 +162,7 @@ interface InterpolationMapProps { kind: "Point" | "SensorReading"; points: (TaggedGenericPointer | TaggedSensorReading)[]; mapTransformProps: MapTransformProps; - getColor(z: number): string; + getColor: GetColor; options: InterpolationOptions; } @@ -174,11 +176,12 @@ export const InterpolationMap = (props: InterpolationMapProps) => { const { quadrant } = props.mapTransformProps; const xOffset = [1, 4].includes(quadrant); const yOffset = [3, 4].includes(quadrant); + const colorInfo = props.getColor(z); return ; + fill={colorInfo.rgb} fillOpacity={colorInfo.a} />; })} ; diff --git a/frontend/farm_designer/map/layers/points/point_layer.tsx b/frontend/farm_designer/map/layers/points/point_layer.tsx index ce6cbffb30..64dca51876 100644 --- a/frontend/farm_designer/map/layers/points/point_layer.tsx +++ b/frontend/farm_designer/map/layers/points/point_layer.tsx @@ -36,7 +36,11 @@ export function PointLayer(props: PointLayerProps) { props.interactions ? {} : { pointerEvents: "none" }; const options = fetchInterpolationOptions(props.farmwareEnvs); generateData({ - kind: "Point", points: soilHeightPoints, mapTransformProps, getColor, options, + kind: "Point", + points: soilHeightPoints, + gridSize: mapTransformProps.gridSize, + getColor, + options, }); return {props.overlayVisible && diff --git a/frontend/farm_designer/map/layers/sensor_readings/__tests__/sensor_readings_layer_test.tsx b/frontend/farm_designer/map/layers/sensor_readings/__tests__/sensor_readings_layer_test.tsx index 203430cdea..c929a8a8e0 100644 --- a/frontend/farm_designer/map/layers/sensor_readings/__tests__/sensor_readings_layer_test.tsx +++ b/frontend/farm_designer/map/layers/sensor_readings/__tests__/sensor_readings_layer_test.tsx @@ -1,5 +1,6 @@ import React from "react"; import { + getMoistureColor, SensorReadingsLayer, SensorReadingsLayerProps, } from "../sensor_readings_layer"; import { @@ -48,7 +49,7 @@ describe("", () => { p.sensorReadings[0].body.mode = ANALOG; const reading = fakeSensorReading(); reading.body.mode = ANALOG; - reading.body.value = 1000; + reading.body.value = 800; reading.body.x = 100; reading.body.y = 200; p.sensorReadings.push(reading); @@ -58,3 +59,17 @@ describe("", () => { expect(layer.find("rect").length).toEqual(1800); }); }); + +describe("getMoistureColor()", () => { + it.each<[number, string, number]>([ + [0, "rgb(0, 0, 255)", 0], + [200, "rgb(0, 0, 255)", 0], + [700, "rgb(0, 0, 255)", 0.2], + [900, "rgb(0, 0, 255)", 0.42], + [1024, "rgb(0, 0, 0)", 0], + ])("returns color for %s: %s %s", (value, color, alpha) => { + const c = getMoistureColor(value); + expect(c.rgb).toEqual(color); + expect(c.a).toEqual(alpha); + }); +}); diff --git a/frontend/farm_designer/map/layers/sensor_readings/sensor_readings_layer.tsx b/frontend/farm_designer/map/layers/sensor_readings/sensor_readings_layer.tsx index f8d3d11f85..020b7e818b 100644 --- a/frontend/farm_designer/map/layers/sensor_readings/sensor_readings_layer.tsx +++ b/frontend/farm_designer/map/layers/sensor_readings/sensor_readings_layer.tsx @@ -7,9 +7,23 @@ import { GardenSensorReading } from "./garden_sensor_reading"; import { last, round } from "lodash"; import { TimeSettings } from "../../../../interfaces"; import { - fetchInterpolationOptions, generateData, InterpolationMap, + fetchInterpolationOptions, generateData, GetColor, InterpolationMap, } from "../points/interpolation_map"; +export const filterMoistureReadings = ( + sensorReadings: TaggedSensorReading[], + sensors: TaggedSensor[], +) => { + const sensorNameByPinLookup: { [x: number]: string } = {}; + sensors.map(x => { sensorNameByPinLookup[x.body.pin || 0] = x.body.label; }); + const readings = sensorReadings + .filter(r => + (sensorNameByPinLookup[r.body.pin] || "").toLowerCase().includes("soil") + && r.body.mode == ANALOG) + .filter(r => r.body.value <= 900); + return { readings, sensorNameByPinLookup }; +}; + export interface SensorReadingsLayerProps { visible: boolean; overlayVisible: boolean; @@ -25,16 +39,14 @@ export function SensorReadingsLayer(props: SensorReadingsLayerProps) { visible, sensorReadings, mapTransformProps, timeSettings, sensors } = props; const mostRecentSensorReading = last(sensorReadings); - const sensorNameByPinLookup: { [x: number]: string } = {}; - sensors.map(x => { sensorNameByPinLookup[x.body.pin || 0] = x.body.label; }); const options = fetchInterpolationOptions(props.farmwareEnvs); - const moistureReadings = sensorReadings - .filter(r => - (sensorNameByPinLookup[r.body.pin] || "").toLowerCase().includes("soil") - && r.body.mode == ANALOG); + const { readings: moistureReadings, sensorNameByPinLookup } = + filterMoistureReadings(sensorReadings, sensors); generateData({ kind: "SensorReading", - points: moistureReadings, mapTransformProps, getColor: getMoistureColor, + points: moistureReadings, + gridSize: mapTransformProps.gridSize, + getColor: getMoistureColor, options, }); return @@ -57,8 +69,15 @@ export function SensorReadingsLayer(props: SensorReadingsLayerProps) { ; } -const getMoistureColor = (value: number) => { - const normalizedValue = round(255 * value / 1024); - if (value > 900) { return "rgb(255, 255, 255)"; } - return `rgb(0, 0, ${normalizedValue})`; +export const getMoistureColor: GetColor = (value: number) => { + const maxValue = 900; + if (value > maxValue) { return { rgb: "rgb(0, 0, 0)", a: 0 }; } + const r = 0; + const g = 0; + const b = 255; + const a = round((0.75 * value / maxValue) ** 3, 2); + return { + rgb: `rgb(${r}, ${g}, ${b})`, + a: a, + }; }; diff --git a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx index 64c074df3b..d1975542d8 100644 --- a/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx +++ b/frontend/farm_designer/map/legend/__tests__/garden_map_legend_test.tsx @@ -45,7 +45,6 @@ describe("", () => { showZones: false, showSensorReadings: false, showMoistureInterpolationMap: false, - hasSensorReadings: false, dispatch: jest.fn(), timeSettings: fakeTimeSettings(), getConfigValue: jest.fn(), @@ -69,7 +68,6 @@ describe("", () => { it("renders with readings", () => { const p = fakeProps(); - p.hasSensorReadings = true; const wrapper = mount(); expect(wrapper.text().toLowerCase()).toContain("readings"); }); diff --git a/frontend/farm_designer/map/legend/garden_map_legend.tsx b/frontend/farm_designer/map/legend/garden_map_legend.tsx index 2f61fd7262..ee188a874a 100644 --- a/frontend/farm_designer/map/legend/garden_map_legend.tsx +++ b/frontend/farm_designer/map/legend/garden_map_legend.tsx @@ -129,8 +129,8 @@ interface LayerTogglesProps extends GardenMapLegendProps { } const LayerToggles = (props: LayerTogglesProps) => { const { toggle, getConfigValue, dispatch, firmwareConfig } = props; const subMenuProps = { dispatch, getConfigValue, firmwareConfig }; - const only2DClass = - getConfigValue(BooleanSetting.three_d_garden) ? "disabled" : ""; + const is3D = getConfigValue(BooleanSetting.three_d_garden); + const only2DClass = is3D ? "disabled" : ""; return
{ value={props.showPoints} label={DeviceSetting.showPoints} onClick={toggle(BooleanSetting.show_points)} /> - + {!is3D && + } { value={props.showZones} label={DeviceSetting.showAreas} onClick={toggle(BooleanSetting.show_zones)} /> - {props.hasSensorReadings && - } - {props.hasSensorReadings && - } + +
; }; diff --git a/frontend/farm_designer/three_d_garden_map.tsx b/frontend/farm_designer/three_d_garden_map.tsx index f47d8e6ba9..358977643b 100644 --- a/frontend/farm_designer/three_d_garden_map.tsx +++ b/frontend/farm_designer/three_d_garden_map.tsx @@ -7,8 +7,9 @@ import { import { clone } from "lodash"; import { BotPosition, SourceFbosConfig } from "../devices/interfaces"; import { - ConfigurationName, TaggedCurve, TaggedGenericPointer, TaggedImage, TaggedPoint, - TaggedPointGroup, TaggedWeedPointer, + ConfigurationName, TaggedCurve, TaggedFarmwareEnv, TaggedGenericPointer, + TaggedImage, TaggedPoint, + TaggedPointGroup, TaggedSensor, TaggedSensorReading, TaggedWeedPointer, } from "farmbot"; import { CameraCalibrationData, DesignerState } from "./interfaces"; import { GetWebAppConfigValue } from "../config_storage/actions"; @@ -22,6 +23,7 @@ import { DeviceAccountSettings } from "farmbot/dist/resources/api_resources"; import { SCENES } from "../settings/three_d_settings"; import { get3DTime, latLng } from "../three_d_garden/time_travel"; import { parseCalibrationData } from "./map/layers/images/map_image"; +import { fetchInterpolationOptions } from "./map/layers/points/interpolation_map"; export interface ThreeDGardenMapProps { botSize: BotSize; @@ -45,7 +47,10 @@ export interface ThreeDGardenMapProps { allPoints: TaggedPoint[]; groups: TaggedPointGroup[]; images: TaggedImage[]; + sensorReadings: TaggedSensorReading[]; + sensors: TaggedSensor[]; cameraCalibrationData: CameraCalibrationData; + farmwareEnvs: TaggedFarmwareEnv[]; } export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { @@ -104,7 +109,8 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.eventDebug = !!getValue("eventDebug"); config.cableDebug = !!getValue("cableDebug"); config.lightsDebug = !!getValue("lightsDebug"); - config.surfaceDebug = !!getValue("surfaceDebug"); + config.moistureDebug = !!getValue("moistureDebug"); + config.surfaceDebug = getValue("surfaceDebug"); config.sun = getValue("sun"); config.ambient = getValue("ambient"); config.heading = getValue("heading"); @@ -159,6 +165,11 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { config.imgCenterX = camCalData.centerX; config.imgCenterY = camCalData.centerY; + const options = fetchInterpolationOptions(props.farmwareEnvs); + config.interpolationStepSize = options.stepSize; + config.interpolationUseNearest = options.useNearest; + config.interpolationPower = options.power; + config.zoom = true; config.pan = true; config.rotate = !props.designer.threeDTopDownView; @@ -176,6 +187,8 @@ export const ThreeDGardenMap = (props: ThreeDGardenMapProps) => { allPoints={props.allPoints} groups={props.groups} images={props.images} + sensorReadings={props.sensorReadings} + sensors={props.sensors} addPlantProps={{ gridSize: props.mapTransformProps.gridSize, dispatch: props.dispatch, diff --git a/frontend/points/__tests__/soil_height_test.tsx b/frontend/points/__tests__/soil_height_test.tsx index bbd62e4741..090bdf097b 100644 --- a/frontend/points/__tests__/soil_height_test.tsx +++ b/frontend/points/__tests__/soil_height_test.tsx @@ -42,7 +42,7 @@ describe("getSoilHeightColor()", () => { tagAsSoilHeight(point1); point1.body.z = 100; const getColor = getSoilHeightColor([point0, point1]); - expect(getColor(50)).toEqual("rgb(128, 128, 128)"); + expect(getColor(50).rgb).toEqual("rgb(128, 128, 128)"); }); }); diff --git a/frontend/points/point_inventory.tsx b/frontend/points/point_inventory.tsx index e181c29b98..638959cc0e 100644 --- a/frontend/points/point_inventory.tsx +++ b/frontend/points/point_inventory.tsx @@ -40,6 +40,7 @@ import { pointGroupSubset } from "../plants/select_plants"; import { Path } from "../internal_urls"; import { deleteAllIds } from "../api/delete_points_handler"; import { NavigationContext } from "../routes_helpers"; +import { GetColor } from "../farm_designer/map/layers/points/interpolation_map"; interface PointsSectionProps { title: string; @@ -52,7 +53,7 @@ interface PointsSectionProps { hoveredPoint: UUID | undefined; dispatch: Function; metaQuery: Record; - getColorOverride?(z: number): string; + getColorOverride?: GetColor; averageZ?: number; sourceFbosConfig?: SourceFbosConfig; } @@ -89,7 +90,7 @@ const PointsSection = (props: PointsSectionProps) => { {genericPoints.map(p => )} diff --git a/frontend/points/soil_height.tsx b/frontend/points/soil_height.tsx index 0791159a92..36209084fd 100644 --- a/frontend/points/soil_height.tsx +++ b/frontend/points/soil_height.tsx @@ -45,7 +45,10 @@ export const getSoilHeightColor = const max = Math.max(...soilHeights); return (z: number) => { const normalizedZ = round(255 * (max > min ? (z - min) / (max - min) : 1)); - return `rgb(${normalizedZ}, ${normalizedZ}, ${normalizedZ})`; + return { + rgb: `rgb(${normalizedZ}, ${normalizedZ}, ${normalizedZ})`, + a: 1, + }; }; }; diff --git a/frontend/sensors/sensor_readings/__tests__/sensor_readings_test.tsx b/frontend/sensors/sensor_readings/__tests__/sensor_readings_test.tsx index 45d3682d13..4b64ef31f3 100644 --- a/frontend/sensors/sensor_readings/__tests__/sensor_readings_test.tsx +++ b/frontend/sensors/sensor_readings/__tests__/sensor_readings_test.tsx @@ -1,3 +1,7 @@ +jest.mock("../../../api/crud", () => ({ + destroy: jest.fn(), +})); + import React from "react"; import { mount } from "enzyme"; import moment from "moment"; @@ -7,6 +11,8 @@ import { fakeSensorReading, fakeSensor, } from "../../../__test_support__/fake_state/resources"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; +import { destroy } from "../../../api/crud"; +import { busy } from "../../../toast/toast"; describe("", () => { const fakeProps = (): SensorReadingsProps => ({ @@ -98,4 +104,29 @@ describe("", () => { expect(wrapper.instance().state.xyzLocation).toEqual(undefined); expect(wrapper.instance().state.sensor).toEqual(undefined); }); + + it("deletes selected readings", () => { + jest.useFakeTimers(); + window.confirm = () => true; + const p = fakeProps(); + const wrapper = mount(); + const reading = fakeSensorReading(); + reading.uuid = "uuid0"; + wrapper.instance().deleteSelected([reading])(); + jest.runAllTimers(); + expect(destroy).toHaveBeenCalledWith("uuid0"); + expect(busy).toHaveBeenCalledWith("Deleting 1 sensor readings..."); + }); + + it("doesn't delete selected readings", () => { + jest.useFakeTimers(); + window.confirm = () => false; + const p = fakeProps(); + const wrapper = mount(); + const reading = fakeSensorReading(); + reading.uuid = "uuid0"; + wrapper.instance().deleteSelected([reading])(); + jest.runAllTimers(); + expect(destroy).not.toHaveBeenCalled(); + }); }); diff --git a/frontend/sensors/sensor_readings/__tests__/table_test.tsx b/frontend/sensors/sensor_readings/__tests__/table_test.tsx index 33e1abb232..36f139b711 100644 --- a/frontend/sensors/sensor_readings/__tests__/table_test.tsx +++ b/frontend/sensors/sensor_readings/__tests__/table_test.tsx @@ -1,3 +1,7 @@ +jest.mock("../../../api/crud", () => ({ + destroy: jest.fn(), +})); + import React from "react"; import { mount } from "enzyme"; import { SensorReadingsTable } from "../table"; @@ -6,6 +10,7 @@ import { fakeSensorReading, fakeSensor, } from "../../../__test_support__/fake_state/resources"; import { fakeTimeSettings } from "../../../__test_support__/fake_time_settings"; +import { destroy } from "../../../api/crud"; describe("", () => { const fakeProps = (sr = fakeSensorReading()): SensorReadingsTableProps => ({ @@ -14,6 +19,7 @@ describe("", () => { timeSettings: fakeTimeSettings(), hover: jest.fn(), hovered: undefined, + dispatch: jest.fn(), }); it("renders", () => { @@ -68,4 +74,14 @@ describe("", () => { const wrapper = mount(); expect(wrapper.find("tr").last().hasClass("selected")).toEqual(true); }); + + it("deletes reading", () => { + const sr = fakeSensorReading(); + const p = fakeProps(sr); + p.hovered = sr.uuid; + const wrapper = mount(); + expect(wrapper.find("tr").last().hasClass("selected")).toEqual(true); + wrapper.find(".fa-trash").first().simulate("click"); + expect(destroy).toHaveBeenCalledWith(sr.uuid); + }); }); diff --git a/frontend/sensors/sensor_readings/interfaces.ts b/frontend/sensors/sensor_readings/interfaces.ts index 610c08bfa4..b515dcf8c5 100644 --- a/frontend/sensors/sensor_readings/interfaces.ts +++ b/frontend/sensors/sensor_readings/interfaces.ts @@ -34,6 +34,7 @@ export interface SensorReadingsTableProps { /** TaggedSensorReading UUID */ hovered: string | undefined; hover: (hovered: string | undefined) => void; + dispatch: Function; } export interface TableRowProps { @@ -46,6 +47,7 @@ export interface TableRowProps { hover: (hovered: string | undefined) => void; hideLocation?: boolean; distance?: number; + dispatch: Function; } export interface SensorSelectionProps { diff --git a/frontend/sensors/sensor_readings/sensor_readings.tsx b/frontend/sensors/sensor_readings/sensor_readings.tsx index 674779e94d..938e1f04a3 100644 --- a/frontend/sensors/sensor_readings/sensor_readings.tsx +++ b/frontend/sensors/sensor_readings/sensor_readings.tsx @@ -9,11 +9,13 @@ import { } from "./time_period_selection"; import { LocationSelection, LocationDisplay } from "./location_selection"; import { SensorSelection } from "./sensor_selection"; -import { TaggedSensor } from "farmbot"; +import { TaggedSensor, TaggedSensorReading } from "farmbot"; import { AxisInputBoxGroupState } from "../../controls/interfaces"; import { SensorReadingsPlot } from "./graph"; import { Position } from "@blueprintjs/core"; import { AddSensorReadingMenu } from "./add_reading"; +import { destroy } from "../../api/crud"; +import { busy } from "../../toast/toast"; export class SensorReadings extends React.Component { @@ -46,6 +48,15 @@ export class SensorReadings showPreviousPeriod: false, deviation: 0, }); + deleteSelected = (readings: TaggedSensorReading[]) => () => { + if (!confirm(t("Delete {{count}} sensor readings?", { + count: readings.length, + }))) { return; } + busy(t("Deleting {{count}} sensor readings...", { count: readings.length })); + readings.map((reading, index) => { + setTimeout(() => this.props.dispatch(destroy(reading.uuid)), index * 250); + }); + }; toggleAddReadingMenu = () => { this.setState({ addReadingMenuOpen: !this.state.addReadingMenuOpen }); @@ -61,6 +72,11 @@ export class SensorReadings

{t("History")}

+