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