import * as React from "react"; import { BooleanSetting } from "../../session_keys"; import { closePlantInfo, unselectPlant } from "./actions"; import { MapTransformProps, TaggedPlant, Mode, AxisNumberProperty, } from "./interfaces"; import { GardenMapProps, GardenMapState } from "../interfaces"; import { getMapSize, getGardenCoordinates, getMode, cursorAtPlant, allowInteraction, } from "./util"; import { Grid, MapBackground, TargetCoordinate, SelectionBox, resizeBox, startNewSelectionBox, maybeUpdateGroup, } from "./background"; import { PlantLayer, SpreadLayer, PointLayer, WeedLayer, ToolSlotLayer, FarmBotLayer, ImageLayer, SensorReadingsLayer, } from "./layers"; import { HoveredPlant, ActivePlantDragHelper } from "./active_plant"; import { DrawnPoint, startNewPoint, resizePoint } from "./drawn_point"; import { Bugs, showBugs } from "./easter_eggs/bugs"; import { dropPlant, dragPlant, beginPlantDrag, maybeSavePlantLocation, } from "./layers/plants/plant_actions"; import { chooseLocation } from "../move_to"; import { GroupOrder } from "../point_groups/group_order_visual"; import { NNPath } from "../point_groups/paths"; import { history } from "../../history"; import { ZonesLayer } from "./layers/zones/zones_layer"; import { ErrorBoundary } from "../../error_boundary"; import { TaggedPoint, TaggedPointGroup, PointType } from "farmbot"; import { findGroupFromUrl } from "../point_groups/group_detail"; import { pointsSelectedByGroup } from "../point_groups/criteria"; import { DrawnWeed } from "./drawn_point/drawn_weed"; import { UUID } from "../../resources/interfaces"; import { throttle } from "lodash"; export class GardenMap extends React.Component> { state: Partial = {}; constructor(props: GardenMapProps) { super(props); this.state = {}; } componentWillUnmount() { // Clear plant selection when navigating away from the designer. unselectPlant(this.props.dispatch)(); } /** Assemble the props needed for placement of items in the map. */ get mapTransformProps(): MapTransformProps { return { quadrant: this.props.botOriginQuadrant, gridSize: this.props.gridSize, xySwap: !!this.props.getConfigValue(BooleanSetting.xy_swap), }; } get mapSize() { return getMapSize(this.mapTransformProps, this.props.gridOffset); } get xySwap() { return this.mapTransformProps.xySwap; } /** Currently editing a plant? */ get isEditing(): boolean { return getMode() === Mode.editPlant; } /** Display plant animations? */ get animate(): boolean { return !this.props.getConfigValue(BooleanSetting.disable_animations); } get group(): TaggedPointGroup | undefined { return findGroupFromUrl(this.props.groups); } get pointsSelectedByGroup(): TaggedPoint[] { return this.group ? pointsSelectedByGroup(this.group, this.props.allPoints) : []; } get groupSelected(): UUID[] { return this.pointsSelectedByGroup.map(point => point.uuid); } /** Save the current plant (if needed) and reset drag state. */ endDrag = throttle(() => { maybeSavePlantLocation({ plant: this.getPlant(), isDragging: this.state.isDragging, dispatch: this.props.dispatch, }); maybeUpdateGroup({ selectionBox: this.state.selectionBox, group: this.group, dispatch: this.props.dispatch, shouldDisplay: this.props.shouldDisplay, editGroupAreaInMap: this.props.designer.editGroupAreaInMap, boxSelected: this.props.designer.selectedPoints, }); this.setState({ isDragging: false, qPageX: 0, qPageY: 0, activeDragXY: { x: undefined, y: undefined, z: undefined }, activeDragSpread: undefined, selectionBox: undefined }); }, 400); getGardenCoordinates = (e: React.DragEvent | React.MouseEvent): AxisNumberProperty | undefined => { return getGardenCoordinates({ mapTransformProps: this.mapTransformProps, gridOffset: this.props.gridOffset, pageX: e.pageX, pageY: e.pageY, }); }; setMapState = (x: Partial) => this.setState(x); /** Map (anywhere) drag start actions. */ startDrag = (e: React.MouseEvent): void => { switch (getMode()) { case Mode.editPlant: const gardenCoords = this.getGardenCoordinates(e); const plant = this.getPlant(); if (cursorAtPlant(plant, gardenCoords)) { beginPlantDrag({ plant, setMapState: this.setMapState, selectedPlant: this.props.selectedPlant, }); } else { // Actions away from plant exit plant edit mode. this.closePanel()(); startNewSelectionBox({ gardenCoords, setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: true, }); } break; case Mode.editGroup: startNewSelectionBox({ gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: !this.props.designer.editGroupAreaInMap, }); break; case Mode.createPoint: startNewPoint({ gardenCoords: this.getGardenCoordinates(e), dispatch: this.props.dispatch, setMapState: this.setMapState, type: "point", }); break; case Mode.createWeed: startNewPoint({ gardenCoords: this.getGardenCoordinates(e), dispatch: this.props.dispatch, setMapState: this.setMapState, type: "weed", }); break; case Mode.clickToAdd: break; } } /** Map background drag start actions. */ startDragOnBackground = (e: React.MouseEvent): void => { switch (getMode()) { case Mode.moveTo: case Mode.createPoint: case Mode.createWeed: case Mode.clickToAdd: case Mode.editPlant: break; case Mode.boxSelect: startNewSelectionBox({ gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: true, }); break; case Mode.editGroup: startNewSelectionBox({ gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: !this.props.designer.editGroupAreaInMap, }); break; default: history.push("/app/designer/plants"); startNewSelectionBox({ gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: true, }); break; } } interactions = (pointerType: PointType): boolean => { if (allowInteraction()) { switch (getMode()) { case Mode.editGroup: case Mode.boxSelect: return (this.props.designer.selectionPointType || ["Plant"]) .includes(pointerType); } } return allowInteraction(); }; /** Return the selected plant, mode-allowing. */ getPlant = (): TaggedPlant | undefined => { return allowInteraction() ? this.props.selectedPlant : undefined; } get currentPoint(): UUID | undefined { return this.props.designer.selectedPoints?.[0]; } handleDragOver = (e: React.DragEvent) => { switch (getMode()) { case Mode.addPlant: case Mode.clickToAdd: e.preventDefault(); // Allows dragged-in plants to be placed in the map e.dataTransfer.dropEffect = "move"; } } handleDragEnter = (e: React.DragEvent) => { switch (getMode()) { case Mode.addPlant: e.preventDefault(); } } handleDrop = (e: React.DragEvent | React.MouseEvent) => { e.preventDefault(); dropPlant({ gardenCoords: this.getGardenCoordinates(e), cropSearchResults: this.props.designer.cropSearchResults, openedSavedGarden: this.props.designer.openedSavedGarden, gridSize: this.props.gridSize, dispatch: this.props.dispatch, }); }; click = (e: React.MouseEvent) => { switch (getMode()) { case Mode.clickToAdd: // Create a new plant in the map this.handleDrop(e); break; case Mode.moveTo: e.preventDefault(); chooseLocation({ gardenCoords: this.getGardenCoordinates(e), dispatch: this.props.dispatch }); break; } } /** Map drag actions. */ drag = (e: React.MouseEvent) => { switch (getMode()) { case Mode.editPlant: dragPlant({ getPlant: this.getPlant, mapTransformProps: this.mapTransformProps, isDragging: this.state.isDragging, dispatch: this.props.dispatch, setMapState: this.setMapState, gridSize: this.props.gridSize, qPageX: this.state.qPageX, qPageY: this.state.qPageY, pageX: e.pageX, pageY: e.pageY, }); break; case Mode.createPoint: resizePoint({ gardenCoords: this.getGardenCoordinates(e), drawnPoint: this.props.designer.drawnPoint, dispatch: this.props.dispatch, isDragging: this.state.isDragging, type: "point", }); break; case Mode.createWeed: resizePoint({ gardenCoords: this.getGardenCoordinates(e), drawnPoint: this.props.designer.drawnWeed, dispatch: this.props.dispatch, isDragging: this.state.isDragging, type: "weed", }); break; case Mode.editGroup: resizeBox({ selectionBox: this.state.selectionBox, plants: this.props.plants, allPoints: this.props.allPoints, selectionPointType: this.props.designer.selectionPointType, getConfigValue: this.props.getConfigValue, gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: !this.props.designer.editGroupAreaInMap, }); break; case Mode.boxSelect: default: resizeBox({ selectionBox: this.state.selectionBox, plants: this.props.plants, allPoints: this.props.allPoints, selectionPointType: this.props.designer.selectionPointType, getConfigValue: this.props.getConfigValue, gardenCoords: this.getGardenCoordinates(e), setMapState: this.setMapState, dispatch: this.props.dispatch, plantActions: true, }); break; } } /** Return to garden (unless selecting more plants). */ closePanel = () => { switch (getMode()) { case Mode.moveTo: return () => { }; case Mode.boxSelect: return this.props.designer.selectedPoints ? () => { } : closePlantInfo(this.props.dispatch); default: return closePlantInfo(this.props.dispatch); } } mapDropAreaProps = () => ({ onDrop: this.handleDrop, onDragEnter: this.handleDragEnter, onDragOver: this.handleDragOver, onMouseLeave: this.endDrag, onMouseUp: this.endDrag, onDragEnd: this.endDrag, onDragStart: (e: React.DragEvent) => e.preventDefault(), style: { height: this.mapSize.h + "px", maxHeight: this.mapSize.h + "px", width: this.mapSize.w + "px", maxWidth: this.mapSize.w + "px" }, }); MapBackground = () => svgDropAreaProps = () => ({ x: this.props.gridOffset.x, y: this.props.gridOffset.y, width: this.xySwap ? this.props.gridSize.y : this.props.gridSize.x, height: this.xySwap ? this.props.gridSize.x : this.props.gridSize.y, onMouseUp: this.endDrag, onMouseDown: this.startDrag, onMouseMove: this.drag, onClick: this.click, }); ImageLayer = () => Grid = () => ZonesLayer = () => SensorReadingsLayer = () => SpreadLayer = () => PointLayer = () => WeedLayer = () => PlantLayer = () => ToolSlotLayer = () => FarmBotLayer = () => HoveredPlant = () => DragHelper = () => SelectionBox = () => TargetCoordinate = () => DrawnPoint = () => DrawnWeed = () => GroupOrder = () => NNPath = () => Bugs = () => showBugs() ? : /** Render layers in order from back to front. */ render() { return
; } }