refactor map transform calculations

pull/786/head
gabrielburnworth 2018-04-11 19:05:08 -07:00
parent 471b4b9b74
commit 27129844c9
22 changed files with 248 additions and 167 deletions

View File

@ -11,7 +11,7 @@
.farm-designer-map {
min-width: 100%;
display: inline-block;
padding: 11rem 2rem 2rem 31.8rem;
padding: 11rem 2rem 2rem 31.8rem; // at zoom = 1.0: 110px 20px 20px 318px
height: 100%;
overflow: auto;
}

View File

@ -20,6 +20,13 @@ import { SelectionBoxData } from "./map/selection_box";
import { BooleanConfigKey } from "../config_storage/web_app_configs";
import { GetWebAppConfigValue } from "../config_storage/actions";
/* BotOriginQuadrant diagram
2 --- 1
| |
3 --- 4
*/
export enum BotOriginQuadrant { ONE = 1, TWO = 2, THREE = 3, FOUR = 4 }
type Mystery = BotOriginQuadrant | number | undefined;

View File

@ -3,11 +3,11 @@ import {
translateScreenToGarden,
getBotSize,
getMapSize,
getXYFromQuadrant,
transformXY,
transformForQuadrant
} from "../util";
import { McuParams } from "farmbot";
import { AxisNumberProperty, BotSize } from "../interfaces";
import { AxisNumberProperty, BotSize, MapTransformProps } from "../interfaces";
import { StepsPerMmXY } from "../../../devices/interfaces";
describe("Utils", () => {
@ -20,11 +20,11 @@ describe("Utils", () => {
describe("translateScreenToGarden()", () => {
it("translates garden coords to screen coords: corner case", () => {
const cornerCase = translateScreenToGarden({
quadrant: 2,
pageX: 520,
pageY: 212,
mapTransformProps: { quadrant: 2, gridSize: { x: 3000, y: 1500 } },
page: { x: 520, y: 212 },
scroll: { left: 0, top: 0 },
zoomLvl: 1,
gridSize: { x: 3000, y: 1500 }
gridOffset: { x: 0, y: 0 },
});
expect(cornerCase.x).toEqual(200);
expect(cornerCase.y).toEqual(100);
@ -32,11 +32,11 @@ describe("translateScreenToGarden()", () => {
it("translates garden coords to screen coords: edge case", () => {
const edgeCase = translateScreenToGarden({
quadrant: 2,
pageX: 1132,
pageY: 382,
mapTransformProps: { quadrant: 2, gridSize: { x: 3000, y: 1500 } },
page: { x: 1132, y: 382 },
scroll: { left: 0, top: 0 },
zoomLvl: 0.3,
gridSize: { x: 3000, y: 1500 }
gridOffset: { x: 0, y: 0 },
});
expect(Math.round(edgeCase.x)).toEqual(2710);
@ -153,44 +153,59 @@ describe("getMapSize()", () => {
});
});
describe("getXYFromQuadrant()", () => {
describe("transformXY", () => {
const gridSize = { x: 2000, y: 1000 };
type QXY = { qx: number, qy: number };
const transformCheck =
(original: QXY, transformed: QXY, transformProps: MapTransformProps) => {
expect(transformXY(original.qx, original.qy, transformProps))
.toEqual(transformed);
expect(transformXY(transformed.qx, transformed.qy, transformProps))
.toEqual(original);
};
it("calculates transformed coordinate: quadrant 2", () => {
const { qx, qy } = getXYFromQuadrant(100, 200, 2, { x: 2000, y: 1000 });
expect(qx).toEqual(100);
expect(qy).toEqual(200);
const original = { qx: 100, qy: 200 };
const transformed = { qx: 100, qy: 200 };
const transformProps = { quadrant: 2, gridSize };
transformCheck(original, transformed, transformProps);
});
it("calculates transformed coordinate: quadrant 4", () => {
const { qx, qy } = getXYFromQuadrant(100, 200, 4, { x: 2000, y: 1000 });
expect(qx).toEqual(1900);
expect(qy).toEqual(800);
const original = { qx: 100, qy: 200 };
const transformed = { qx: 1900, qy: 800 };
const transformProps = { quadrant: 4, gridSize };
transformCheck(original, transformed, transformProps);
});
it("calculates transformed coordinate: quadrant 4 (outside of grid)", () => {
const { qx, qy } = getXYFromQuadrant(2200, 1100, 4, { x: 2000, y: 1000 });
expect(qx).toEqual(-200);
expect(qy).toEqual(-100);
const original = { qx: 2200, qy: 1100 };
const transformed = { qx: -200, qy: -100 };
const transformProps = { quadrant: 4, gridSize };
transformCheck(original, transformed, transformProps);
});
});
describe("transformForQuadrant()", () => {
it("calculates transform for quadrant 1", () => {
expect(transformForQuadrant(1, { x: 200, y: 100 }))
expect(transformForQuadrant({ quadrant: 1, gridSize: { x: 200, y: 100 } }))
.toEqual("scale(-1, 1) translate(-200, 0)");
});
it("calculates transform for quadrant 2", () => {
expect(transformForQuadrant(2, { x: 200, y: 100 }))
expect(transformForQuadrant({ quadrant: 2, gridSize: { x: 200, y: 100 } }))
.toEqual("scale(1, 1) translate(0, 0)");
});
it("calculates transform for quadrant 3", () => {
expect(transformForQuadrant(3, { x: 200, y: 100 }))
expect(transformForQuadrant({ quadrant: 3, gridSize: { x: 200, y: 100 } }))
.toEqual("scale(1, -1) translate(0, -100)");
});
it("calculates transform for quadrant 4", () => {
expect(transformForQuadrant(4, { x: 200, y: 100 }))
expect(transformForQuadrant({ quadrant: 4, gridSize: { x: 200, y: 100 } }))
.toEqual("scale(-1, -1) translate(-200, -100)");
});
});

View File

@ -1,13 +1,12 @@
import * as React from "react";
import { getXYFromQuadrant } from "./util";
import { transformXY } from "./util";
import { BotExtentsProps } from "./interfaces";
export function BotExtents(props: BotExtentsProps) {
const { stopAtHome, botSize, mapTransformProps } = props;
const { quadrant, gridSize } = mapTransformProps;
const homeLength = getXYFromQuadrant(
botSize.x.value, botSize.y.value, quadrant, gridSize);
const homeZero = getXYFromQuadrant(2, 2, quadrant, gridSize);
const homeLength = transformXY(
botSize.x.value, botSize.y.value, mapTransformProps);
const homeZero = transformXY(2, 2, mapTransformProps);
return <g
id="extents"

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { DragHelpersProps } from "./interfaces";
import { round, getXYFromQuadrant, getMapSize } from "./util";
import { round, transformXY, getMapSize } from "./util";
import { isUndefined } from "util";
import { BotPosition } from "../../devices/interfaces";
import { Color } from "../../ui/index";
@ -54,14 +54,13 @@ export function DragHelpers(props: DragHelpersProps) {
const {
dragging, plant, zoomLvl, activeDragXY, mapTransformProps, plantAreaOffset
} = props;
const { quadrant, gridSize } = mapTransformProps;
const mapSize = getMapSize(gridSize, plantAreaOffset);
} = props;
const mapSize = getMapSize(mapTransformProps.gridSize, plantAreaOffset);
const { radius, x, y } = plant.body;
const scale = 1 + Math.round(15 * (1.8 - zoomLvl)) / 10; // scale factor
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
const gardenCoord: BotPosition = { x: round(x), y: round(y), z: 0 };
return <g id="drag-helpers" fill={Color.darkGray}>

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { MapTransformProps } from "./interfaces";
import { getXYFromQuadrant } from "./util";
import { transformXY } from "./util";
import { CurrentPointPayl } from "../interfaces";
export interface DrawnPointProps {
@ -10,9 +10,8 @@ export interface DrawnPointProps {
export function DrawnPoint(props: DrawnPointProps) {
const { data, mapTransformProps } = props;
const { quadrant, gridSize } = mapTransformProps;
const { cx, cy, r, color } = data;
const { qx, qy } = getXYFromQuadrant(cx, cy, quadrant, gridSize);
const { qx, qy } = transformXY(cx, cy, mapTransformProps);
return <g
id="current-point"
stroke={color ? color : "green"}

View File

@ -12,7 +12,7 @@ import {
translateScreenToGarden,
round,
ScreenToGardenParams,
getXYFromQuadrant,
transformXY,
getMapSize
} from "./util";
import { findBySlug } from "../search_selectors";
@ -29,7 +29,7 @@ import {
ImageLayer,
} from "./layers";
import { cachedCrop } from "../../open_farm/icons";
import { AxisNumberProperty } from "./interfaces";
import { AxisNumberProperty, MapTransformProps } from "./interfaces";
import { SelectionBox, SelectionBoxData } from "./selection_box";
import { Actions } from "../../constants";
import { isNumber } from "lodash";
@ -69,6 +69,13 @@ export class GardenMap extends
this.state = {};
}
get mapTransformProps(): MapTransformProps {
return {
quadrant: this.props.botOriginQuadrant,
gridSize: this.props.gridSize
};
}
componentWillUnmount() {
unselectPlant(this.props.dispatch)();
}
@ -106,13 +113,12 @@ export class GardenMap extends
const page = document.querySelector(".farm-designer");
if (el && map && page) {
const zoomLvl = parseFloat(window.getComputedStyle(map).zoom || DRAG_ERROR);
const { pageX, pageY } = e;
const params: ScreenToGardenParams = {
quadrant: this.props.botOriginQuadrant,
pageX: pageX + page.scrollLeft - this.props.gridOffset.x * zoomLvl,
pageY: pageY + map.scrollTop * zoomLvl - this.props.gridOffset.y * zoomLvl,
page: { x: e.pageX, y: e.pageY },
scroll: { left: page.scrollLeft, top: map.scrollTop * zoomLvl },
mapTransformProps: this.mapTransformProps,
gridOffset: this.props.gridOffset,
zoomLvl,
gridSize: this.props.gridSize
};
return translateScreenToGarden(params);
} else {
@ -256,11 +262,10 @@ export class GardenMap extends
case Mode.editPlant:
const plant = this.getPlant();
const map = document.querySelector(".farm-designer-map");
const { botOriginQuadrant, gridSize } = this.props;
const { gridSize } = this.props;
if (this.state.isDragging && plant && map) {
const zoomLvl = parseFloat(window.getComputedStyle(map).zoom || DRAG_ERROR);
const { qx, qy } = getXYFromQuadrant(
e.pageX, e.pageY, botOriginQuadrant, gridSize);
const { qx, qy } = transformXY(e.pageX, e.pageY, this.mapTransformProps);
const deltaX = Math.round((qx - (this.state.pageX || qx)) / zoomLvl);
const deltaY = Math.round((qy - (this.state.pageY || qy)) / zoomLvl);
this.setState({
@ -308,10 +313,7 @@ export class GardenMap extends
render() {
const { gridSize } = this.props;
const mapSize = getMapSize(gridSize, this.props.gridOffset);
const mapTransformProps = {
quadrant: this.props.botOriginQuadrant,
gridSize
};
const mapTransformProps = this.mapTransformProps;
return <div
className="drop-area"
style={{

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { GardenPlantProps, GardenPlantState } from "./interfaces";
import { cachedCrop, DEFAULT_ICON, svgToUrl } from "../../open_farm/icons";
import { round, getXYFromQuadrant } from "./util";
import { round, transformXY } from "./util";
import { DragHelpers } from "./drag_helpers";
import { Session } from "../../session";
import { BooleanSetting } from "../../session_keys";
@ -52,11 +52,10 @@ export class GardenPlant extends
render() {
const { selected, dragging, plant, grayscale, mapTransformProps,
activeDragXY, zoomLvl } = this.props;
const { quadrant, gridSize } = mapTransformProps;
const { id, radius, x, y } = plant.body;
const { icon } = this.state;
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
const alpha = dragging ? 0.4 : 1.0;
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { GardenPointProps } from "./interfaces";
import { defensiveClone } from "../../util";
import { getXYFromQuadrant } from "./util";
import { transformXY } from "./util";
const POINT_STYLES = {
stroke: "green",
@ -12,11 +12,10 @@ const POINT_STYLES = {
export function GardenPoint(props: GardenPointProps) {
const { point, mapTransformProps } = props;
const { quadrant, gridSize } = mapTransformProps;
const { id, x, y } = point.body;
const styles = defensiveClone(POINT_STYLES);
styles.stroke = point.body.meta.color || "green";
const { qx, qy } = getXYFromQuadrant(x, y, quadrant, gridSize);
const { qx, qy } = transformXY(x, y, mapTransformProps);
return <g id={"point-" + id}>
<circle id="point-radius" cx={qx} cy={qy} r={point.body.radius} {...styles} />
<circle id="point-center" cx={qx} cy={qy} r={2} {...styles} />

View File

@ -1,15 +1,16 @@
import * as React from "react";
import { GridProps } from "./interfaces";
import { getXYFromQuadrant, transformForQuadrant } from "./util";
import { transformXY, transformForQuadrant } from "./util";
import * as _ from "lodash";
import { Color } from "../../ui/index";
export function Grid(props: GridProps) {
const { quadrant, gridSize } = props.mapTransformProps;
const origin = getXYFromQuadrant(0, 0, quadrant, gridSize);
const arrowEnd = getXYFromQuadrant(25, 25, quadrant, gridSize);
const xLabel = getXYFromQuadrant(15, -10, quadrant, gridSize);
const yLabel = getXYFromQuadrant(-11, 18, quadrant, gridSize);
const { mapTransformProps } = props;
const { gridSize } = mapTransformProps;
const origin = transformXY(0, 0, mapTransformProps);
const arrowEnd = transformXY(25, 25, mapTransformProps);
const xLabel = transformXY(15, -10, mapTransformProps);
const yLabel = transformXY(-11, 18, mapTransformProps);
return <g className="drop-area-background" onClick={props.onClick}>
<defs>
<pattern id="minor_grid"
@ -34,7 +35,7 @@ export function Grid(props: GridProps) {
<g id="grid">
<rect id="minor-grid"
width={gridSize.x} height={gridSize.y} fill="url(#minor_grid)" />
<rect id="major-grid" transform={transformForQuadrant(quadrant, gridSize)}
<rect id="major-grid" transform={transformForQuadrant(mapTransformProps)}
width={gridSize.x} height={gridSize.y} fill="url(#major_grid)" />
<rect id="border" width={gridSize.x} height={gridSize.y} fill="none"
stroke="rgba(0,0,0,0.3)" strokeWidth={2} />
@ -57,12 +58,12 @@ export function Grid(props: GridProps) {
<g id="axis-values" fontFamily="Arial" fontSize="10"
textAnchor="middle" dominantBaseline="central" fill="rgba(0, 0, 0, 0.3)">
{_.range(100, gridSize.x, 100).map((i) => {
const location = getXYFromQuadrant(i, -10, quadrant, gridSize);
const location = transformXY(i, -10, mapTransformProps);
return <text key={"x-label-" + i}
x={location.qx} y={location.qy}>{i}</text>;
})}
{_.range(100, gridSize.y, 100).map((i) => {
const location = getXYFromQuadrant(-15, i, quadrant, gridSize);
const location = transformXY(-15, i, mapTransformProps);
return <text key={"y-label-" + i}
x={location.qx} y={location.qy}>{i}</text>;
})}

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { TaggedPlantPointer } from "../../../resources/tagged_resources";
import { DesignerState } from "../../interfaces";
import { getXYFromQuadrant, round } from "../util";
import { transformXY, round } from "../util";
import { MapTransformProps } from "../interfaces";
import { SpreadCircle } from "./spread_layer";
import { Circle } from "../circle";
@ -41,9 +41,8 @@ export class HoveredPlantLayer extends
render() {
const { icon } = this.props.designer.hoveredPlant;
const { currentPlant, mapTransformProps, dragging, isEditing } = this.props;
const { quadrant, gridSize } = mapTransformProps;
const { id, x, y, radius } = this.plantInfo;
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
const hovered = !!this.props.designer.hoveredPlant.icon;
const scaledRadius = currentPlant ? radius : radius * 1.2;
const alpha = dragging ? 0.4 : 1.0;

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { Component } from "react";
import { TaggedPlantPointer } from "../../../resources/tagged_resources";
import { round, getXYFromQuadrant } from "../util";
import { round, transformXY } from "../util";
import { cachedCrop } from "../../../open_farm/icons";
import { MapTransformProps } from "../interfaces";
import { SpreadOverlapHelper } from "../spread_overlap_helper";
@ -82,8 +82,7 @@ export class SpreadCircle extends
render() {
const { radius, x, y, id } = this.props.plant.body;
const { selected, mapTransformProps } = this.props;
const { quadrant, gridSize } = mapTransformProps;
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
const animate = !Session.deprecatedGetBool(BooleanSetting.disable_animations);
return <g id={"spread-" + id}>

View File

@ -2,7 +2,7 @@ import * as React from "react";
import { TaggedImage } from "../../resources/tagged_resources";
import { CameraCalibrationData, BotOriginQuadrant } from "../interfaces";
import { MapTransformProps } from "./interfaces";
import { getXYFromQuadrant } from "./util";
import { transformXY } from "./util";
import { isNumber, round } from "lodash";
const PRECISION = 3; // Number of decimals for image placement coordinates
@ -123,7 +123,7 @@ export function MapImage(props: MapImageProps) {
const imageOffsetX = parse(offset.x);
const imageOffsetY = parse(offset.y);
const imageOrigin = origin ? origin.split("\"").join("") : undefined;
const { quadrant, gridSize } = props.mapTransformProps;
const { quadrant } = props.mapTransformProps;
/* Check if the image exists. */
if (image) {
@ -143,7 +143,7 @@ export function MapImage(props: MapImageProps) {
x: x + imageOffsetX - size.x / 2,
y: y + imageOffsetY - size.y / 2
};
const qCoords = getXYFromQuadrant(o.x, o.y, quadrant, gridSize);
const qCoords = transformXY(o.x, o.y, props.mapTransformProps);
const transformProps = { quadrant, qCoords, size, imageOrigin };
return <image
xlinkHref={imageUrl}

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { MapTransformProps } from "./interfaces";
import { getXYFromQuadrant, round } from "./util";
import { transformXY, round } from "./util";
import { isNumber } from "lodash";
export type SelectionBoxData =
@ -13,10 +13,9 @@ export interface SelectionBoxProps {
export function SelectionBox(props: SelectionBoxProps) {
const { x0, y0, x1, y1 } = props.selectionBox;
const { quadrant, gridSize } = props.mapTransformProps;
if (isNumber(x0) && isNumber(y0) && isNumber(x1) && isNumber(y1)) {
const initial = getXYFromQuadrant(round(x0), round(y0), quadrant, gridSize);
const drag = getXYFromQuadrant(round(x1), round(y1), quadrant, gridSize);
const initial = transformXY(round(x0), round(y0), props.mapTransformProps);
const drag = transformXY(round(x1), round(y1), props.mapTransformProps);
const x = Math.min(initial.qx, drag.qx);
const y = Math.min(initial.qy, drag.qy);
const width = Math.max(initial.qx, drag.qx) - x;

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { SpreadOverlapHelperProps } from "./interfaces";
import { round, getXYFromQuadrant } from "./util";
import { round, transformXY } from "./util";
import { isUndefined } from "util";
import { BotPosition } from "../../devices/interfaces";
import { cachedCrop } from "../../open_farm/icons";
@ -144,10 +144,8 @@ export class SpreadOverlapHelper extends
render() {
const { dragging, plant, activeDragXY, activeDragSpread,
mapTransformProps } = this.props;
const { quadrant, gridSize } = mapTransformProps;
const { radius, x, y } = plant.body;
const { qx, qy } = getXYFromQuadrant(
round(x), round(y), quadrant, gridSize);
const { qx, qy } = transformXY(round(x), round(y), mapTransformProps);
const gardenCoord: BotPosition = { x: round(x), y: round(y), z: 0 };
// Convert spread diameter to radius (in mm).

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { MapTransformProps } from "./interfaces";
import { getXYFromQuadrant, round } from "./util";
import { transformXY, round } from "./util";
import { BotPosition } from "../../devices/interfaces";
import { isNumber } from "lodash";
import { Color } from "../../ui/index";
@ -12,9 +12,8 @@ export interface TargetCoordinateProps {
export function TargetCoordinate(props: TargetCoordinateProps) {
const { x, y } = props.chosenLocation;
const { quadrant, gridSize } = props.mapTransformProps;
if (isNumber(x) && isNumber(y)) {
const { qx, qy } = getXYFromQuadrant(round(x), round(y), quadrant, gridSize);
const { qx, qy } = transformXY(round(x), round(y), props.mapTransformProps);
return <g id="target-coordinate">
<defs>
<g id={"target-coordinate-crosshair-segment"}>

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { SlotWithTool } from "../../resources/interfaces";
import { getXYFromQuadrant } from "./util";
import { transformXY } from "./util";
import { MapTransformProps } from "./interfaces";
import * as _ from "lodash";
import { ToolbaySlot, ToolNames, Tool } from "./tool_graphics";
@ -35,8 +35,9 @@ export class ToolSlotPoint extends
render() {
const { id, x, y, pullout_direction } = this.slot.toolSlot.body;
const { quadrant, gridSize } = this.props.mapTransformProps;
const { qx, qy } = getXYFromQuadrant(x, y, quadrant, gridSize);
const { mapTransformProps } = this.props;
const { quadrant } = mapTransformProps;
const { qx, qy } = transformXY(x, y, this.props.mapTransformProps);
const toolName = this.slot.tool ? this.slot.tool.body.name : "no tool";
const toolProps = {
x: qx,

View File

@ -1,9 +1,26 @@
import { BotOriginQuadrant, isBotOriginQuadrant } from "../interfaces";
import { McuParams } from "farmbot";
import { StepsPerMmXY } from "../../devices/interfaces";
import { CheckedAxisLength, AxisNumberProperty, BotSize } from "./interfaces";
import {
CheckedAxisLength, AxisNumberProperty, BotSize, MapTransformProps
} from "./interfaces";
import { trim } from "../../util";
/*
* Farm Designer Map Utilities
*
* Terms and Definitions:
* GARDEN coordinates: real coordinates (could be sent to bot)
* MAP coordinates: displayed locations (transformed according to map)
*
* Example:
* garden coordinate of {x: 100, y: 100}
* would be displayed at {x: 300, y: 300}
* if the bot axis sizes are {x: 400, y: 400}
* and the origin is in the lower right (map quadrant)
*/
/** multiple to round to for round() */
const SNAP = 10;
/**
@ -14,80 +31,131 @@ export function round(num: number) {
return (Math.round(num / SNAP) * SNAP);
}
/*
* Map coordinate calculations
*
* Constant:
* left and top map padding
* Map grid offset (for now)
*
* Dynamic:
* mouse position
* left and top scroll
* zoom level
* quadrant
* grid size
* XY swap (coming soon)
*
*/
/** Controlled by .farm-designer-map padding x10 */
const mapPadding = { left: 318, top: 110 };
/** "x" => "left" and "y" => "top" */
const leftOrTop: Record<"x" | "y", "top" | "left"> = { x: "left", y: "top" };
type XYCoordinate = { x: number, y: number };
export interface ScreenToGardenParams {
quadrant: BotOriginQuadrant;
pageX: number;
pageY: number;
page: XYCoordinate;
scroll: { left: number, top: number };
zoomLvl: number;
gridSize: AxisNumberProperty;
mapTransformProps: MapTransformProps;
gridOffset: AxisNumberProperty;
}
export function translateScreenToGarden(params: ScreenToGardenParams) {
const { pageX, pageY, zoomLvl, quadrant, gridSize } = params;
const rawX = round((pageX - 320) / zoomLvl);
const rawY = round((pageY - 110) / zoomLvl);
const x = calculateXBasedOnQuadrant({ value: rawX, quadrant, gridAxisLength: gridSize.x });
const y = calculateYBasedOnQuadrant({ value: rawY, quadrant, gridAxisLength: gridSize.y });
return { x, y };
/** Transform screen coordinates into garden coordinates */
export function translateScreenToGarden(
params: ScreenToGardenParams
): XYCoordinate {
const { page, scroll, zoomLvl, mapTransformProps, gridOffset } = params;
const screenXY = page;
const mapXY = ["x", "y"].reduce<XYCoordinate>(
(result: XYCoordinate, axis: "x" | "y") => {
const unscrolled = screenXY[axis] - scroll[leftOrTop[axis]];
const map = unscrolled - mapPadding[leftOrTop[axis]];
const grid = map - gridOffset[axis] * zoomLvl;
const unscaled = round(grid / zoomLvl);
result[axis] = unscaled;
return result;
}, { x: 0, y: 0 });
const gardenXY = quadTransform({ coordinate: mapXY, mapTransformProps });
return gardenXY;
}
interface CalculateQuadrantParams {
value: number;
quadrant: BotOriginQuadrant;
gridAxisLength: number;
/* BotOriginQuadrant diagram
2 --- 1
| |
3 --- 4
*/
const NORMAL_QUADRANTS: Record<"x" | "y", BotOriginQuadrant[]> = {
// `2` is shared, i.e. no change needed for either axis when it is selected
x: [3, 2],
y: [2, 1]
};
const MIRRORED_QUADRANTS: Record<"x" | "y", BotOriginQuadrant[]> = {
// `4` is shared, i.e. change needed for both axes when it is selected
x: [1, 4],
y: [4, 3]
};
interface QuadTransformParams {
/** garden or map coordinates */
coordinate: XYCoordinate;
/** props necessary for coordinate transformation */
mapTransformProps: MapTransformProps;
}
function calculateXBasedOnQuadrant(params: CalculateQuadrantParams) {
const { value, quadrant, gridAxisLength } = params;
/** Quadrant coordinate transformation */
function quadTransform(params: QuadTransformParams): XYCoordinate {
const { coordinate, mapTransformProps } = params;
const { quadrant, gridSize } = mapTransformProps;
if (isBotOriginQuadrant(quadrant)) {
switch (quadrant) {
case 1:
case 4:
return gridAxisLength - value;
case 2:
case 3:
return value;
default:
throw new Error("Something went wrong calculating the X origin.");
}
return ["x", "y"].reduce<XYCoordinate>(
(result: XYCoordinate, axis: "x" | "y") => {
switch (quadrant) {
case MIRRORED_QUADRANTS[axis][0]:
case MIRRORED_QUADRANTS[axis][1]:
result[axis] = gridSize[axis] - coordinate[axis];
return result;
case NORMAL_QUADRANTS[axis][0]:
case NORMAL_QUADRANTS[axis][1]:
result[axis] = coordinate[axis];
return result;
default:
throw new Error(`Something went wrong calculating the ${axis} origin.`);
}
}, { x: 0, y: 0 });
} else {
throw new Error("Invalid bot origin quadrant.");
}
}
function calculateYBasedOnQuadrant(params: CalculateQuadrantParams) {
const { value, quadrant, gridAxisLength } = params;
if (isBotOriginQuadrant(quadrant)) {
switch (quadrant) {
case 3:
case 4:
return gridAxisLength - value;
case 1:
case 2:
return value;
default:
throw new Error("Something went wrong calculating the Y origin.");
}
} else {
throw new Error("Invalid bot origin quadrant.");
}
}
export function getXYFromQuadrant(
/**
* Transform between garden and map coordinates
*
* Used for placing things in the Farm Designer map
* or getting the real coordinates of things in the map.
*
* @param coordinate: garden or map coordinate
* @param mapTransformProps: props necessary for coordinate transformation
*/
export function transformXY(
x: number,
y: number,
q: BotOriginQuadrant,
gridSize: AxisNumberProperty
mapTransformProps: MapTransformProps
): { qx: number, qy: number } {
const transformed = quadTransform({ coordinate: { x, y }, mapTransformProps });
return {
qx: calculateXBasedOnQuadrant({ value: x, quadrant: q, gridAxisLength: gridSize.x }),
qy: calculateYBasedOnQuadrant({ value: y, quadrant: q, gridAxisLength: gridSize.y })
qx: transformed.x,
qy: transformed.y
};
}
/** Determine bot axis lengths according to firmware settings */
export function getBotSize(
botMcuParams: McuParams,
stepsPerMmXY: StepsPerMmXY,
@ -114,6 +182,7 @@ export function getBotSize(
return { x: getAxisLength("x"), y: getAxisLength("y") };
}
/** Calculate map dimensions */
export function getMapSize(
gridSize: AxisNumberProperty,
gridOffset: AxisNumberProperty
@ -124,11 +193,11 @@ export function getMapSize(
};
}
/* Transform object based on selected map quadrant and grid size. */
/** Transform object based on selected map quadrant and grid size. */
export const transformForQuadrant =
(quadrant: BotOriginQuadrant, gridSize: AxisNumberProperty) => {
(mapTransformProps: MapTransformProps): string => {
const quadrantFlips = () => {
switch (quadrant) {
switch (mapTransformProps.quadrant) {
case 1: return { x: -1, y: 1 };
case 2: return { x: 1, y: 1 };
case 3: return { x: 1, y: -1 };
@ -136,7 +205,7 @@ export const transformForQuadrant =
default: return { x: 1, y: 1 };
}
};
const origin = getXYFromQuadrant(0, 0, quadrant, gridSize);
const origin = transformXY(0, 0, mapTransformProps);
const flip = quadrantFlips();
const translate = { x: flip.x * origin.qx, y: flip.y * origin.qy };
return trim(

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { AxisNumberProperty, MapTransformProps } from "../interfaces";
import { getMapSize, getXYFromQuadrant } from "../util";
import { getMapSize, transformXY } from "../util";
import { BotPosition } from "../../../devices/interfaces";
import { Color } from "../../../ui/index";
import { botPositionLabel } from "./bot_position_label";
@ -24,11 +24,11 @@ export class BotFigure extends
setHover = (state: boolean) => { this.setState({ hovered: state }); };
render() {
const { name, position, plantAreaOffset, eStopStatus } = this.props;
const { quadrant, gridSize } = this.props.mapTransformProps;
const mapSize = getMapSize(gridSize, plantAreaOffset);
const positionQ = getXYFromQuadrant(
(position.x || 0), (position.y || 0), quadrant, gridSize);
const { name, position, plantAreaOffset, eStopStatus, mapTransformProps
} = this.props;
const mapSize = getMapSize(mapTransformProps.gridSize, plantAreaOffset);
const positionQ = transformXY(
(position.x || 0), (position.y || 0), mapTransformProps);
const color = eStopStatus ? Color.virtualRed : Color.darkGray;
const opacity = name.includes("encoder") ? 0.25 : 0.75;
return <g id={name}>

View File

@ -1,6 +1,6 @@
import * as React from "react";
import { AxisNumberProperty, MapTransformProps } from "../interfaces";
import { getMapSize, getXYFromQuadrant } from "../util";
import { getMapSize, transformXY } from "../util";
import { BotPosition } from "../../../devices/interfaces";
import * as _ from "lodash";
import { Session } from "../../../session";
@ -116,11 +116,10 @@ function vacuumFigure(
}
export function BotPeripherals(props: BotPeripheralsProps) {
const { peripherals, position, plantAreaOffset } = props;
const { quadrant, gridSize } = props.mapTransformProps;
const mapSize = getMapSize(gridSize, plantAreaOffset);
const positionQ = getXYFromQuadrant(
(position.x || 0), (position.y || 0), quadrant, gridSize);
const { peripherals, position, plantAreaOffset, mapTransformProps } = props;
const mapSize = getMapSize(mapTransformProps.gridSize, plantAreaOffset);
const positionQ = transformXY(
(position.x || 0), (position.y || 0), mapTransformProps);
return <g className={"virtual-peripherals"}>
{peripherals.map((x, i) => {

View File

@ -1,7 +1,7 @@
import * as React from "react";
import * as _ from "lodash";
import { MapTransformProps } from "../interfaces";
import { getXYFromQuadrant } from "../util";
import { transformXY } from "../util";
import { BotPosition } from "../../../devices/interfaces";
import { Color } from "../../../ui";
@ -38,9 +38,8 @@ export interface BotTrailProps {
}
export function BotTrail(props: BotTrailProps) {
const { quadrant, gridSize } = props.mapTransformProps;
const toQ = (ox: number, oy: number) =>
getXYFromQuadrant(ox, oy, quadrant, gridSize);
transformXY(ox, oy, props.mapTransformProps);
const { x, y } = props.position;
const watering = !!_.first(props.peripherals

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { BotPosition } from "../../../devices/interfaces";
import { MapTransformProps, AxisNumberProperty } from "../interfaces";
import { getXYFromQuadrant } from "../util";
import { transformXY } from "../util";
import { Color } from "../../../ui";
import { botPositionLabel } from "./bot_position_label";
@ -13,13 +13,12 @@ export interface NegativePositionLabelProps {
export function NegativePositionLabel(props: NegativePositionLabelProps) {
const { position, mapTransformProps, plantAreaOffset } = props;
const { quadrant, gridSize } = mapTransformProps;
const xIsNegative = position.x && position.x < 0;
const yIsNegative = position.y && position.y < 0;
const origin = getXYFromQuadrant(
const origin = transformXY(
-plantAreaOffset.x + 40,
-plantAreaOffset.y - 10,
quadrant, gridSize);
mapTransformProps);
return <g id={"negative-position-label"}
fontFamily="Arial" textAnchor="middle" dominantBaseline="central"