diff --git a/frontend/farm_designer/map/layers/images/__tests__/images_layer_test.tsx b/frontend/farm_designer/map/layers/images/__tests__/images_layer_test.tsx
index 3e87bb3b9..b8b69ae30 100644
--- a/frontend/farm_designer/map/layers/images/__tests__/images_layer_test.tsx
+++ b/frontend/farm_designer/map/layers/images/__tests__/images_layer_test.tsx
@@ -26,13 +26,12 @@ describe("", () => {
images: [image],
mapTransformProps: fakeMapTransformProps(),
cameraCalibrationData: {
- offset: { x: "0", y: "0" },
- origin: "TOP_LEFT",
- rotation: "0",
- scale: "1",
- calibrationZ: "0"
+ offset: { x: undefined, y: undefined },
+ origin: undefined,
+ rotation: undefined,
+ scale: undefined,
+ calibrationZ: undefined,
},
- sizeOverride: { width: 10, height: 10 },
getConfigValue: jest.fn(),
};
}
@@ -41,7 +40,7 @@ describe("", () => {
const p = fakeProps();
const wrapper = shallow();
const layer = wrapper.find("#image-layer");
- expect(layer.find("MapImage").html()).toContain("x=\"0\"");
+ expect(layer.find("MapImage").html()).toContain("image");
});
it("toggles visibility off", () => {
diff --git a/frontend/farm_designer/map/layers/images/__tests__/map_image_test.tsx b/frontend/farm_designer/map/layers/images/__tests__/map_image_test.tsx
index dd3ae6220..436d2e3d3 100644
--- a/frontend/farm_designer/map/layers/images/__tests__/map_image_test.tsx
+++ b/frontend/farm_designer/map/layers/images/__tests__/map_image_test.tsx
@@ -45,9 +45,22 @@ describe("", () => {
const p = fakeProps();
p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
const wrapper = mount();
+ wrapper.setState({ width: 100, height: 100 });
expect(wrapper.html()).toContain("image_url");
});
+ it("gets image size", () => {
+ const p = fakeProps();
+ p.image && (p.image.body.meta = { x: 0, y: 0, z: 0 });
+ const wrapper = mount();
+ expect(wrapper.state()).toEqual({ width: 0, height: 0 });
+ const img = new Image();
+ img.width = 100;
+ img.height = 200;
+ wrapper.instance().imageCallback(img)();
+ expect(wrapper.state()).toEqual({ width: 100, height: 200 });
+ });
+
interface ExpectedData {
size: { width: number, height: number };
sx: number;
@@ -71,6 +84,7 @@ describe("", () => {
extra?: ExtraTranslationData) => {
it(`renders image: INPUT_SET_${num}`, () => {
const wrapper = mount();
+ wrapper.setState({ width: 480, height: 640 });
expect(wrapper.find("image").props()).toEqual({
xlinkHref: "image_url",
x: 0,
@@ -100,7 +114,6 @@ describe("", () => {
INPUT_SET_1.mapTransformProps = fakeMapTransformProps();
INPUT_SET_1.mapTransformProps.gridSize = { x: 5900, y: 2900 },
INPUT_SET_1.mapTransformProps.quadrant = 3;
- INPUT_SET_1.sizeOverride = { width: 480, height: 640 };
const INPUT_SET_2 = cloneDeep(INPUT_SET_1);
INPUT_SET_2.image && (INPUT_SET_2.image.body.meta = {
@@ -125,13 +138,16 @@ describe("", () => {
const INPUT_SET_7 = cloneDeep(INPUT_SET_6);
INPUT_SET_7.cameraCalibrationData.origin = "BOTTOM_RIGHT";
+ const INPUT_SET_9 = cloneDeep(INPUT_SET_6);
+ INPUT_SET_9.cameraCalibrationData.origin = "TOP_LEFT";
+
const INPUT_SET_8 = cloneDeep(INPUT_SET_7);
INPUT_SET_8.mapTransformProps.xySwap = true;
const DATA = [
INPUT_SET_1,
INPUT_SET_1, INPUT_SET_2, INPUT_SET_3, INPUT_SET_4, INPUT_SET_5,
- INPUT_SET_6, INPUT_SET_7, INPUT_SET_8
+ INPUT_SET_6, INPUT_SET_7, INPUT_SET_8, INPUT_SET_9,
];
const expectedSize = { width: 385.968, height: 514.624 };
@@ -157,6 +173,9 @@ describe("", () => {
renderedTest(7, DATA, {
size: expectedSize, sx: 1, sy: 1, tx: 5436.016, ty: 2259.688
});
+ renderedTest(9, DATA, {
+ size: expectedSize, sx: -1, sy: -1, tx: -5821.984, ty: -2774.312
+ });
renderedTest(8, DATA, {
size: expectedSize, sx: 1, sy: 1, tx: 2388.344, ty: 5307.36
}, { rot: 90, sx: -1, sy: 1, tx: -514.624, ty: -514.624 });
diff --git a/frontend/farm_designer/map/layers/images/image_layer.tsx b/frontend/farm_designer/map/layers/images/image_layer.tsx
index 8e357a6f4..1b17e9808 100644
--- a/frontend/farm_designer/map/layers/images/image_layer.tsx
+++ b/frontend/farm_designer/map/layers/images/image_layer.tsx
@@ -6,37 +6,42 @@ import { MapImage } from "./map_image";
import { reverse, cloneDeep } from "lodash";
import { GetWebAppConfigValue } from "../../../../config_storage/actions";
import moment from "moment";
+import { equals } from "../../../../util";
export interface ImageLayerProps {
visible: boolean;
images: TaggedImage[];
mapTransformProps: MapTransformProps;
cameraCalibrationData: CameraCalibrationData;
- sizeOverride?: { width: number, height: number };
getConfigValue: GetWebAppConfigValue;
}
-export function ImageLayer(props: ImageLayerProps) {
- const {
- visible, images, mapTransformProps, cameraCalibrationData, sizeOverride,
- getConfigValue
- } = props;
- const imageFilterBegin = getConfigValue("photo_filter_begin");
- const imageFilterEnd = getConfigValue("photo_filter_end");
- return
- {visible &&
- reverse(cloneDeep(images))
- .filter(x => !imageFilterEnd ||
- moment(x.body.created_at).isBefore(imageFilterEnd.toString()))
- .filter(x => !imageFilterBegin ||
- moment(x.body.created_at).isAfter(imageFilterBegin.toString()))
- .map(img =>
-
- )}
- ;
+export class ImageLayer extends React.Component {
+
+ shouldComponentUpdate(nextProps: ImageLayerProps) {
+ return !equals(this.props, nextProps);
+ }
+
+ render() {
+ const {
+ visible, images, mapTransformProps, cameraCalibrationData, getConfigValue
+ } = this.props;
+ const imageFilterBegin = getConfigValue("photo_filter_begin");
+ const imageFilterEnd = getConfigValue("photo_filter_end");
+ return
+ {visible &&
+ reverse(cloneDeep(images))
+ .filter(x => !imageFilterEnd ||
+ moment(x.body.created_at).isBefore(imageFilterEnd.toString()))
+ .filter(x => !imageFilterBegin ||
+ moment(x.body.created_at).isAfter(imageFilterBegin.toString()))
+ .map(img =>
+
+ )}
+ ;
+ }
}
diff --git a/frontend/farm_designer/map/layers/images/map_image.tsx b/frontend/farm_designer/map/layers/images/map_image.tsx
index e9bbc1c1b..d94cfc273 100644
--- a/frontend/farm_designer/map/layers/images/map_image.tsx
+++ b/frontend/farm_designer/map/layers/images/map_image.tsx
@@ -4,6 +4,7 @@ import { CameraCalibrationData, BotOriginQuadrant } from "../../../interfaces";
import { MapTransformProps } from "../../interfaces";
import { transformXY } from "../../util";
import { isNumber, round } from "lodash";
+import { equals } from "../../../../util";
const PRECISION = 3; // Number of decimals for image placement coordinates
/** Show all images roughly on map when no calibration values are present. */
@@ -33,21 +34,14 @@ const cameraZCheck =
Math.abs(imageZ - calibrationZ) < 5;
};
-interface ImageSize {
- width: number;
- height: number;
-}
-
-/* Get the size of the image at the URL. Allow overriding for tests. */
-const getImageSize = (url: string, size?: ImageSize): ImageSize => {
- if (url.includes("placehold")) { return { width: 0, height: 0 }; }
- if (size) { return size; }
+/* Get the size of the image at the URL. */
+const getImageSize = (
+ url: string,
+ onLoad: (img: HTMLImageElement) => () => void
+): void => {
const imageData = new Image();
imageData.src = url;
- return {
- height: imageData.height || 480,
- width: imageData.width || 640
- };
+ imageData.onload = onLoad(imageData);
};
/* Flip (mirror) image based on orientation of camera. */
@@ -113,11 +107,37 @@ const transform = (props: TransformProps): string => {
+ xySwapTransform;
};
+interface ParsedCalibrationData {
+ noCalib: boolean;
+ imageScale: number | undefined;
+ imageOffsetX: number | undefined;
+ imageOffsetY: number | undefined;
+ imageOrigin: string | undefined;
+}
+
+/** If calibration data exists, parse it, usually to a number.
+ * Otherwise, return values for pre-calibration preview. */
+const parseCalibrationData =
+ (props: CameraCalibrationData): ParsedCalibrationData => {
+ const { scale, offset, origin } = props;
+ const noCalib = PRE_CALIBRATION_PREVIEW && !parse(scale);
+ const imageScale = noCalib ? 0.6 : parse(scale);
+ const imageOffsetX = noCalib ? 0 : parse(offset.x);
+ const imageOffsetY = noCalib ? 0 : parse(offset.y);
+ const cleanOrigin = origin ? origin.split("\"").join("") : undefined;
+ const imageOrigin = noCalib ? "BOTTOM_LEFT" : cleanOrigin;
+ return { noCalib, imageScale, imageOffsetX, imageOffsetY, imageOrigin };
+ };
+
export interface MapImageProps {
image: TaggedImage | undefined;
cameraCalibrationData: CameraCalibrationData;
mapTransformProps: MapTransformProps;
- sizeOverride?: ImageSize;
+}
+
+interface MapImageState {
+ width: number;
+ height: number;
}
/*
@@ -125,43 +145,55 @@ export interface MapImageProps {
* Assume the image that is provided from the Farmware is rotated correctly.
* Require camera calibration data to display the image.
*/
-// tslint:disable-next-line:cyclomatic-complexity
-export function MapImage(props: MapImageProps) {
- const { image, cameraCalibrationData, sizeOverride } = props;
- const { scale, offset, origin, calibrationZ } = cameraCalibrationData;
- const noCalib = PRE_CALIBRATION_PREVIEW && !parse(scale);
- const imageScale = noCalib ? 1.5 : parse(scale);
- const imageOffsetX = noCalib ? 0 : parse(offset.x);
- const imageOffsetY = noCalib ? 0 : parse(offset.y);
- const cleanOrigin = origin ? origin.split("\"").join("") : undefined;
- const imageOrigin = noCalib ? "BOTTOM_LEFT" : cleanOrigin;
- const { quadrant, xySwap } = props.mapTransformProps;
+export class MapImage extends React.Component {
+ state: MapImageState = { width: 0, height: 0 };
- /* Check if the image exists. */
- if (image) {
- const imageUrl = image.body.attachment_url;
- const { x, y, z } = image.body.meta;
- const imageAnnotation = image.body.meta.name;
- const { width, height } = getImageSize(imageUrl, sizeOverride);
-
- /* Check for all necessary camera calibration and image data. */
- if (isNumber(x) && isNumber(y) && height > 0 && width > 0 &&
- isNumber(imageScale) && imageScale > 0 &&
- cameraZCheck(z, calibrationZ) && isRotated(imageAnnotation, noCalib) &&
- isNumber(imageOffsetX) && isNumber(imageOffsetY) && imageOrigin) {
- /* Use pixel to coordinate scale to scale image. */
- const size = { x: width * imageScale, y: height * imageScale };
- const o = { // Coordinates of top left corner of image for placement
- x: x + imageOffsetX - size.x / 2,
- y: y + imageOffsetY - size.y / 2
- };
- const qCoords = transformXY(o.x, o.y, props.mapTransformProps);
- const transformProps = { quadrant, qCoords, size, imageOrigin, xySwap };
- return ;
- }
+ shouldComponentUpdate(nextProps: MapImageProps, nextState: MapImageState) {
+ const propsChanged = !equals(this.props, nextProps);
+ const stateChanged = !equals(this.state, nextState);
+ return propsChanged || stateChanged;
+ }
+
+ imageCallback = (img: HTMLImageElement) => () => {
+ const { width, height } = img;
+ this.setState({ width, height });
+ };
+
+ render() {
+ const { image, cameraCalibrationData } = this.props;
+ const {
+ noCalib, imageScale, imageOffsetX, imageOffsetY, imageOrigin
+ } = parseCalibrationData(cameraCalibrationData);
+ const { calibrationZ } = cameraCalibrationData;
+ const { quadrant, xySwap } = this.props.mapTransformProps;
+
+ /* Check if the image exists. */
+ if (image && !image.body.attachment_url.includes("placehold")) {
+ const imageUrl = image.body.attachment_url;
+ const { x, y, z } = image.body.meta;
+ const imageAnnotation = image.body.meta.name;
+ getImageSize(imageUrl, this.imageCallback);
+ const { width, height } = this.state;
+
+ /* Check for all necessary camera calibration and image data. */
+ if (isNumber(x) && isNumber(y) && height > 0 && width > 0 &&
+ isNumber(imageScale) && imageScale > 0 &&
+ cameraZCheck(z, calibrationZ) && isRotated(imageAnnotation, noCalib) &&
+ isNumber(imageOffsetX) && isNumber(imageOffsetY) && imageOrigin) {
+ /* Use pixel to coordinate scale to scale image. */
+ const size = { x: width * imageScale, y: height * imageScale };
+ const o = { // Coordinates of top left corner of image for placement
+ x: x + imageOffsetX - size.x / 2,
+ y: y + imageOffsetY - size.y / 2
+ };
+ const qCoords = transformXY(o.x, o.y, this.props.mapTransformProps);
+ const transformProps = { quadrant, qCoords, size, imageOrigin, xySwap };
+ return ;
+ }
+ }
+ return ;
}
- return ;
}
diff --git a/frontend/farmware/index.tsx b/frontend/farmware/index.tsx
index 6b4d5ac03..15907fa19 100644
--- a/frontend/farmware/index.tsx
+++ b/frontend/farmware/index.tsx
@@ -15,7 +15,6 @@ import {
FarmwareForm, needsFarmwareForm, farmwareHelpText
} from "./farmware_forms";
import { urlFriendly } from "../util";
-import { history } from "../history";
import { ToolTips, Actions } from "../constants";
import { FarmwareInfo } from "./farmware_info";
import { Farmwares, FarmwareManifestInfo } from "./interfaces";
@@ -129,13 +128,9 @@ export class FarmwarePage extends React.Component {
type: Actions.SELECT_FARMWARE,
payload: "Photos"
});
- if (Object.values(this.props.farmwares).length > 0) {
- const farmwareNames = Object.values(this.props.farmwares).map(x => x.name);
- setActiveFarmwareByName(farmwareNames);
- } else {
- // Farmware information not available. Load default Farmware page.
- history.push("/app/farmware");
- }
+ const farmwareNames = Object.values(this.props.farmwares).map(x => x.name)
+ .concat(Object.keys(FARMWARE_NAMES_1ST_PARTY));
+ setActiveFarmwareByName(farmwareNames);
}
/** Load Farmware input panel contents for 1st & 3rd party Farmware. */