FarmBot/Farmbot-Web-App

View on GitHub
frontend/farm_designer/map/sequence_visualization.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
import React from "react";
import { Actions } from "../../constants";
import { UUID } from "../../resources/interfaces";
import {
  SequenceBodyItem, LegalSequenceKind,
  MoveAbsolute, Home, FindHome, Calibrate, Zero,
} from "farmbot";
import { MapTransformProps } from "./interfaces";
import { transformXY } from "./util";
import { Color } from "../../ui";
import { BotPosition } from "../../devices/interfaces";
import { zoomCompensation } from "./zoom";
import {
  findPointerByTypeAndId, findSlotByToolId,
} from "../../resources/selectors";
import { store } from "../../redux/store";
import { findVariableByName } from "../../resources/sequence_meta";
import { getStepTag } from "../../resources/sequence_tagging";
import {
  computeCoordinate,
} from "../../sequences/step_tiles/tile_computed_move/compute";
import { FilePath, Icon, Path } from "../../internal_urls";

const ICON_LOOKUP: Partial<Record<LegalSequenceKind, Icon>> = {
  // _if: Icon.settings,
  // assertion: Icon.settings,
  // calibrate: Icon.settings,
  change_ownership: Icon.settings,
  check_updates: Icon.settings,
  // emergency_lock: Icon.settings,
  // emergency_unlock: Icon.settings,
  // execute: Icon.settings,
  execute_script: Icon.farmware,
  factory_reset: Icon.settings,
  // find_home: Icon.settings,
  flash_firmware: Icon.settings,
  // home: Icon.settings,
  install_farmware: Icon.farmware,
  install_first_party_farmware: Icon.farmware,
  move: Icon.controls,
  move_absolute: Icon.controls,
  // move_relative: Icon.settings,
  power_off: Icon.settings,
  read_pin: Icon.sensors,
  read_status: Icon.settings,
  reboot: Icon.settings,
  remove_farmware: Icon.farmware,
  // send_message: Icon.settings,
  // set_servo_angle: Icon.settings,
  set_user_env: Icon.settings,
  sync: Icon.settings,
  take_photo: Icon.photos,
  // toggle_pin: Icon.settings,
  update_farmware: Icon.farmware,
  update_resource: Icon.settings,
  wait: Icon.calendar,
  // write_pin: Icon.settings,
  // zero: Icon.settings,

};
type Position = { x: number, y: number, icon?: Icon, uuid?: string };

export const visualizeInMap = (sequenceUuid: UUID | undefined) => ({
  type: Actions.VISUALIZE_SEQUENCE,
  payload: sequenceUuid,
});

export interface SequenceVisualizationProps {
  visualizedSequenceUUID: UUID | undefined;
  visualizedSequenceBody: SequenceBodyItem[];
  hoveredSequenceStep: string | undefined;
  mapTransformProps: MapTransformProps;
  botPosition: BotPosition;
  zoomLvl: number;
  dispatch: Function;
}

/**
 * Sequence visualizer.
 *
 * Display a visualization of Sequence step movements and actions in the map.
 * Can be toggled on/off and updates in realtime when sequence steps are
 * modified or the current bot position is changed. Hover a step to view the
 * corresponding visual or vice versa.
 *
 * | movement  | icon  | other | Sequence step
 * |-----------|-------|-------|---------------
 * |           |       |   x   | _if
 * |           |       |   x   | assertion
 * |     x     |       |       | calibrate
 * |           |   x   |       | change_ownership
 * |           |   x   |       | check_updates
 * |           |   _   |       | emergency_lock
 * |           |   _   |       | emergency_unlock
 * |           |       |   x   | execute
 * |           |   x   |       | execute_script
 * |           |   x   |       | factory_reset
 * |     x     |       |       | find_home
 * |           |   x   |       | flash_firmware
 * |     x     |       |       | home
 * |           |   x   |       | install_farmware
 * |           |   x   |       | install_first_party_farmware
 * |     x     |   x   |       | move
 * |     x     |   x   |       | move_absolute
 * |     x     |       |       | move_relative
 * |           |   x   |       | power_off
 * |           |   x   |       | read_pin
 * |           |   x   |       | read_status
 * |           |   x   |       | reboot
 * |           |   x   |       | remove_farmware
 * |           |   _   |       | send_message
 * |           |   _   |       | set_servo_angle
 * |           |   x   |       | set_user_env
 * |           |   x   |       | sync
 * |           |   x   |       | take_photo
 * |           |   _   |       | toggle_pin
 * |           |   x   |       | update_farmware
 * |           |   x   |       | update_resource
 * |           |   x   |       | wait
 * |           |   _   |       | write_pin
 * |     x     |       |       | zero
 *
 * Options:
 *  * Guided step-through with sensor value input and pin state display
 *  * Non-default external variable support
 */
export const SequenceVisualization = (props: SequenceVisualizationProps) => {
  const { visualizedSequenceBody, botPosition, visualizedSequenceUUID } = props;
  const positions = preparePositions(
    visualizedSequenceBody, botPosition, visualizedSequenceUUID);
  const travels = preparePositionPairs(positions);
  const { mapTransformProps, zoomLvl, hoveredSequenceStep, dispatch } = props;
  const commonProps = {
    mapTransformProps, zoomLvl, hoveredSequenceStep, dispatch
  };
  const point = addLocation(commonProps);
  const line = addLine(commonProps);
  const icon = addIcon(commonProps);
  return <g id={"visualized-sequence"}>
    {travels.map(({ start, end }, index) =>
      <g id={`segment-${index}`} key={index}>
        {line(start, end)}
        {point(end)}
        {icon(start)}
        {icon(end)}
      </g>)}
  </g>;
};

export const hoverSequenceStep =
  (uuid: string | undefined) => (dispatch: Function) => () => Path.inDesigner() &&
    dispatch({
      type: Actions.HOVER_SEQUENCE_STEP,
      payload: uuid,
    });

interface AddSVGElementProps {
  mapTransformProps: MapTransformProps;
  zoomLvl: number;
  hoveredSequenceStep: string | undefined;
  dispatch: Function;
}

const addLocation = (props: AddSVGElementProps) =>
  (location: Position) => {
    const { x, y } = location;
    const { qx, qy } = transformXY(x, y, props.mapTransformProps);
    const { hoveredSequenceStep } = props;
    const hovered = hoveredSequenceStep && hoveredSequenceStep == location.uuid;
    return <circle
      onMouseEnter={props.dispatch(hoverSequenceStep(location.uuid))}
      onMouseLeave={props.dispatch(hoverSequenceStep(undefined))}
      cx={qx} cy={qy}
      r={zoomCompensation(props.zoomLvl, 10)}
      fill={Color.darkOrange}
      fillOpacity={hovered ? 1 : 0.5} />;
  };

const addLine = (props: AddSVGElementProps) =>
  (start: Position, end: Position) => {
    const { mapTransformProps, zoomLvl } = props;
    const transformedStart = transformXY(start.x, start.y, mapTransformProps);
    const transformedEnd = transformXY(end.x, end.y, mapTransformProps);
    const { hoveredSequenceStep } = props;
    const hovered = hoveredSequenceStep && hoveredSequenceStep == end.uuid;
    return <line
      onMouseEnter={props.dispatch(hoverSequenceStep(end.uuid))}
      onMouseLeave={props.dispatch(hoverSequenceStep(undefined))}
      x1={transformedStart.qx} y1={transformedStart.qy}
      x2={transformedEnd.qx} y2={transformedEnd.qy}
      stroke={Color.darkOrange}
      strokeWidth={zoomCompensation(zoomLvl, hovered ? 8 : 5)}
      strokeOpacity={hovered ? 1 : 0.5} />;
  };

const addIcon = (props: AddSVGElementProps) =>
  (location: Position) => {
    const { x, y } = location;
    const { qx, qy } = transformXY(x, y, props.mapTransformProps);
    const size = zoomCompensation(props.zoomLvl, 25);
    const { hoveredSequenceStep } = props;
    const hovered = hoveredSequenceStep && hoveredSequenceStep == location.uuid;
    return location.icon
      ? <image
        onMouseEnter={props.dispatch(hoverSequenceStep(location.uuid))}
        onMouseLeave={props.dispatch(hoverSequenceStep(undefined))}
        x={qx - size / 2} y={qy - size / 2}
        width={size} height={size}
        xlinkHref={FilePath.icon(location.icon)}
        opacity={hovered ? 1 : 0.5} />
      : <g />;
  };

/** Generate a list of positions dictated by Sequence body steps. */
const preparePositions = (
  sequenceBody: SequenceBodyItem[],
  initialPosition: BotPosition,
  sequenceUuid: UUID | undefined,
) => {
  const positions: Position[] = [];
  positions.push({
    x: initialPosition.x || 0,
    y: initialPosition.y || 0,
  });
  sequenceBody.map(step => {
    const previous = positions[positions.length - 1];
    switch (step.kind) {
      case "move":
        const moveCoordinate = computeCoordinate({
          step,
          botPosition: { x: previous.x, y: previous.y, z: undefined },
          resourceIndex: store.getState().resources.index,
          sequenceUuid,
        });
        positions.push(moveCoordinate);
        break;
      case "move_absolute":
        const moveAbsoluteCoordinate = reduceMoveAbsolute(step, sequenceUuid);
        if (moveAbsoluteCoordinate) { positions.push(moveAbsoluteCoordinate); }
        break;
      case "move_relative":
        const relative = step.args;
        positions.push({
          x: previous.x + relative.x,
          y: previous.y + relative.y,
        });
        break;
      case "home":
      case "find_home":
      case "zero":
      case "calibrate":
        const homingCoordinate = reduceHomingStep(step, previous);
        if (homingCoordinate) { positions.push(homingCoordinate); }
        break;
    }
    positions[positions.length - 1].icon = ICON_LOOKUP[step.kind];
    positions[positions.length - 1].uuid = getStepTag(step);
  });
  return positions;
};

const reduceMoveAbsolute = (
  step: MoveAbsolute,
  sequenceUuid: UUID | undefined,
): Position | undefined => {
  const offset = step.args.offset.args;
  const ri = store.getState().resources.index;
  switch (step.args.location.kind) {
    case "coordinate":
      const coordinate = step.args.location.args;
      return {
        x: coordinate.x + offset.x,
        y: coordinate.y + offset.y,
      };
    case "point":
      const { pointer_id } = step.args.location.args;
      const point = findPointerByTypeAndId(ri, "Point", pointer_id);
      return {
        x: point.body.x + offset.x,
        y: point.body.y + offset.y,
      };
    case "tool":
      const { tool_id } = step.args.location.args;
      const toolSlot = findSlotByToolId(ri, tool_id);
      if (!toolSlot) { return; }
      return {
        x: toolSlot.body.x + offset.x,
        y: toolSlot.body.y + offset.y,
      };
    case "identifier":
      const { label } = step.args.location.args;
      if (!sequenceUuid) { return; }
      const variable = findVariableByName(ri, sequenceUuid, label);
      if (!(variable && variable.vector)) { return; }
      return {
        x: variable.vector.x + offset.x,
        y: variable.vector.y + offset.y,
      };
  }
};

const reduceHomingStep = (
  step: Home | FindHome | Calibrate | Zero,
  prevPosition: Position,
): Position | undefined => {
  switch (step.args.axis) {
    case "x":
      return { x: 0, y: prevPosition.y };
    case "y":
      return { x: prevPosition.x, y: 0 };
    case "all":
      return { x: 0, y: 0 };
  }
};

/** Group a list of positions into position pairs. */
const preparePositionPairs = (positions: Position[]) => {
  const pairs:
    { start: Position, end: Position }[] =
    [];
  positions.map((position, index) => {
    if (index > 0) {
      const previous = positions[index - 1];
      pairs.push({ start: previous, end: position });
    }
  });
  return pairs;
};