189 lines
6.0 KiB
TypeScript
189 lines
6.0 KiB
TypeScript
import * as React from "react";
|
|
import { Xyz, LocationName, Dictionary } from "farmbot";
|
|
import moment from "moment";
|
|
import { BotLocationData, BotPosition } from "../../devices/interfaces";
|
|
import { trim } from "../../util";
|
|
import {
|
|
cloneDeep, max, get, isNumber, isEqual, takeRight, ceil, range
|
|
} from "lodash";
|
|
import { t } from "../../i18next_wrapper";
|
|
|
|
const HEIGHT = 50;
|
|
const HISTORY_LENGTH_SECONDS = 120;
|
|
const BORDER_WIDTH = 15;
|
|
const BORDERS = BORDER_WIDTH * 2;
|
|
const MAX_X = HISTORY_LENGTH_SECONDS;
|
|
const DEFAULT_Y_MAX = 100;
|
|
|
|
const COLOR_LOOKUP: Dictionary<string> = {
|
|
x: "red", y: "green", z: "blue"
|
|
};
|
|
const LINEWIDTH_LOOKUP: Dictionary<number> = {
|
|
position: 0.5, scaled_encoders: 0.25
|
|
};
|
|
|
|
export enum MotorPositionHistory {
|
|
array = "motorPositionHistoryArray",
|
|
}
|
|
|
|
type Entry = {
|
|
timestamp: number,
|
|
locationData: Record<LocationName, BotPosition>
|
|
};
|
|
|
|
type Paths = Record<LocationName, Record<Xyz, string>>;
|
|
|
|
const getArray = (): Entry[] =>
|
|
JSON.parse(get(sessionStorage, MotorPositionHistory.array, "[]"));
|
|
|
|
const getReversedArray = (): Entry[] => cloneDeep(getArray()).reverse();
|
|
|
|
const getLastEntry = (): Entry | undefined => {
|
|
const array = getArray();
|
|
return array[array.length - 1];
|
|
};
|
|
|
|
const findYLimit = (): number => {
|
|
const array = getArray();
|
|
const arrayAbsMax = max(array.map(entry =>
|
|
max(["position", "scaled_encoders"].map((name: LocationName) =>
|
|
max(["x", "y", "z"].map((axis: Xyz) =>
|
|
Math.abs(entry.locationData[name][axis] || 0) + 1))))));
|
|
return Math.max(ceil(arrayAbsMax || 0, -2), DEFAULT_Y_MAX);
|
|
};
|
|
|
|
const updateArray = (update: Entry): Entry[] => {
|
|
const arr = getArray();
|
|
const last = getLastEntry();
|
|
if (update && isNumber(update.locationData.position.x) &&
|
|
(!last || !isEqual(last.timestamp, update.timestamp))) {
|
|
arr.push(update);
|
|
}
|
|
const newArray = takeRight(arr, 100)
|
|
.filter(x => {
|
|
const entryAge = (last ? last.timestamp : moment().unix()) - x.timestamp;
|
|
return entryAge <= HISTORY_LENGTH_SECONDS;
|
|
});
|
|
sessionStorage.setItem(MotorPositionHistory.array, JSON.stringify(newArray));
|
|
return newArray;
|
|
};
|
|
|
|
const newPaths = (): Paths => ({
|
|
position: { x: "", y: "", z: "" },
|
|
scaled_encoders: { x: "", y: "", z: "" },
|
|
raw_encoders: { x: "", y: "", z: "" }
|
|
});
|
|
|
|
const getPaths = (): Paths => {
|
|
const last = getLastEntry();
|
|
const maxY = findYLimit();
|
|
const paths = newPaths();
|
|
if (last) {
|
|
getReversedArray().map(entry => {
|
|
["position", "scaled_encoders"].map((name: LocationName) => {
|
|
["x", "y", "z"].map((axis: Xyz) => {
|
|
const lastPos = last.locationData[name][axis];
|
|
const pos = entry.locationData[name][axis];
|
|
if (isNumber(lastPos) && isFinite(lastPos)
|
|
&& isNumber(maxY) && isNumber(pos)) {
|
|
if (!paths[name][axis].startsWith("M")) {
|
|
const yStart = -lastPos / maxY * HEIGHT / 2;
|
|
paths[name][axis] = `M ${MAX_X},${yStart} `;
|
|
}
|
|
const x = MAX_X - (last.timestamp - entry.timestamp);
|
|
const y = -pos / maxY * HEIGHT / 2;
|
|
paths[name][axis] += `L ${x},${y} `;
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
return paths;
|
|
};
|
|
|
|
const TitleLegend = () => {
|
|
const titleY = -(HEIGHT + BORDER_WIDTH) / 2;
|
|
const legendX = HISTORY_LENGTH_SECONDS / 4;
|
|
return <g id="title_with_legend">
|
|
<text fill={COLOR_LOOKUP.x} fontWeight={"bold"}
|
|
x={legendX - 10} y={titleY}>{"X"}</text>
|
|
<text fill={COLOR_LOOKUP.y} fontWeight={"bold"}
|
|
x={legendX} y={titleY}>{"Y"}</text>
|
|
<text fill={COLOR_LOOKUP.z} fontWeight={"bold"}
|
|
x={legendX + 10} y={titleY}>{"Z"}</text>
|
|
<text fontWeight={"bold"}
|
|
x={HISTORY_LENGTH_SECONDS / 2} y={titleY}>{t("Position (mm)")}</text>
|
|
</g>;
|
|
};
|
|
|
|
const YAxisLabels = () => {
|
|
const maxY = findYLimit();
|
|
return <g id="y_axis_labels">
|
|
{[maxY, maxY / 2, 0, -maxY / 2, -maxY].map(yPosition =>
|
|
<g key={"y_axis_label_" + yPosition}>
|
|
<text x={MAX_X + BORDER_WIDTH / 2} y={-yPosition / maxY * HEIGHT / 2}>
|
|
{yPosition}
|
|
</text>
|
|
<text x={-BORDER_WIDTH / 2} y={-yPosition / maxY * HEIGHT / 2}>
|
|
{yPosition}
|
|
</text>
|
|
</g>)}
|
|
</g>;
|
|
};
|
|
|
|
const XAxisLabels = () =>
|
|
<g id="x_axis_labels">
|
|
<text x={HISTORY_LENGTH_SECONDS / 2} y={HEIGHT / 2 + BORDER_WIDTH / 1.25}
|
|
fontStyle={"italic"}>
|
|
{t("seconds ago")}
|
|
</text>
|
|
{range(0, HISTORY_LENGTH_SECONDS + 1, 20).map(secondsAgo =>
|
|
<text key={"x_axis_label_" + secondsAgo}
|
|
x={MAX_X - secondsAgo} y={HEIGHT / 2 + BORDER_WIDTH / 3}>
|
|
{secondsAgo}
|
|
</text>)}
|
|
</g>;
|
|
|
|
const PlotBackground = () =>
|
|
<g id="plot_background">
|
|
<rect fill="white" x={0} y={-HEIGHT / 2} width={"100%"} height={"100%"} />
|
|
<line x1={0} y1={0} x2={MAX_X} y2={0} strokeWidth={0.25} stroke={"grey"} />
|
|
</g>;
|
|
|
|
const PlotLines = ({ locationData }: { locationData: BotLocationData }) => {
|
|
updateArray({ timestamp: moment().unix(), locationData });
|
|
const paths = getPaths();
|
|
return <g id="plot_lines">
|
|
{["position", "scaled_encoders"].map((name: LocationName) =>
|
|
["x", "y", "z"].map((axis: Xyz) =>
|
|
<path key={name + axis} fill={"none"}
|
|
stroke={COLOR_LOOKUP[axis]} strokeWidth={LINEWIDTH_LOOKUP[name]}
|
|
strokeLinecap={"round"} strokeLinejoin={"round"}
|
|
d={paths[name][axis]} />))}
|
|
</g>;
|
|
};
|
|
|
|
export const MotorPositionPlot = (props: { locationData: BotLocationData }) => {
|
|
return <svg
|
|
className="motor-position-plot-border"
|
|
style={{ marginTop: "2rem", maxHeight: "250px" }}
|
|
width="100%"
|
|
height="100%"
|
|
viewBox={trim(`${-BORDER_WIDTH} ${-HEIGHT / 2 - BORDER_WIDTH}
|
|
${HISTORY_LENGTH_SECONDS + BORDERS} ${HEIGHT + BORDERS}`)}>
|
|
<TitleLegend />
|
|
<YAxisLabels />
|
|
<XAxisLabels />
|
|
<svg
|
|
className="motor-position-plot"
|
|
width={HISTORY_LENGTH_SECONDS}
|
|
height={HEIGHT}
|
|
x={0}
|
|
y={-HEIGHT / 2}
|
|
viewBox={`0 ${-HEIGHT / 2} ${HISTORY_LENGTH_SECONDS} ${HEIGHT}`}>
|
|
<PlotBackground />
|
|
<PlotLines locationData={props.locationData} />
|
|
</svg>
|
|
</svg>;
|
|
};
|