import * as React from "react"; import { TaggedImage } from "farmbot"; 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. */ const PRE_CALIBRATION_PREVIEW = true; /* Parse floats in camera calibration environment variables. */ const parse = (str: string | undefined) => { const parsed = str ? parseFloat(str) : NaN; return !isNaN(parsed) ? parsed : undefined; }; /* Check if the image has been rotated according to the calibration value. */ const isRotated = (annotation: string | undefined, noCalib: boolean) => { if (PRE_CALIBRATION_PREVIEW && noCalib) { return true; } return annotation && (annotation.includes("rotated") || annotation.includes("marked") || annotation.includes("calibration_result")); }; /* Check if the calibration data is valid for the image provided using z. */ const cameraZCheck = (imageZ: number | undefined, calibZ: string | undefined) => { if (PRE_CALIBRATION_PREVIEW && !calibZ) { return true; } const calibrationZ = parse(calibZ); return isNumber(imageZ) && isNumber(calibrationZ) && Math.abs(imageZ - calibrationZ) < 5; }; /* 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; imageData.onload = onLoad(imageData); }; /* Flip (mirror) image based on orientation of camera. */ const originAdjustment = (imageOrigin: string) => { switch (imageOrigin) { case "BOTTOM_RIGHT": return { x: -1, y: -1 }; case "BOTTOM_LEFT": return { x: 1, y: -1 }; case "TOP_RIGHT": return { x: -1, y: 1 }; case "TOP_LEFT": default: return { x: 1, y: 1 }; } }; /* Flip (mirror) image based on map quadrant. */ const quadrantAdjustment = (quadrant: BotOriginQuadrant) => { switch (quadrant) { case 1: return { x: -1, y: 1 }; case 3: return { x: 1, y: -1 }; case 4: return { x: -1, y: -1 }; case 2: default: return { x: 1, y: 1 }; } }; interface TransformProps { quadrant: BotOriginQuadrant; qCoords: { qx: number, qy: number }; size: { x: number, y: number }; imageOrigin: string; xySwap: boolean; } /* Image transform string. Flip and place image at the correct map location. */ const transform = (props: TransformProps): string => { const { quadrant, qCoords, size, imageOrigin, xySwap } = props; const { qx, qy } = qCoords; const orginAdjusted = originAdjustment(imageOrigin); const quadrantAdjusted = quadrantAdjustment(quadrant); const flip = { x: orginAdjusted.x * quadrantAdjusted.x, y: orginAdjusted.y * quadrantAdjusted.y }; const toZero = { x: orginAdjusted.x < 0 ? -size.x : 0, y: orginAdjusted.y < 0 ? -size.y : 0 }; const translate = { x: round(flip.x * qx + toZero.x, PRECISION), y: round(flip.y * qy + toZero.y, PRECISION) }; const xySwapTransform = xySwap ? ` rotate(90) scale(${-1}, ${1}) translate(${-size.y}, ${-size.y})` : ""; return `scale(${flip.x}, ${flip.y}) translate(${translate.x}, ${translate.y})` + 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 ? "TOP_LEFT" : cleanOrigin; return { noCalib, imageScale, imageOffsetX, imageOffsetY, imageOrigin }; }; export interface MapImageProps { image: TaggedImage | undefined; cameraCalibrationData: CameraCalibrationData; mapTransformProps: MapTransformProps; } interface MapImageState { width: number; height: number; } /* * Place the camera image in the Farm Designer map. * Assume the image that is provided from the Farmware is rotated correctly. * Require camera calibration data to display the image. */ export class MapImage extends React.Component { state: MapImageState = { width: 0, height: 0 }; 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 ; } }