frontend/farm_designer/move_to.tsx
import React from "react";
import { Row, Popover } from "../ui";
import { BotPosition } from "../devices/interfaces";
import { move } from "../devices/actions";
import { useNavigate } from "react-router-dom";
import { AxisInputBox } from "../controls/axis_input_box";
import { isNumber, isUndefined, sum } from "lodash";
import { Actions, Content } from "../constants";
import { AxisNumberProperty } from "./map/interfaces";
import { t } from "../i18next_wrapper";
import { SafeZCheckbox } from "../sequences/step_tiles/tile_computed_move/safe_z";
import { Position, Slider } from "@blueprintjs/core";
import { Path } from "../internal_urls";
import { setMovementStateFromPosition } from "../connectivity/log_handlers";
import { Vector3, Xyz } from "farmbot";
import { Link } from "../link";
import {
GetWebAppConfigValue, setWebAppConfigValue,
} from "../config_storage/actions";
import { StringSetting } from "../session_keys";
import { MovementState } from "../interfaces";
import { getUrlQuery } from "../util";
export interface MoveToFormProps {
chosenLocation: BotPosition;
currentBotLocation: BotPosition;
botOnline: boolean;
locked: boolean;
dispatch: Function;
}
interface MoveToFormState {
z: number | undefined;
safeZ: boolean;
speed: number;
}
export class MoveToForm extends React.Component<MoveToFormProps, MoveToFormState> {
state = { z: this.props.chosenLocation.z, safeZ: false, speed: 100 };
get vector(): { x: number, y: number, z: number } {
const { chosenLocation } = this.props;
const newX = chosenLocation.x;
const newY = chosenLocation.y;
const { x, y, z } = this.props.currentBotLocation;
const inputZ = this.state.z;
return {
x: isNumber(newX) ? newX : (x || 0),
y: isNumber(newY) ? newY : (y || 0),
z: isNumber(inputZ) ? inputZ : (z || 0),
};
}
render() {
const { x, y } = this.props.chosenLocation;
const { botOnline, locked } = this.props;
return <div className={"move-to-form"}>
<Row className="move-to-grid">
<label>{t("X AXIS")}</label>
<label>{t("Y AXIS")}</label>
<label>{t("Z AXIS")}</label>
<div />
<input disabled name="x" value={isNumber(x) ? x : "---"} />
<input disabled name="y" value={isNumber(y) ? y : "---"} />
<AxisInputBox
onChange={(_, val: number) => this.setState({ z: val })}
axis={"z"}
value={this.state.z} />
<button
onClick={() => {
this.props.dispatch(setMovementStateFromPosition(
this.props.currentBotLocation, this.vector));
move({
...this.vector,
speed: this.state.speed,
safeZ: this.state.safeZ,
});
}}
className={["fb-button green",
(botOnline && !locked) ? "" : "pseudo-disabled",
].join(" ")}
title={botOnline
? t("Move to this coordinate")
: t(Content.NOT_AVAILABLE_WHEN_OFFLINE)}>
{t("GO")}
</button>
</Row>
<Row className={"speed-grid"}>
<label>{t("Speed")}</label>
<Slider min={1} max={100} labelValues={[1, 50, 100]}
labelRenderer={value => `${value}%`}
value={this.state.speed}
onChange={speed => this.setState({ speed })} />
</Row>
<SafeZCheckbox checked={this.state.safeZ}
onChange={() => this.setState({ safeZ: !this.state.safeZ })} />
</div>;
}
}
export const MoveModeLink = () => {
const navigate = useNavigate();
return <div className="move-to-mode">
<button
className="fb-button gray"
title={t("open move mode panel")}
onClick={() => navigate(Path.location())}>
{t("move mode")}
</button>
</div>;
};
/** Mark a new bot target location on the map. */
export const chooseLocation = (props: {
navigate: (url: string) => void,
gardenCoords: AxisNumberProperty | undefined,
dispatch: Function,
}) => {
if (props.gardenCoords) {
const loc = {
x: Math.max(0, props.gardenCoords.x),
y: Math.max(0, props.gardenCoords.y),
z: 0,
};
props.dispatch(chooseLocationAction(loc));
navigateToLocation(props.navigate, loc);
}
};
type GoButtonAxes = "X" | "Y" | "Z" | "XY" | "XYZ";
const GO_BUTTON_AXES: GoButtonAxes[] = ["X", "Y", "Z", "XY", "XYZ"];
export interface GoToThisLocationButtonProps {
defaultAxes: string;
locationCoordinate: Vector3;
botOnline: boolean;
arduinoBusy: boolean;
dispatch: Function;
currentBotLocation: BotPosition;
movementState: MovementState;
}
interface GoToThisLocationButtonState {
open: boolean;
setAsDefault: boolean;
}
export class GoToThisLocationButton
extends React.Component<GoToThisLocationButtonProps,
GoToThisLocationButtonState> {
state: GoToThisLocationButtonState = { open: false, setAsDefault: false };
toggle = (key: keyof GoToThisLocationButtonState) => () =>
this.setState({ ...this.state, [key]: !this.state[key] });
render() {
const goText = (axes: string) => `${t("GO")} (${axes.split("").join(", ")})`;
const current = this.props.currentBotLocation;
const target = this.props.locationCoordinate;
const { arduinoBusy, botOnline, dispatch, defaultAxes } = this.props;
const unavailableContent = () => {
if (arduinoBusy) { return t("FarmBot is busy"); }
if (!botOnline) { return t("FarmBot is offline"); }
};
const unavailable = arduinoBusy || !botOnline;
const classes = (className: string) => [
className,
"fb-button gray",
unavailable ? "pseudo-disabled" : "",
].join(" ");
const defaultDestination = coordinateFromAxes(target, current, defaultAxes);
const remaining = movementPercentRemaining(current, this.props.movementState);
return <div className={"go-button-axes-wrapper row no-gap"}>
<button
className={classes("go-button-axes-text")}
title={goText(this.props.defaultAxes)}
onMouseEnter={() => dispatch(chooseLocationAction(defaultDestination))}
onMouseLeave={() => dispatch(unChooseLocationAction())}
onClick={() => {
if (unavailable) {
this.toggle("open")();
return;
}
dispatch(setMovementStateFromPosition(current, defaultDestination));
move(defaultDestination);
this.setState({ open: false });
}}>
{remaining && !isNaN(remaining) && arduinoBusy
? <div className={"movement-progress"}
style={{ width: `${remaining}%`, top: 0, left: 0 }} />
: <i />}
<p>{goText(this.props.defaultAxes)}</p>
</button>
<Popover position={Position.BOTTOM_RIGHT}
isOpen={this.state.open}
className={"go-button-axes"}
popoverClassName={"go-button-axes-popover"}
target={<button
className={classes("go-button-axes-dropdown")}
title={t("options")}
onClick={this.toggle("open")}>
<i className={"fa fa-chevron-down"} />
</button>}
content={unavailable
? unavailableContent()
: <div className={"go-axes"}>
{GO_BUTTON_AXES.map(axes => {
const destination = coordinateFromAxes(target, current, axes);
return <button key={axes}
className={`${axes.toLowerCase()} fb-button gray`}
title={goText(axes)}
onMouseEnter={() => dispatch(chooseLocationAction(destination))}
onMouseLeave={() => dispatch(unChooseLocationAction())}
onClick={() => {
if (this.state.setAsDefault) {
dispatch(setWebAppConfigValue(
StringSetting.go_button_axes, axes));
this.setState({ setAsDefault: false });
}
dispatch(setMovementStateFromPosition(current, destination));
move(destination);
this.setState({ open: false });
}}>
{goText(axes)}
</button>;
})}
<div className={"save-as-default-wrapper"}>
<p>{t("Save as default")}</p>
<input type={"checkbox"}
title={t("save as default")}
onChange={this.toggle("setAsDefault")}
checked={this.state.setAsDefault} />
</div>
<Link to={Path.location(target)}>
{t("More options")}
<i className={"fa fa-external-link"} />
</Link>
</div>} />
</div>;
}
}
export const validGoButtonAxes = (getConfigValue: GetWebAppConfigValue) =>
"" + (getConfigValue(StringSetting.go_button_axes) || "XY");
const coordinateFromAxes =
(target: Vector3, current: BotPosition, axes: string) => ({
x: axes.includes("X") ? target.x : (current.x || 0),
y: axes.includes("Y") ? target.y : (current.y || 0),
z: axes.includes("Z") ? target.z : (current.z || 0),
});
export const chooseLocationAction = (target: BotPosition) => ({
type: Actions.CHOOSE_LOCATION,
payload: target,
});
export const unChooseLocationAction = () => ({
type: Actions.CHOOSE_LOCATION,
payload: { x: undefined, y: undefined, z: undefined },
});
const navigateToLocation = (
navigate: (path: string) => void,
location: BotPosition,
) => {
!isUndefined(location.x) &&
Path.getSlug(Path.designer()) === "location" &&
parseFloat("" + getUrlQuery("x")) != location.x &&
parseFloat("" + getUrlQuery("y")) != location.y &&
navigate(Path.location({ x: location.x, y: location.y }));
};
export const movementPercentRemaining =
(botPosition: BotPosition, movementState: MovementState) => {
const { start, distance } = movementState;
const absDistanceArray: number[] = [];
const all = ["x", "y", "z"].map((axis: Xyz) => {
const axisPosition = botPosition[axis];
const axisStart = start[axis];
if (!isNumber(axisPosition) || !isNumber(axisStart) || distance[axis] == 0) {
return 0;
}
const absDistance = Math.abs(distance[axis]);
absDistanceArray.push(absDistance);
const progress = (axisPosition - axisStart) / distance[axis];
return Math.max(Math.min(progress * absDistance, absDistance), 0);
});
return sum(all) / sum(absDistanceArray) * 100;
};