FarmBot/Farmbot-Web-App

View on GitHub
frontend/farm_designer/map/layers/spread/spread_overlap_helper.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import * as React from "react";
import { SpreadOverlapHelperProps } from "../../interfaces";
import { round, transformXY, defaultSpreadCmDia } from "../../util";
import { BotPosition } from "../../../../devices/interfaces";
import { cachedCrop } from "../../../../open_farm/cached_crop";
import { isUndefined } from "lodash";

enum OverlapColor {
  NONE = "none",
  SOME = "green",
  SMALL = "yellow",
  MEDIUM = "orange",
  LARGE = "red",
}

export enum SpreadOption {
  ActivePlant = "dragged plant",
  InactivePlant = "stationary plants",
  WorseCase = "whichever results in higher % overlap",
  LesserCase = "whichever results in lower % overlap"
}

type SpreadRadii = { inactive: number, active: number };

interface SpreadCircleState {
  inactiveSpread: number | undefined;
}

export function getDiscreteColor(
  overlap: number, spreadRadius: number): OverlapColor {
  // Return overlap severity color based on discrete intervals.
  if (overlap > spreadRadius * 0.9) {
    return OverlapColor.LARGE;
  }
  if (overlap > spreadRadius * 0.6) {
    return OverlapColor.MEDIUM;
  }
  if (overlap > spreadRadius * 0.3) {
    return OverlapColor.SMALL;
  }
  if (overlap > 0) {
    return OverlapColor.SOME;
  }
  return OverlapColor.NONE;
}

export function getContinuousColor(overlap: number, spreadRadius: number) {
  // Smoothly vary color based on overlap: darkgreen > yellow > orange > red.
  if (overlap > 0) {
    const normalized = Math.round(
      Math.max(0, Math.min(spreadRadius, overlap)) / spreadRadius * 255 * 2);
    if (normalized < 255) { // green to yellow
      const r = Math.min(normalized, 255);
      const g = Math.min(100 + normalized, 255); // dark instead of bright green
      const a = Math.min(0.3, Math.round(0.5 * normalized / 510 * 100) / 100);
      return `rgba(${r}, ${g}, 0, ${a})`;
    } else { // yellow to red
      const g = Math.min(255 * 2 - normalized, 255);
      return `rgba(255, ${g}, 0, 0.3)`;
    }
  } else {
    return "none";
  }
}

export function getRadius(option: SpreadOption, data: SpreadRadii): number {
  // Get spread radius to use to evaluate overlap severity.
  switch (option) {
    case SpreadOption.ActivePlant:
      return data.active;
    case SpreadOption.InactivePlant:
      return data.inactive;
    case SpreadOption.WorseCase:
      return Math.min(data.active, data.inactive);
    case SpreadOption.LesserCase:
      return Math.max(data.active, data.inactive);
  }
}

export function getOverlap(
  // Get the overlap of the active and inactive plant spread.
  activeXYZ: BotPosition | undefined,
  plantXYZ: BotPosition,
  spreadData: SpreadRadii,
): number {
  if (activeXYZ && !isUndefined(activeXYZ.x) && !isUndefined(activeXYZ.y)
    && plantXYZ && !isUndefined(plantXYZ.x) && !isUndefined(plantXYZ.y)) {
    // Plant editing (dragging) is occuring
    const activeXY = { x: round(activeXYZ.x), y: round(activeXYZ.y) };
    const distance = Math.sqrt(
      Math.pow((activeXY.x - plantXYZ.x), 2) +
      Math.pow((activeXY.y - plantXYZ.y), 2));
    const overlap = round(Math.abs(Math.min(0,
      distance
      - getRadius(SpreadOption.InactivePlant, spreadData)
      - getRadius(SpreadOption.ActivePlant, spreadData))));
    return overlap;
  }
  return 0;
}

export function overlapText(
  qx: number,
  qy: number,
  overlap: number,
  spreadData: SpreadRadii,
): JSX.Element {
  // Display spread overlap percentages for debugging purposes.
  const activeSpreadDia = spreadData.active * 2;
  const inactiveSpreadDia = spreadData.inactive * 2;
  const percentage = (spread: number) =>
    round(Math.min(100, Math.min(Math.min(activeSpreadDia, inactiveSpreadDia),
      overlap) / spread * 100));
  if (overlap > 0) {
    return <g id="overlap-values">
      <text x={qx} y={qy} dy={-75}>
        {"Active: " + percentage(activeSpreadDia) + "%"}
      </text>
      <text x={qx} y={qy} dy={-50}>
        {"Inactive: " + percentage(inactiveSpreadDia) + "%"}
      </text>
      <text x={qx} y={qy} dy={25}>
        {getDiscreteColor(
          overlap, getRadius(SpreadOption.InactivePlant, spreadData))}
      </text>
    </g>;
  } else {
    return <g />;
  }
}

export class SpreadOverlapHelper extends
  React.Component<SpreadOverlapHelperProps, SpreadCircleState> {
  state: SpreadCircleState = { inactiveSpread: undefined };

  componentDidMount() {
    cachedCrop(this.props.plant.body.openfarm_slug)
      .then(({ spread }) => this.setState({ inactiveSpread: spread }));
  }

  render() {
    const { dragging, plant, activeDragXY, activeDragSpread,
      mapTransformProps } = this.props;
    const { radius, x, y } = plant.body;
    const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
    const gardenCoord: BotPosition = { x: round(x), y: round(y), z: 0 };
    const { inactiveSpread } = this.state;
    // Convert spread diameter in cm to radius in mm.
    const spreadRadii = {
      active: (activeDragSpread || 0) / 2 * 10,
      inactive: (inactiveSpread || defaultSpreadCmDia(radius)) / 2 * 10,
    };

    const overlapValue = getOverlap(activeDragXY, gardenCoord, spreadRadii);
    // Overlap is evaluated against the inactive plant since evaluating
    // against the active plant would require keeping a list of all plants
    // overlapping the active plant. Therefore, the spread overlap helper
    // should be thought of as a tool checking the inactive plants, not
    // the plant being edited. Dragging a plant with a small spread into
    // the area of a plant with large spread will illustrate this point.
    const color = getContinuousColor(
      overlapValue, getRadius(SpreadOption.InactivePlant, spreadRadii));

    return <g id="overlap-indicator">
      {!dragging && // Non-active plants
        <circle
          className="overlap-circle"
          cx={qx}
          cy={qy}
          r={spreadRadii.inactive}
          fill={color} />}
      {this.props.showOverlapValues && !dragging &&
        overlapText(qx, qy, overlapValue, spreadRadii)}
    </g>;
  }
}