diff --git a/README.md b/README.md index 6debf2d..302461d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Supported modules: * AV8BNA Harrier * Ka-50 Blackshark * AH-64D Apache (Pilot and CP/G) +* OH-58D Kiowa Warrior * Mirage F1EE * UH-60L Blackhawk * Hercules (Requires patch available at https://github.com/Summit60/DCS-Hercules-TheWay-patch) diff --git a/electron/TCPSender.js b/electron/TCPSender.js index fddc5a3..daea898 100644 --- a/electron/TCPSender.js +++ b/electron/TCPSender.js @@ -8,6 +8,7 @@ class TCPSender { client .connect(42070, "127.0.0.1", function () { client.write(JSON.stringify(msg) + "\n"); + client.end(); }) .on("error", (e) => { console.log(e); diff --git a/package-lock.json b/package-lock.json index 7b2fe6f..27a1a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "theway", - "version": "2.4.2", + "version": "2.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "theway", - "version": "2.4.2", + "version": "2.5.0", "dependencies": { "electron-devtools-installer": "^3.2.0", "electron-is-dev": "^2.0.0", diff --git a/package.json b/package.json index e06433a..d744cd2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theway", - "version": "2.4.2", + "version": "2.5.0", "private": true, "main": "electron/electron.js", "dependencies": { diff --git a/public/assets/moduleImages/OH58D.jpg b/public/assets/moduleImages/OH58D.jpg new file mode 100644 index 0000000..2b60442 Binary files /dev/null and b/public/assets/moduleImages/OH58D.jpg differ diff --git a/src/App.js b/src/App.js index 2714598..017793e 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from "react-redux"; import ModalContainer from "react-modal-promise"; import SourceSelector from "./components/SourceSelector"; -import WaypointList from "./components/WaypointList"; +import WaypointList from "./components/waypoints/WaypointList"; import theWayTheme from "./theme/TheWayTheme"; import TransferControls from "./components/TransferControls"; import TitleBar from "./components/TitleBar"; diff --git a/src/components/WaypointItem.js b/src/components/WaypointItem.js deleted file mode 100644 index d59db39..0000000 --- a/src/components/WaypointItem.js +++ /dev/null @@ -1,157 +0,0 @@ -import { - Button, - IconButton, - ListItem, - ListItemText, - Stack, - Grid, - TextField, - Input, - Box, - Tooltip, - Typography, - Collapse, - Grow, -} from "@mui/material"; -import { - DragHandle, - ArrowDropDown, - ArrowDropUp, - Delete, -} from "@mui/icons-material"; -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; - -import "./WaypointItem.css"; - -const { ipcRenderer } = window.require("electron"); - -const WaypointItem = (props) => { - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id: props.id }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; - - const handleInputFocus = () => { - ipcRenderer.send("focus"); - }; - const handleInputDefocus = (event) => { - ipcRenderer.send("defocus"); - event.target.blur(); - }; - const handleInputFinished = (event) => { - if (event.key === "Enter") { - props.onExpand(props.id, !props.expanded); - handleInputDefocus(); - } - }; - - return ( - - - - - - - {props.expanded ? ( - props.onRename(e, props.id)} - onMouseEnter={handleInputFocus} - onMouseLeave={handleInputDefocus} - onKeyDown={handleInputFinished} - onFocus={(e) => e.target.select()} - /> - ) : ( - {props.name} - )} - - - - - - LAT - - - - - - - - - - - LONG - - - - - - - - - - - ELEV - - - - props.onElevation(e, props.id)} - onMouseEnter={handleInputFocus} - onMouseLeave={handleInputDefocus} - onKeyDown={handleInputFinished} - > - - - - - - - - {props.pending ? ( - - - - - - ) : ( - - props.onDelete(e, props.id)}> - - - props.onExpand(props.id, !props.expanded)} - > - {props.expanded ? : } - - - - - - )} - - - - - ); -}; - -export default WaypointItem; diff --git a/src/components/waypoints/WaypointFieldsLatLong.js b/src/components/waypoints/WaypointFieldsLatLong.js new file mode 100644 index 0000000..6873078 --- /dev/null +++ b/src/components/waypoints/WaypointFieldsLatLong.js @@ -0,0 +1,59 @@ +import { Box, Grid, Input, Typography } from "@mui/material"; + +const WaypointFieldsLatLong = ({ + id, + latHem, + longHem, + lat, + long, + elev, + onElevation, + handleInputFocus, + handleInputDefocus, + handleInputFinished, +}) => { + return ( + + + + + LAT + + + + + + + + + + + LONG + + + + + + + + + + + ELEV + + + + onElevation(e, id)} + onMouseEnter={handleInputFocus} + onMouseLeave={handleInputDefocus} + onKeyDown={handleInputFinished} + > + + + + ); +}; + +export default WaypointFieldsLatLong; diff --git a/src/components/waypoints/WaypointFieldsMGRS.js b/src/components/waypoints/WaypointFieldsMGRS.js new file mode 100644 index 0000000..f08f6c1 --- /dev/null +++ b/src/components/waypoints/WaypointFieldsMGRS.js @@ -0,0 +1,45 @@ +import { Box, Grid, Input, Typography } from "@mui/material"; + +const WaypointFieldsMGRS = ({ + id, + mgrs, + elev, + onElevation, + handleInputFocus, + handleInputDefocus, + handleInputFinished, +}) => { + return ( + + + + + MGRS + + + + + + + + + + + ELEV + + + + onElevation(e, id)} + onMouseEnter={handleInputFocus} + onMouseLeave={handleInputDefocus} + onKeyDown={handleInputFinished} + > + + + + ); +}; + +export default WaypointFieldsMGRS; diff --git a/src/components/WaypointItem.css b/src/components/waypoints/WaypointItem.css similarity index 100% rename from src/components/WaypointItem.css rename to src/components/waypoints/WaypointItem.css diff --git a/src/components/waypoints/WaypointItem.js b/src/components/waypoints/WaypointItem.js new file mode 100644 index 0000000..a0e1434 --- /dev/null +++ b/src/components/waypoints/WaypointItem.js @@ -0,0 +1,145 @@ +import { + Button, + IconButton, + ListItem, + ListItemText, + Stack, + Grid, + TextField, + Box, + Tooltip, + Collapse, + Grow, +} from "@mui/material"; +import { + DragHandle, + ArrowDropDown, + ArrowDropUp, + Delete, +} from "@mui/icons-material"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import "./WaypointItem.css"; +import WaypointFieldsMGRS from "./WaypointFieldsMGRS"; +import WaypointFieldsLatLong from "./WaypointFieldsLatLong"; + +const { ipcRenderer } = window.require("electron"); + +const WaypointItem = ({ + wp, + pending, + onSave, + onDelete, + onRename, + expanded, + onExpand, + onElevation, +}) => { + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: wp?.id }); + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + const handleInputFocus = () => { + ipcRenderer.send("focus"); + }; + const handleInputDefocus = (event) => { + ipcRenderer.send("defocus"); + event.target.blur(); + }; + const handleInputFinished = (event) => { + if (event.key === "Enter") { + onExpand(wp?.id, !expanded); + handleInputDefocus(); + } + }; + + return ( + + + + + + + {expanded ? ( + onRename(e, wp?.id)} + onMouseEnter={handleInputFocus} + onMouseLeave={handleInputDefocus} + onKeyDown={handleInputFinished} + onFocus={(e) => e.target.select()} + /> + ) : ( + {wp?.name || "New Waypoint"} + )} + + {wp?.MGRS ? ( + + ) : ( + + )} + + + + + {pending ? ( + + + + + + ) : ( + + onDelete(e, wp?.id)}> + + + onExpand(wp?.id, !expanded)}> + {expanded ? : } + + + + + + )} + + + + + ); +}; + +export default WaypointItem; diff --git a/src/components/WaypointList.js b/src/components/waypoints/WaypointList.js similarity index 82% rename from src/components/WaypointList.js rename to src/components/waypoints/WaypointList.js index e13badd..56fd066 100644 --- a/src/components/WaypointList.js +++ b/src/components/waypoints/WaypointList.js @@ -1,8 +1,8 @@ import { Box, Button, Card, List, Typography } from "@mui/material"; import WaypointItem from "./WaypointItem"; import { useDispatch, useSelector } from "react-redux"; -import { waypointsActions } from "../store/waypoints"; -import Convertors from "../utils/Convertors"; +import { waypointsActions } from "../../store/waypoints"; +import Convertors from "../../utils/Convertors"; import { closestCenter, DndContext } from "@dnd-kit/core"; import { restrictToVerticalAxis, @@ -13,7 +13,7 @@ import { SortableContext, verticalListSortingStrategy, } from "@dnd-kit/sortable"; -import ConvertModuleWaypoints from "../utils/ConvertModuleWaypoints"; +import ConvertModuleWaypoints from "../../utils/ConvertModuleWaypoints"; import { useEffect, useRef, useState } from "react"; const WaypointList = () => { @@ -56,8 +56,11 @@ const WaypointList = () => { }; const elevationHandler = (event, id) => { - const elev = Convertors.fToM(event.target.value); - dispatch(waypointsActions.changeElevation({ id, elev })); + const enteredValue = event.target.value; + if (!isNaN(enteredValue)) { + const elev = Convertors.fToM(event.target.value); + dispatch(waypointsActions.changeElevation({ id, elev })); + } }; const expandHandler = (id, isExpanded) => { @@ -103,14 +106,8 @@ const WaypointList = () => { {moduleCoordinates.map((wp) => ( { ))} {isPending && ( - + )} ) : ( diff --git a/src/moduleCommands/GetModuleCommands.js b/src/moduleCommands/GetModuleCommands.js index 9848ca8..2d616e6 100644 --- a/src/moduleCommands/GetModuleCommands.js +++ b/src/moduleCommands/GetModuleCommands.js @@ -9,6 +9,7 @@ import ka50 from "./ka50"; import miragef1 from "./miragef1"; import uh60l from "./uh60l"; import hercules from "./Hercules"; +import oh58d from "./oh58d"; export default function getModuleCommands(module, waypoints, buttonExtraDelay) { switch (module) { @@ -77,6 +78,16 @@ export default function getModuleCommands(module, waypoints, buttonExtraDelay) { hercules.extraDelay = buttonExtraDelay; return hercules.createButtonCommands(waypoints); } + case "OH58Dright-seat": { + oh58d.extraDelay = buttonExtraDelay; + oh58d.slotVariant = "OH58Dright-seat"; + return oh58d.createButtonCommands(waypoints); + } + case "OH58Dleft-seat": { + oh58d.extraDelay = buttonExtraDelay; + oh58d.slotVariant = "OH58Dleft-seat"; + return oh58d.createButtonCommands(waypoints); + } default: return []; } diff --git a/src/moduleCommands/askUserAboutSeat.js b/src/moduleCommands/askUserAboutSeat.js index ad23eaa..1af3b6d 100644 --- a/src/moduleCommands/askUserAboutSeat.js +++ b/src/moduleCommands/askUserAboutSeat.js @@ -76,6 +76,20 @@ const askUserAboutSeat = async (module, userPreferences) => { "Make sure you have downloaded the Patch for the Hercules module: https://github.com/Summit60/DCS-Hercules-TheWay-patch", }).then(() => "Hercules"); } + } else if (module === "OH58D") { + if (moduleSpecificPreferences?.includes("Right Seat")) + return "OH58Dright-seat"; + else if (moduleSpecificPreferences?.includes("Left Seat")) + return "OH58Dleft-seat"; + else { + return TwoOptionsDialog({ + title: "What seat are you in?", + op1: "Right Seat", + op2: "Left Seat", + }).then((option) => + option === "Left Seat" ? "OH58Dleft-seat" : "OH58Dright-seat", + ); + } } else return module; }; diff --git a/src/moduleCommands/oh58d.js b/src/moduleCommands/oh58d.js new file mode 100644 index 0000000..81022f8 --- /dev/null +++ b/src/moduleCommands/oh58d.js @@ -0,0 +1,171 @@ +import createButtonPress from "../models/ButtonPress"; + +class oh58d { + static extraDelay = 100; + static #delay100 = 100 + this.extraDelay; + static slotVariant = ""; + static #oh58dkeycodes = { + // Keyboard keys (device 14) + a: 3063, + b: 3064, + c: 3065, + d: 3066, + e: 3067, + f: 3068, + g: 3069, + h: 3070, + i: 3071, + j: 3072, + k: 3073, + l: 3074, + m: 3075, + n: 3076, + o: 3077, + p: 3078, + q: 3079, + r: 3080, + s: 3081, + t: 3082, + u: 3083, + v: 3084, + w: 3085, + x: 3086, + y: 3087, + z: 3088, + // number keys + 0: 3062, + 1: 3053, + 2: 3054, + 3: 3055, + 4: 3056, + 5: 3057, + 6: 3058, + 7: 3059, + 8: 3060, + 9: 3061, + // special keys + " ": 3094, + ".": 3090, + "-": 3095, + enter: 3089, + clear: 3091, + // pilot MFD (device 11) copilot MFD (device 23) + vsd: 3008, + hsd: 3009, + l1: 3001, + l2: 3002, + l3: 3003, + l4: 3004, + l5: 3005, + + r1: 3013, + r2: 3014, + r3: 3015, + r4: 3016, + r5: 3017, + }; + static #codesPayload = []; + + static #isPilot() { + return this.slotVariant === "OH58Dright-seat"; + } + + static createButtonCommands(waypoints) { + this.#codesPayload = []; + this.#codesPayload.push( + // enter nav setup on pilot MFD + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["hsd"], + this.#delay100, + ), + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["r2"], + this.#delay100, + ), + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["l4"], + this.#delay100, + ), + ); + + for (const waypoint of waypoints) { + this.#codesPayload.push( + // enter waypoint + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["l2"], + this.#delay100, + ), + createButtonPress(14, this.#oh58dkeycodes["clear"], this.#delay100), + ); + const [zone, squareId, easting, northing] = waypoint.MGRS.split(" "); + let convertedWaypoint = `${zone} ${squareId} ${easting.slice( + 0, + 4, + )} ${northing.slice(0, 4)}`; + for (let i = 0; i < convertedWaypoint.length; i++) { + if (convertedWaypoint.charAt(i) !== " ") { + this.#codesPayload.push( + // enter each digit of MGRS into scratchpad + createButtonPress( + 14, + this.#oh58dkeycodes[convertedWaypoint.charAt(i).toLowerCase()], + this.#delay100, + ), + ); + } + } + this.#codesPayload.push( + // enter MGRS into UFC + createButtonPress(14, this.#oh58dkeycodes["enter"], this.#delay100), + ); + + this.#codesPayload.push( + // select altitude on mfd + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["l4"], + this.#delay100, + ), + createButtonPress(14, this.#oh58dkeycodes["clear"], this.#delay100), // clear current alt + ); + + for (let i = 0; i < waypoint.elev.length; i++) { + // enter in waypoint elevation + this.#codesPayload.push( + createButtonPress( + 14, + this.#oh58dkeycodes[waypoint.elev.charAt(i).toLowerCase()], + this.#delay100, + ), + ); + } + + this.#codesPayload.push( + // press enter on scratchpad + createButtonPress(14, this.#oh58dkeycodes["enter"], this.#delay100), + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["r5"], + this.#delay100, + ), + ); + } + + this.#codesPayload.push( + // exit nav setup on pilot MFD + createButtonPress( + this.#isPilot() ? 11 : 23, + this.#oh58dkeycodes["vsd"], + this.#delay100, + ), + ); + + return this.#codesPayload; + } +} + +export default oh58d; diff --git a/src/utils/ConvertModuleWaypoints.js b/src/utils/ConvertModuleWaypoints.js index 44cff80..08346dd 100644 --- a/src/utils/ConvertModuleWaypoints.js +++ b/src/utils/ConvertModuleWaypoints.js @@ -20,6 +20,7 @@ const convert = (dcsWaypoints, module) => { //long 000.00.000 let waypoints = []; for (const dcsWaypoint of dcsWaypoints) { + const name = dcsWaypoint.name; const id = dcsWaypoint.id; const dmmLat = Convertors.decimalToDMM(dcsWaypoint.lat); @@ -190,6 +191,22 @@ const convert = (dcsWaypoints, module) => { } return waypoints; } + case "OH58D": { + let waypoints = []; + for (const dcsWaypoint of dcsWaypoints) { + const name = dcsWaypoint.name; + const id = dcsWaypoint.id; + const MGRS = Convertors.decimalToMGRS(dcsWaypoint.lat, dcsWaypoint.long); + const elev = Math.trunc(Convertors.mToF(dcsWaypoint.elev)).toString(); + waypoints.push({ + name, + id, + MGRS, + elev, + }); + } + return waypoints; + } } }; diff --git a/src/utils/Convertors.js b/src/utils/Convertors.js index d46eece..a924578 100644 --- a/src/utils/Convertors.js +++ b/src/utils/Convertors.js @@ -21,6 +21,73 @@ class Convertors { static fToM(f) { return f / 3.280839895; } + + static decimalToMGRS(Lat, Long) { + // source: https://stackoverflow.com/questions/46728319/how-to-convert-between-lat-long-and-mgrs-using-javascript-without-dependence-on + if (Lat < -80) return "Too far South"; + if (Lat > 84) return "Too far North"; + const c = 1 + Math.floor((Long + 180) / 6); + const e = c * 6 - 183; + const k = (Lat * Math.PI) / 180; + const l = (Long * Math.PI) / 180; + const m = (e * Math.PI) / 180; + const n = Math.cos(k); + const o = 0.006739496819936062 * Math.pow(n, 2); + const p = 40680631590769 / (6356752.314 * Math.sqrt(1 + o)); + const q = Math.tan(k); + const r = q * q; + const t = l - m; + const u = 1.0 - r + o; + const v = 5.0 - r + 9 * o + 4.0 * (o * o); + const w = 5.0 - 18.0 * r + r * r + 14.0 * o - 58.0 * r * o; + const x = 61.0 - 58.0 * r + r * r + 270.0 * o - 330.0 * r * o; + const y = 61.0 - 479.0 * r + 179.0 * (r * r) - r * r * r; + const z = 1385.0 - 3111.0 * r + 543.0 * (r * r) - r * r * r; + let aa = + p * n * t + + (p / 6.0) * Math.pow(n, 3) * u * Math.pow(t, 3) + + (p / 120.0) * Math.pow(n, 5) * w * Math.pow(t, 5) + + (p / 5040.0) * Math.pow(n, 7) * y * Math.pow(t, 7); + let ab = + 6367449.14570093 * + (k - + 0.00251882794504 * Math.sin(2 * k) + + 0.00000264354112 * Math.sin(4 * k) - + 0.00000000345262 * Math.sin(6 * k) + + 0.000000000004892 * Math.sin(8 * k)) + + (q / 2.0) * p * Math.pow(n, 2) * Math.pow(t, 2) + + (q / 24.0) * p * Math.pow(n, 4) * v * Math.pow(t, 4) + + (q / 720.0) * p * Math.pow(n, 6) * x * Math.pow(t, 6) + + (q / 40320.0) * p * Math.pow(n, 8) * z * Math.pow(t, 8); + aa = aa * 0.9996 + 500000.0; + ab = ab * 0.9996; + if (ab < 0.0) ab += 10000000.0; + const ad = "CDEFGHJKLMNPQRSTUVWXX".charAt(Math.floor(Lat / 8 + 10)); + const ae = Math.floor(aa / 100000); + const af = ["ABCDEFGH", "JKLMNPQR", "STUVWXYZ"][(c - 1) % 3].charAt(ae - 1); + const ag = Math.floor(ab / 100000) % 20; + const ah = ["ABCDEFGHJKLMNPQRSTUV", "FGHJKLMNPQRSTUVABCDE"][ + (c - 1) % 2 + ].charAt(ag); + + function pad(val) { + if (val < 10) { + val = "0000" + val; + } else if (val < 100) { + val = "000" + val; + } else if (val < 1000) { + val = "00" + val; + } else if (val < 10000) { + val = "0" + val; + } + return val; + } + aa = Math.floor(aa % 100000); + aa = pad(aa); + ab = Math.floor(ab % 100000); + ab = pad(ab); + return c + ad + " " + af + ah + " " + aa + " " + ab; + } } export default Convertors; diff --git a/src/utils/constants.js b/src/utils/constants.js index d64eaa3..cd9d5b3 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -25,4 +25,5 @@ export const supportedModules = [ "Mirage-F1EE", "UH-60L", "Hercules", + "OH58D", ]; diff --git a/testing/debuggingStateKiowa.json b/testing/debuggingStateKiowa.json new file mode 100644 index 0000000..dc68098 --- /dev/null +++ b/testing/debuggingStateKiowa.json @@ -0,0 +1,3 @@ +{ + "payload": "[{\"type\":\"dcsPoint/changeCoords\",\"payload\":{\"model\":\"OH58D\",\"coords\":{\"lat\":\"41.905925545347\",\"long\":\"43.783457188399\"},\"elev\":\"1677.6647949219\"}},{\"type\":\"waypoints/addDcsWaypoint\",\"payload\":{\"lat\":41.905925545347,\"long\":43.783457188399,\"elev\":1677.6647949219}},{\"type\":\"waypoints/addDcsWaypoint\",\"payload\":{\"lat\":41.905925545347,\"long\":43.783457188399,\"elev\":1677.6647949219}},{\"type\":\"waypoints/addDcsWaypoint\",\"payload\":{\"lat\":41.905925545347,\"long\":43.783457188399,\"elev\":1677.6647949219}}]" +}