FarmBot/Farmbot-Web-App

View on GitHub
frontend/point_groups/paths.tsx

Summary

Maintainability
A
35 mins
Test Coverage
import React from "react";
import { sortGroupBy, sortOptionsTable } from "./point_group_sort";
import { isUndefined, sortBy, uniq } from "lodash";
import { PointGroupSortType } from "farmbot/dist/resources/api_resources";
import { Actions } from "../constants";
import { edit, save } from "../api/crud";
import { TaggedPointGroup, TaggedPoint, TaggedSensorReading } from "farmbot";

export const convertToXY =
  (points: (TaggedPoint | TaggedSensorReading)[]): { x: number, y: number }[] =>
    points
      .map(p => ({ x: p.body.x, y: p.body.y }))
      .filter(p => !isUndefined(p.x) && !isUndefined(p.y))
      .map(p => p as { x: number, y: number });

export const distance = (
  p1: { x: number, y: number },
  p2: { x: number, y: number },
) =>
  Math.pow(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2), 0.5);

const pathDistance = (pathPoints: (TaggedPoint | TaggedSensorReading)[]) => {
  let total = 0;
  let prev: { x: number, y: number } | undefined = undefined;
  convertToXY(pathPoints)
    .map(p => {
      prev ? total += distance(p, prev) : 0;
      prev = p;
    });
  return Math.round(total);
};

export const findNearest = (
  from: { x: number, y: number },
  available: (TaggedPoint | TaggedSensorReading)[],
): TaggedPoint | TaggedSensorReading | undefined => {
  const distances = available
    .filter(p => !isUndefined(p.body.x) && !isUndefined(p.body.y))
    .map(p => ({
      point: p,
      distance: distance({ x: p.body.x as number, y: p.body.y as number }, from)
    }));
  return sortBy(distances, "distance")[0]?.point;
};

export const nn = (pathPoints: TaggedPoint[]) => {
  let available = pathPoints.slice(0);
  const ordered: (TaggedPoint | TaggedSensorReading)[] = [];
  let from = { x: 0, y: 0 };
  pathPoints.map(() => {
    const nearest = findNearest(from, available);
    if (!nearest || isUndefined(nearest.body.x) || isUndefined(nearest.body.y)) {
      return;
    }
    ordered.push(nearest);
    from = { x: nearest.body.x, y: nearest.body.y };
    available = available.filter(p => p.uuid !== nearest.uuid);
  });
  return ordered;
};

export const alternating = (pathPoints: TaggedPoint[], axis: "xy" | "yx") => {
  const axis0: "x" | "y" = axis[0] as "x" | "y";
  const axis1: "x" | "y" = axis[1] as "x" | "y";
  const ordered: TaggedPoint[] = [];
  const rowCoordinates = sortBy(uniq(pathPoints.map(p => p.body[axis0])));
  const rows = rowCoordinates.map((rowCoordinate, index) => {
    const row = sortBy(pathPoints.filter(p =>
      p.body[axis0] == rowCoordinate), "body." + axis1);
    return index % 2 == 0 ? row : row.reverse();
  });
  rows.map(row => row.map(p => ordered.push(p)));
  return ordered;
};

export type ExtendedPointGroupSortType = PointGroupSortType;

const SORT_TYPES: ExtendedPointGroupSortType[] = [
  "random", "yx_descending", "yx_ascending", "xy_descending", "xy_ascending"];

export interface PathInfoBarProps {
  sortTypeKey: ExtendedPointGroupSortType;
  dispatch: Function;
  group: TaggedPointGroup;
  pathData: { [key: string]: number };
}

export const PathInfoBar = (props: PathInfoBarProps) => {
  const { sortTypeKey, dispatch, group } = props;
  const pathLength = props.pathData[sortTypeKey];
  const maxLength = Math.max(...Object.values(props.pathData));
  const normalizedLength = pathLength / maxLength * 100;
  const sortLabel = () => {
    switch (sortTypeKey) {
      default: return sortOptionsTable()[sortTypeKey];
    }
  };
  const selected = group.body.sort_type == sortTypeKey;
  return <div className={`sort-option-bar ${selected ? "selected" : ""}`}
    onMouseEnter={() =>
      dispatch({ type: Actions.TRY_SORT_TYPE, payload: sortTypeKey })}
    onMouseLeave={() =>
      dispatch({ type: Actions.TRY_SORT_TYPE, payload: undefined })}
    onClick={() => {
      dispatch(edit(group, { sort_type: sortTypeKey }));
      dispatch(save(group.uuid));
    }}>
    <div className={"sort-path-info-bar"}
      style={{ width: `${normalizedLength}%` }}>
      {`${sortLabel()}: ${Math.round(pathLength / 10) / 100}m`}
    </div>
  </div>;
};

export interface PathsProps {
  pathPoints: TaggedPoint[];
  dispatch: Function;
  group: TaggedPointGroup;
}

interface PathsState {
  pathData: Record<string, number>;
}

export class Paths extends React.Component<PathsProps, PathsState> {
  state: PathsState = { pathData: {} };

  generatePathData = (pathPoints: TaggedPoint[]) => {
    const newPathData: Record<string, number> = {};
    SORT_TYPES.map((sortType: PointGroupSortType) =>
      newPathData[sortType] = pathDistance(sortGroupBy(sortType, pathPoints)));
    newPathData.xy_alternating = pathDistance(alternating(pathPoints, "xy"));
    newPathData.yx_alternating = pathDistance(alternating(pathPoints, "yx"));
    newPathData.nn = pathDistance(nn(pathPoints));
    this.setState({ pathData: newPathData });
  };

  componentDidMount = () => this.generatePathData(this.props.pathPoints);

  render() {
    return <div className={"group-sort-types"}>
      {SORT_TYPES.concat(["yx_alternating", "xy_alternating", "nn"])
        .reverse()
        .map(sortType =>
          <PathInfoBar key={sortType}
            sortTypeKey={sortType}
            dispatch={this.props.dispatch}
            group={this.props.group}
            pathData={this.state.pathData} />)}
    </div>;
  }
}