add virtual bot trail setting

pull/737/head
gabrielburnworth 2018-03-19 20:26:09 -07:00
parent 8b6bf0d516
commit fedd40f22d
7 changed files with 83 additions and 24 deletions

View File

@ -3,7 +3,7 @@ import { fetchLabFeatures } from "../labs_features_list_data";
describe("fetchLabFeatures", () => {
it("basically just initializes stuff", () => {
const val = fetchLabFeatures();
expect(val.length).toBe(8);
expect(val.length).toBe(9);
expect(val[0].value).toBeFalsy();
const { callback } = val[0];
if (callback) {

View File

@ -74,6 +74,13 @@ export const fetchLabFeatures = (): LabsFeature[] => ([
value: false,
confirmationMessage: t(Content.DISCARD_UNSAVED_CHANGES_CONFIRM)
},
{
name: t("Display virtual FarmBot trail"),
description: t(Content.VIRTUAL_TRAIL),
storageKey: BooleanSetting.display_trail,
value: false,
callback: () => sessionStorage.virtualTrailRecords = "[]"
},
].map(fetchRealValue));
/** Always allow toggling from true => false (deactivate).

View File

@ -341,6 +341,11 @@ export namespace Content {
trim(`Warning! When enabled, any unsaved changes
will be discarded when refreshing or closing the page. Are you sure?`);
export const VIRTUAL_TRAIL =
trim(`Display a virtual trail for FarmBot in the Farm Designer map to show
movement and watering history while the map is open. Toggling this setting
will clear data for the current trail.`);
// Device
export const NOT_HTTPS =
trim(`WARNING: Sending passwords via HTTP:// is not secure.`);

View File

@ -5,13 +5,17 @@ import { BotTrail, BotTrailProps, VirtualTrail } from "../bot_trail";
describe("<BotTrail/>", () => {
function fakeProps(): BotTrailProps {
sessionStorage[VirtualTrail.records] = JSON.stringify([
{ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 2 }, { x: 3, y: 3 },
{ x: 4, y: 4 }]);
{ coord: { x: 0, y: 0 }, water: 0 },
{ coord: { x: 1, y: 1 }, water: 10 },
{ coord: { x: 2, y: 2 }, water: 0 },
{ coord: { x: 3, y: 3 }, water: 0 },
{ coord: { x: 4, y: 4 }, water: 20 }]);
return {
position: { x: 0, y: 0, z: 0 },
mapTransformProps: {
quadrant: 2, gridSize: { x: 3000, y: 1500 }
}
},
peripherals: []
};
}
@ -19,28 +23,28 @@ describe("<BotTrail/>", () => {
sessionStorage[VirtualTrail.length] = JSON.stringify(5);
const p = fakeProps();
p.mapTransformProps.quadrant = 2;
const wrapper = shallow(<BotTrail {...p } />);
const wrapper = shallow(<BotTrail {...p} />);
const lines = wrapper.find("#trail").find("line");
expect(lines.length).toEqual(4);
expect(lines.first().props()).toEqual({
id: "trail-line-1",
stroke: "red",
strokeOpacity: 0.25,
strokeWidth: 0.5,
strokeWidth: 1.5,
x1: 2, x2: 1, y1: 2, y2: 1
});
expect(lines.last().props()).toEqual({
id: "trail-line-4",
stroke: "red",
strokeOpacity: 1,
strokeWidth: 2,
strokeWidth: 3,
x1: 0, x2: 4, y1: 0, y2: 4
});
});
it("shows default length trail", () => {
sessionStorage[VirtualTrail.length] = undefined;
const wrapper = shallow(<BotTrail {...fakeProps() } />);
const wrapper = shallow(<BotTrail {...fakeProps()} />);
const lines = wrapper.find("#trail").find("line");
expect(lines.length).toEqual(5);
});
@ -49,9 +53,24 @@ describe("<BotTrail/>", () => {
sessionStorage[VirtualTrail.length] = undefined;
const p = fakeProps();
p.position = { x: 4, y: 4, z: 0 };
const wrapper = shallow(<BotTrail {...p } />);
const wrapper = shallow(<BotTrail {...p} />);
const lines = wrapper.find("#trail").find("line");
expect(lines.length).toEqual(4);
});
it("shows water", () => {
const wrapper = shallow(<BotTrail {...fakeProps()} />);
const circles = wrapper.find("#trail").find("circle");
expect(circles.length).toEqual(2);
});
it("updates water circle size", () => {
const p = fakeProps();
p.position = { x: 4, y: 4, z: 0 };
p.peripherals = [{ label: "water", value: true }];
const wrapper = shallow(<BotTrail {...p} />);
const water = wrapper.find("#trail").find("circle").last();
expect(water.props().r).toEqual(21);
});
});

View File

@ -3,20 +3,30 @@ import * as _ from "lodash";
import { MapTransformProps } from "../interfaces";
import { getXYFromQuadrant } from "../util";
import { BotPosition } from "../../../devices/interfaces";
import { Color } from "../../../ui";
type TrailRecord = Record<"x" | "y", number | undefined>;
type TrailRecord = {
coord: Record<"x" | "y", number | undefined>,
water: number | undefined
} | undefined;
export enum VirtualTrail {
records = "virtualTrailRecords",
length = "virtualTrailLength"
}
function getNewTrailArray(update: TrailRecord): TrailRecord[] {
function getNewTrailArray(update: TrailRecord, watering: boolean): TrailRecord[] {
const key = VirtualTrail.records; // sessionStorage location
const trailLength = _.get(sessionStorage, VirtualTrail.length, 100);
const arr = JSON.parse(_.get(sessionStorage, key, "[]")); // get array
const arr: TrailRecord[] = JSON.parse(_.get(sessionStorage, key, "[]"));
if (arr.length > (trailLength - 1)) { arr.shift(); } // max length reached
if (!_.isEqual(_.last(arr), update)) { arr.push(update); } // unique addition
const last = arr[arr.length - 1]; // most recent item in array
if (update && update.coord &&
(!last || !_.isEqual(last.coord, update.coord))) { // coordinate comparison
arr.push(update); // unique addition
} else { // nothing new to add, increase water circle size if watering
if (watering && last && _.isNumber(last.water)) { last.water += 1; }
}
sessionStorage.setItem(key, JSON.stringify(arr)); // save array
return _.takeRight(arr, trailLength);
}
@ -24,23 +34,39 @@ function getNewTrailArray(update: TrailRecord): TrailRecord[] {
export interface BotTrailProps {
position: BotPosition;
mapTransformProps: MapTransformProps;
peripherals: { label: string, value: boolean }[];
}
export function BotTrail(props: BotTrailProps) {
const { quadrant, gridSize } = props.mapTransformProps;
const toQ = (ox: number, oy: number) =>
getXYFromQuadrant(ox, oy, quadrant, gridSize);
const { x, y } = props.position;
const array = getNewTrailArray({ x, y });
const watering = !!_.first(props.peripherals
.filter(p => p.label.toLowerCase().includes("water"))
.map(p => p.value));
const array = getNewTrailArray({ coord: { x, y }, water: 0 }, watering);
return <g id="trail">
{array.map((cur: TrailRecord, i: number) => {
const prev = array[i - 1]; // previous trail coordinate
const opacity = Math.round((i / (array.length - 1)) * 100) / 100;
if (i > 0 && _.isNumber(prev.x) && _.isNumber(prev.y)
&& _.isNumber(cur.x) && _.isNumber(cur.y)) {
const p1 = getXYFromQuadrant(cur.x, cur.y, quadrant, gridSize);
const p2 = getXYFromQuadrant(prev.x, prev.y, quadrant, gridSize);
return <line id={`trail-line-${i}`} key={i}
stroke="red" strokeOpacity={opacity} strokeWidth={opacity * 2}
x1={p1.qx} y1={p1.qy} x2={p2.qx} y2={p2.qy} />;
const prev = (array[i - 1] || { coord: undefined }).coord; // prev coord
const opacity = _.round(Math.max(0.25, i / (array.length - 1)), 2);
if (i > 0 && cur && prev && _.isNumber(prev.x) && _.isNumber(prev.y)
&& _.isNumber(cur.coord.x) && _.isNumber(cur.coord.y)
&& _.isNumber(cur.water)) {
const p1 = toQ(cur.coord.x, cur.coord.y);
const p2 = toQ(prev.x, prev.y);
return <g key={i}>
<line id={`trail-line-${i}`}
stroke="red" strokeOpacity={opacity} strokeWidth={1 + opacity * 2}
x1={p1.qx} y1={p1.qy} x2={p2.qx} y2={p2.qy} />
{cur.water &&
<circle id={`trail-water-${i}`}
fill={Color.blue} opacity={opacity / 2}
cx={p1.qx} cy={p1.qy} r={cur.water} />}
</g>;
}
})}
</g>;

View File

@ -32,6 +32,7 @@ export function VirtualFarmBot(props: VirtualFarmBotProps) {
{displayTrail &&
<BotTrail
position={props.botLocationData.position}
mapTransformProps={mapTransformProps} />}
mapTransformProps={mapTransformProps}
peripherals={peripherals} />}
</g>;
}

View File

@ -14,4 +14,5 @@ export enum Color {
soilCloud = "#90612f",
black = "#000000",
orange = "#ffa500",
blue = "#3377dd",
}