diff --git a/client/public/assets/icon-loiter.png b/client/public/assets/icon-loiter.png new file mode 100644 index 00000000..7dbd95a0 Binary files /dev/null and b/client/public/assets/icon-loiter.png differ diff --git a/client/public/assets/icon-time.png b/client/public/assets/icon-time.png new file mode 100644 index 00000000..7dbd95a0 Binary files /dev/null and b/client/public/assets/icon-time.png differ diff --git a/client/public/assets/icon-turn.png b/client/public/assets/icon-turn.png new file mode 100644 index 00000000..9e2150e9 Binary files /dev/null and b/client/public/assets/icon-turn.png differ diff --git a/client/public/assets/icon-unlim.png b/client/public/assets/icon-unlim.png new file mode 100644 index 00000000..7d09bcc5 Binary files /dev/null and b/client/public/assets/icon-unlim.png differ diff --git a/client/src/commands.js b/client/src/commands.js new file mode 100644 index 00000000..a15c5c5e --- /dev/null +++ b/client/src/commands.js @@ -0,0 +1,10 @@ + +const Commands = { + waypoint: 16, + unlimLoiter: 17, + turnLoiter: 18, + timeLoiter: 19, + jump: 177 +} + +export default Commands \ No newline at end of file diff --git a/client/src/components/FlightMap.js b/client/src/components/FlightMap.js index 9c4e2f7a..1c6e23df 100644 --- a/client/src/components/FlightMap.js +++ b/client/src/components/FlightMap.js @@ -5,11 +5,41 @@ import { MapContainer, TileLayer, Tooltip, Marker, Polyline, Circle, LayersContr import { httpget } from "../backend.js" import L from "leaflet" +import Commands from "../commands.js" import PolylineDecorator from "../pages/FlightData/tabs/FlightPlan/PolylineDecorator.js" import RotatedMarker from "./RotatedMarker.js" import { useInterval } from "../util" import { Box, Button } from "components/UIElements" import { red } from "theme/Colors" +import { first, get } from "lodash" +import { promiseImpl } from "ejs" +import { hasSelectionSupport } from "@testing-library/user-event/dist/utils/index.js" + +// v is onChange value, value is validated value (i.e. your data because to have been set it must have been validated), set is setter +const signedFloatValidation = (v, value, set) => { + if (!Number.isNaN(Number(v)) && v.length > 0) { + if (v.endsWith(".")) { + set(null) + } else { + set(Number(v)) + } + return v + } else if (v.substring(0, v.length - 1).endsWith(".")) { + return v.substring(0, v.length - 1) + } else if (v.length === 0) { + set(null) + return v + } else if (v.substring(0, Math.max(v.length - 1, 1)) === "-") { + set(null) + return v.substring(0, Math.max(v.length - 1, 1)) + } else if (Number.isNaN(parseFloat(v))) { + return "" + } + + return value +} + +const EMPTY_JUMP = -1 const FlightPlanMap = props => { const [state, setState] = useState({ @@ -20,6 +50,8 @@ const FlightPlanMap = props => { const [icons, setIcons] = useState({}) const tileRef = useRef(null) + const [firstJump, setFirstJump] = useState(EMPTY_JUMP) + useEffect(() => { httpget("/interop/mission", response => { setState(response.data.result.mapCenterPos) @@ -51,7 +83,7 @@ const FlightPlanMap = props => { return { num: marker.num, cmd: marker.cmd, p1: marker.p1, p2: marker.p2, lat: marker.lat, lng: marker.lon, alt: marker.alt * 3.281 } // convert altitude from meters to feet }) props.setters.path(points) - props.setters.pathSave(points) + props.setters.pathSave(structuredClone(points)) }) var MarkerIcon = L.Icon.extend({ @@ -90,6 +122,10 @@ const FlightPlanMap = props => { searchGrid: new MarkerIcon({ iconUrl: "../assets/icon-searchGrid.png" }), path: new MarkerIcon({ iconUrl: "../assets/icon-path.png" }), home: new MarkerIcon({ iconUrl: "../assets/icon-home.png" }), + unlim: new MarkerIcon({ iconUrl: "../assets/icon-unlim.png" }), + time: new MarkerIcon({ iconUrl: "../assets/icon-time.png" }), + turn: new MarkerIcon({ iconUrl: "../assets/icon-turn.png" }), + jump: new MarkerIcon({ iconUrl: "../assets/icon-waypoints.png" }), uav: new VehicleIcon({ iconUrl: "../assets/uav.svg" }), uavDirection: new DirectionPointerIcon({ iconUrl: "../assets/pointer.svg" }), uavDirectionOutline: new DirectionPointerIcon({ iconUrl: "../assets/pointer-outline.svg" }), @@ -106,6 +142,10 @@ const FlightPlanMap = props => { }, []) + useEffect(() => { + setFirstJump(EMPTY_JUMP) + }, [props.mode, props.placementMode]) + const checkInternet = () => { if (navigator.onLine) { fetch("https://g.co", { @@ -138,19 +178,56 @@ const FlightPlanMap = props => { let get = props.getters["path"] let set = props.setters["path"] let temp = get.slice() + if (datatype == "unlim" || datatype == "time" || datatype == "turn") { + datatype = "path" + } let loc = { ...props.getters[datatype][idx], lat: event.target.getLatLng().lat, lng: event.target.getLatLng().lng, opacity: 0.5 } temp[idx] = loc set(temp) props.setSaved(false) } + const jumpClick = (key, datatype) => { + if (props.placementMode === "disabled" || props.mode !== "jump") { + return + } + if (datatype === "unlim" || datatype === "turn" || datatype === "time" || datatype === "path") { + if (firstJump === -1) { + setFirstJump(key) + } else { + let path = props.getters.path.slice() + let point = { num: firstJump + 1, cmd: Commands.jump, p1: key + (key < firstJump ? 0 : 1), p2: 3 } + props.setSaved(false) + props.setters.path([...path.slice(0, firstJump).map(p => { + if (p.cmd == Commands.jump) { + if (p.p1 > firstJump) { + p.p1 += 1 + } + } + + return p + }), point, ...path.slice(firstJump, path.length).map(p => { + p.num += 1 + if (p.p1 > firstJump) { + p.p1 += 1 + } + return p + })]) + setFirstJump(EMPTY_JUMP) + } + } + } + const popup = (latlng, key, datatype, popupMenu, draggable) => { return ( { handleMove(event, key - (props.getters.path[0].num === 0 ? 0 : 1), datatype) } + dragend: (event) => { handleMove(event, key - (props.getters.path[0].num === 0 ? 0 : 1), datatype) }, + click: () => { + jumpClick(key, datatype) + } }} onkeydown={event => handleKeyPress(event, key)} draggable={draggable} @@ -179,23 +256,23 @@ const FlightPlanMap = props => { } const handleClick = event => { + if (props.placementMode === "disabled" || props.mode === "jump") { + return + } if (props.mode) { let get = props.getters["path"] let set = props.setters["path"] - if (props.mode === "push" || (props.mode === "insert" && get.length < 2)) { + + if (props.placementMode === "push" || (props.placementMode === "insert" && get.length < 2)) { let temp = get.slice() - let point = { lat: event.latlng.lat, lng: event.latlng.lng, opacity: 0.5, num: get.length + (get[0]?.num === 0 ? -1 : 1) } - if (temp[temp.length - 1]?.cmd === 177) { - temp = [...temp.slice(0, temp.length - 1), point, temp[temp.length - 1]] - } else { - temp.push(point) - } + let point = { lat: event.latlng.lat, lng: event.latlng.lng, opacity: 0.5, num: get.length + (get[0]?.num === 0 ? -1 : 1), cmd: Commands[props.mode] } + temp.push(point) set(temp) - } else if (props.mode === "insert") { + } else if (props.placementMode === "insert") { const getPerpendicularDistance = (i) => { let first let second - if (get[i]?.cmd === 177) { + if (get[i]?.cmd === Commands.jump) { first = get[i - 1] second = get[get[i].p1 - (get[0].num === 0 ? 0 : 1)] } else { @@ -240,16 +317,13 @@ const FlightPlanMap = props => { } let path = get.slice() - if (get[min]?.cmd === 177) { - path = [...path.slice(0, min), { num: min, lat: event.latlng.lat, lng: event.latlng.lng, opacity: 0.5 }, ...(path.slice(min).map(point => ({ ...point, num: point.num + 1 })))] + if (get[min]?.cmd === Commands.jump) { + path = [...path.slice(0, min), { num: min, lat: event.latlng.lat, lng: event.latlng.lng, opacity: 0.5, cmd: Commands[props.mode] }, ...(path.slice(min).map(point => ({ ...point, num: point.num + 1 })))] } else { - path = [...path.slice(0, min + 1), { num: min + (get[0]?.num === 0 ? 1 : 2), lat: event.latlng.lat, lng: event.latlng.lng, opacity: 0.5 }, ...(path.slice(min + 1).map(point => ({ ...point, num: point.num + 1 })))] + path = [...path.slice(0, min + 1), { num: min + (get[0]?.num === 0 ? 1 : 2), lat: event.latlng.lat, lng: event.latlng.lng, opacity: 0.5, cmd: Commands[props.mode] }, ...(path.slice(min + 1).map(point => ({ ...point, num: point.num + 1 })))] } set(path) } - if (props.saved && props.mode !== "disabled") { - props.setSaved(false) - } } } @@ -267,6 +341,67 @@ const FlightPlanMap = props => { return null; }; + const MarkerPopup = ({ marker, i }) => { + return ( +
+ Altitude (feet) + signedFloatValidation(v, marker.alt, (k) => { + let path = props.getters.path + props.setters.path([...path.slice(0, i), { ...marker, alt: k }, ...path.slice(i + 1)]) + })} /> + +
+ ) + } + return (
{ })} - + {props.getters.ugvDrop.lat == null ? null : singlePopup(props.getters.ugvDrop, "ugvDrop")} @@ -364,49 +499,52 @@ const FlightPlanMap = props => { - (marker.num !== 0) && (marker.cmd !== 177))} color="#10336B" decoratorColor="#1d5cc2" /> + marker.cmd !== Commands.jump)} color="#10336B" decoratorColor="#1d5cc2" /> {props.getters.path.map((marker, i) => { - if (marker.num === 0) { - return singlePopup(marker, "home") - } else if (marker.cmd === 177) { - return + + if (marker.cmd === Commands.jump) { + let j = i - 1 + if (!props.getters.path[i - 1].lat) { + while (j >= 0 && !props.getters.path[j].lat) { + j-- + } + } + return ( + <> + + {popup({...marker, lng: (props.getters.path[j].lng + props.getters.path[marker.p1 - 1].lng)/2, lat: (props.getters.path[j].lat + props.getters.path[marker.p1 - 1].lat)/2}, marker.num, "jump", ( +
+ Jump from {i} to {marker.p1} + +
+ ), true)} + + ) + } else if (marker.cmd === Commands.unlimLoiter) { + return popup(marker, marker.num, "unlim", ( +
+ Unlimited Loiter Point + +
+ ), true) + } else if (marker.cmd === Commands.turnLoiter) { + return popup(marker, marker.num, "turn", ( +
+ Turn Loiter + +
+ ), true) + } else if (marker.cmd === Commands.timeLoiter) { + return popup(marker, marker.num, "time", ( +
+ Time Loiter + +
+ ), true) } return popup(marker, marker.num, "path", ( -
- Altitude (feet) - { - let path = props.getters.path; - if (!Number.isNaN(Number(v)) && v.length > 0) { - if (v.endsWith(".")) { - props.setters.path([...path.slice(0, i), { ...marker, alt: null }, ...path.slice(i + 1)]) - } else { - props.setters.path([...path.slice(0, i), { ...marker, alt: Number(v) }, ...path.slice(i + 1)]) - } - return v - } else if (v.substring(0, v.length - 1).endsWith(".")) { - return v.substring(0, v.length - 1) - } else if (v.length === 0) { - props.setters.path([...path.slice(0, i), { ...marker, alt: null }, ...path.slice(i + 1)]) - return v - } else if (v.substring(0, Math.max(v.length - 1, 1)) === "-") { - props.setters.path([...path.slice(0, i), { ...marker, alt: null }, ...path.slice(i + 1)]) - return v.substring(0, Math.max(v.length - 1, 1)) - } else if (Number.isNaN(parseFloat(v))) { - return "" - } - - return marker.altitude - }} /> - -
+ ), true) })}
@@ -417,4 +555,4 @@ const FlightPlanMap = props => { ) } -export default FlightPlanMap +export default FlightPlanMap \ No newline at end of file diff --git a/client/src/components/UIElements/Button.js b/client/src/components/UIElements/Button.js index 61fdc00a..42644ec3 100644 --- a/client/src/components/UIElements/Button.js +++ b/client/src/components/UIElements/Button.js @@ -8,7 +8,7 @@ import Link from "./Link" import { ReactComponent as RawWarning } from "icons/warning.svg" import { unselectable } from "css.js" -const Button = forwardRef(({ active, onChange, controlled, to, href, careful = false, ...props }, ref) => { +const Button = forwardRef(({ active, disabled, onChange, onClick, controlled, to, href, careful = false, ...props }, ref) => { const [isActive, setActive] = useState(active ?? false) return ( @@ -16,16 +16,18 @@ const Button = forwardRef(({ active, onChange, controlled, to, href, careful = f ref={ref} className="paragraph" active={controlled ? active : isActive} + disabled={disabled} onMouseDown={() => { - if (!controlled) setActive(true) + if (!disabled) setActive(true) }} onMouseUp={() => { - if (!controlled) + if (!disabled) setTimeout(() => { if (!careful) setActive(false) if (ref?.current) setActive(false) if (onChange) onChange() - }, 100) + if (onClick) onClick() + }, 50) }} to={to} href={href} @@ -65,9 +67,9 @@ export const StyledButton = styled(Link).attrs(props => ({ ${unselectable} position: relative; box-sizing: border-box; - background: ${props => (props.active ? (props.color ?? blue) : dark)}; + background: ${props => (props.active && !props.disabled ? (props.color ?? blue) : dark)}; transition: background-color 0.1s ease; - color: ${props => (props.active ? dark : (props.color ?? blue))} !important; + color: ${props => (props.active ? dark : (!props.disabled ? props.color ?? blue : "grey"))} !important; text-decoration: none !important; display: flex; justify-content: center; @@ -75,7 +77,7 @@ export const StyledButton = styled(Link).attrs(props => ({ text-align: center; padding-top: ${props => (props.large ? "1rem" : "0.3rem")}; padding-bottom: ${props => (props.large ? "1rem" : "0.3rem")}; - cursor: pointer; + cursor: ${props => props.disabled ? "not-allowed" : "pointer"}; ::after { content: ""; @@ -84,7 +86,7 @@ export const StyledButton = styled(Link).attrs(props => ({ right: 0; bottom: 0; height: 0.25rem; - background: ${props => props.color ?? blue}; + background: ${props => (props.disabled ? "grey" : props.color) ?? blue}; transition: height 0.1s ease; } @@ -94,8 +96,8 @@ export const StyledButton = styled(Link).attrs(props => ({ left: 0; right: 0; bottom: 0; - height: 0.5rem; - background: ${props => props.color ?? blue}; + height: ${props => !props.disabled ? "0.5rem" : "0.25rem"}; + background: ${props => (props.disabled ? "grey" : props.color) ?? blue}; } ` diff --git a/client/src/components/UIElements/Switch.js b/client/src/components/UIElements/Switch.js new file mode 100644 index 00000000..2767e246 --- /dev/null +++ b/client/src/components/UIElements/Switch.js @@ -0,0 +1,9 @@ +import React, { useState } from "react" + +const Switch = (props) => { + return ( + <> + ) +} + +export default Switch \ No newline at end of file diff --git a/client/src/pages/FlightData/index.js b/client/src/pages/FlightData/index.js index 108e9280..4affde0e 100644 --- a/client/src/pages/FlightData/index.js +++ b/client/src/pages/FlightData/index.js @@ -25,7 +25,9 @@ TODO: Display list highlighting (and vice versa) */ const FlightData = () => { - const [mode, setMode] = useState("disabled") + const [mode, setMode] = useState("waypoint") + const [placementMode, setPlacementMode] = useState("push") + const [previousMode, setPreviousMode] = useState("disabled") const [saved, setSaved] = useState(true) const [defaultAlt, setDefaultAlt] = useState(100) @@ -87,9 +89,13 @@ const FlightData = () => { obstacles: "Obstacles", offAxis: "Off Axis ODLC", searchGrid: "ODLC Search Grid", + loiter: "loiter", path: "Mission Path", + unlim: "Unlimited Loiter", + turn: "Turn Loiter", + time: "Time Loiter", + jump: "Jump", uav: "UAV", - ugv: "UGV", home: "Home Waypoint" } @@ -131,9 +137,13 @@ const FlightData = () => { getters={getters} setters={setters} mode={mode} + setMode={setMode} + previousMode={previousMode} + setPreviousMode={setPreviousMode} + placementMode={placementMode} + setPlacementMode={setPlacementMode} saved={saved} setSaved={setSaved} - setMode={setMode} tabName={"Map"} /> @@ -144,6 +154,10 @@ const FlightData = () => { setters={setters} mode={mode} saved={saved} + previousMode={previousMode} + setPreviousMode={setPreviousMode} + placementMode={placementMode} + setPlacementMode={setPlacementMode} setSaved={setSaved} setMode={setMode} /> diff --git a/client/src/pages/FlightData/tabs/FlightPlan/FlightPlanToolbar.js b/client/src/pages/FlightData/tabs/FlightPlan/FlightPlanToolbar.js index 1fa7dafe..e4f71916 100644 --- a/client/src/pages/FlightData/tabs/FlightPlan/FlightPlanToolbar.js +++ b/client/src/pages/FlightData/tabs/FlightPlan/FlightPlanToolbar.js @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react" -import { Box, Button, RadioList } from "components/UIElements" +import { Box, Button, Dropdown, RadioList } from "components/UIElements" import { red } from "theme/Colors" import { httppost } from "backend" import { Modal, ModalHeader, ModalBody } from "components/Containers" +import Commands from "commands" const FlightPlanToolbar = props => { const [open, setOpen] = useState(false) @@ -25,7 +26,7 @@ const FlightPlanToolbar = props => { props.setSaved(true) props.setters.pathSave(path) - httppost("/uav/commands/generate", { "waypoints": path.map(waypoint => ({ ...waypoint, lon: waypoint.lng, alt: waypoint.alt / 3.281 })) }) // convert feet to meters for altitude + httppost("/uav/commands/generate", { "waypoints": path.map(waypoint => ({ ...waypoint, lat: waypoint.lat ?? 0.0, lon: waypoint.lng ?? 0.0, alt: (waypoint.alt ?? 0.0) / 3.281 })) }) // convert feet to meters for altitude } return ( @@ -43,10 +44,21 @@ const FlightPlanToolbar = props => { savePath(path) }}>Set as default ({props.getters.defaultAlt} ft) - props.setMode(event.target.value)} name="pointMode"> - Don't make points - Push Mode - Insertion Mode +
+ { + props.setPlacementMode(v) + }}> + Disable + Push + Insert + +
+ { props.setMode(event.target.value); console.log(event.target.value) }} name="pointMode"> + Waypoints + Add Jump + Unlimited Loiter + Turn Loiter + Time Loiter
Default Altitude (ft): @@ -73,42 +85,51 @@ const FlightPlanToolbar = props => { return props.getters.defaultAlt }} />
- {props.getters.path.length === 0 ? null : - - } - {props.saved == true ? null : ( -
- +
+ - + savePath(props.getters.path) + }}>Click to save + + {props.saved ? null : You have unsaved points! -
- )} - {props.getters.path.length === 0 ? ( - - ) : null} + } +
+ +
+ + +
) }