diff --git a/client/package.json b/client/package.json index f96322ca..80775352 100644 --- a/client/package.json +++ b/client/package.json @@ -5,8 +5,8 @@ "version": "0.0.0", "private": true, "scripts": { - "start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"", "copy": "copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet.css", + "start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"", "watch": "watchify .\\src\\index.ts --debug -p [ tsify --noImplicitAny ] -o .\\public\\javascripts\\bundle.js" }, "dependencies": { diff --git a/client/public/images/formations/range.png b/client/public/images/formations/range.png index fdcf8b0d..9df137cb 100644 Binary files a/client/public/images/formations/range.png and b/client/public/images/formations/range.png differ diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index cd9d5b1e..63ccc67b 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -1,7 +1,7 @@ /* Page style */ body { - display: flex; + display: grid; margin: 0; padding: 0; } @@ -43,6 +43,33 @@ body { } +/**************************************/ + +.olympus-dialog { + align-self: center; + background:white; + border-radius: 10px; + display: flex; + flex-direction: column; + justify-self: center; + padding:10px; + position:absolute; + width:fit-content; + z-index: 9999; +} + +.olympus-dialog-close { + cursor:pointer; + position:absolute; + right:10px; + top:5px; +} + +.olympus-dialog-header { + font-weight:bold; +} + + /**************************************/ .control-panel { @@ -69,29 +96,49 @@ body { /***** AIC *****/ #aic-control-panel { - color:white; left: 550px; } -.aic-enabled #aic-control-panel .olympus-button { +#aic-control-panel .olympus-button { filter:invert(100%); } -#aic-formation-panel { +#aic-toolbox, #aic-callsign-panel { + align-items: flex-start; align-self: center; - background:#eaeaea; - border-bottom-right-radius: 10px; - border-top-right-radius: 10px; + flex-direction: column; + row-gap: 10px; display:none; - justify-self: left; - padding:10px; position:absolute; } -.aic-enabled #aic-formation-panel { +.aic-panel { + background:#eaeaea; + border-bottom-right-radius: 10px; + border-top-right-radius: 10px; + justify-self: left; + padding:5px 10px; +} + +.aic-enabled #aic-toolbox, .aic-enabled #aic-callsign-panel { display:flex; - flex-direction: column; +} + +.aic-enabled #aic-callsign-panel { + align-self: auto; + top: 100px; +} + +.aic-panel h2 { + font-size:90%; + margin:0; + padding:0; + text-align: center; +} + +#aic-callsign-display { + text-align: center; } #aic-formation-list { @@ -102,16 +149,95 @@ body { #aic-formation-list > div { align-items: center; + cursor: pointer; display:flex; flex-direction: column; justify-content: center; - margin-top:1em; + margin-top:10px; + position:relative; } #aic-formation-list .aic-formation-image img { border: 1px solid #ccc; border-radius: 10px; - max-width: 75px; + max-width: 50px; +} + +#aic-formation-list .aic-formation-name { + font-size:90%; +} + +#aic-formation-list .aic-formation-descriptor { + background:white; + border-radius: 10px; + left:100px; + padding:5px; + position:absolute; + width: max-content; +} + +#aic-teleprompt { + background-color: white; + border:2px solid black; + border-radius: 10px; + bottom: 50px; + color: black; + display: none; + justify-content: center; + justify-self: center; + padding: 10px; + position: absolute; + width: fit-content; + z-index: 1000; +} + +.aic-enabled #aic-teleprompt { + display:flex; +} + +#aic-descriptor { + display:flex; + flex-direction: row; +} + +#aic-descriptor .aic-descriptor-section { + display:flex; + flex-direction: column; + margin:0 10px; +} + +#aic-descriptor .aic-descriptor-section-label { + background-color:#eaeaea; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + padding:.25em; + text-align: center; +} + +#aic-descriptor .aic-descriptor-components { + display:flex; + flex-direction: row; +} + +#aic-descriptor .aic-descriptor-components .aic-descriptor-component { + margin:0 5px; + text-align: center; +} + +#aic-descriptor .aic-descriptor-component-label { + display:none; +} + +#aic-descriptor .aic-descriptor-component-value:after { + content:","; +} + +#aic-descriptor .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after { + content:";"; +} + +#aic-descriptor .aic-descriptor-section:last-of-type .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after { + content:"."; } @@ -162,4 +288,12 @@ body { #unit-control-panel { top: 50px; } +} + + + + + +.hide { + display:none !important; } \ No newline at end of file diff --git a/client/src/aic/aic.ts b/client/src/aic/aic.ts index 86a5e237..7fc70c64 100644 --- a/client/src/aic/aic.ts +++ b/client/src/aic/aic.ts @@ -1,37 +1,79 @@ -interface AICFormation { - "descriptor" : string, - "icon" : string, - "label" : string, - "name" : string -} +import { AICFormation } from "./AICFormation"; +import { AICFormation_Azimuth } from "./AICFormation/Azimuth"; +import { AICFormation_Range } from "./AICFormation/Range"; +import { AICFormation_Single } from "./AICFormation/Single"; +import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection"; +// import { AICFormationDescriptor } from "./aicformationdescriptor" export class AIC { #status:boolean = true; - #formations:AICFormation[] = [{ - "descriptor" : "group, single, Bullseye, , , , tracks , ", - "icon" : "single.png", - "label" : "Single", - "name" : "single" - }, { - "descriptor" : "", - "icon" : "azimuth.png", - "label" : "Azimuth", - "name" : "azimuth" - }, { - "descriptor" : "", - "icon" : "range.png", - "label" : "Range", - "name" : "range" - }]; + #formations = [ + + new AICFormation_Single(), + new AICFormation_Range(), + new AICFormation_Azimuth() + + ]; constructor() { this.#onStatusUpdate(); + // This feels kind of dirty + let $aicFormationList = document.getElementById( "aic-formation-list" ); + + if ( $aicFormationList ) { + + this.getFormations().forEach( formation => { + + // Image + let $imageDiv = document.createElement( "div" ); + $imageDiv.classList.add( "aic-formation-image" ); + + let $img = document.createElement( "img" ); + $img.src = "images/formations/" + formation.icon; + + $imageDiv.appendChild( $img ); + + // Name + let $nameDiv = document.createElement( "div" ); + $nameDiv.classList.add( "aic-formation-name" ); + $nameDiv.innerText = formation.label; + + // Wrapper + let $wrapperDiv = document.createElement( "div" ); + $wrapperDiv.dataset.formationName = formation.name; + $wrapperDiv.appendChild( $imageDiv ) + $wrapperDiv.appendChild( $nameDiv ); + $wrapperDiv.addEventListener( "click", ( ev ) => { + + const controlTypeInput = document.querySelector( "input[type='radio'][name='control-type']:checked" ); + + let controlTypeValue:any = ( controlTypeInput instanceof HTMLInputElement && [ "broadcast", "tactical" ].indexOf( controlTypeInput.value ) > -1 ) ? controlTypeInput.value : "broadcast"; + + // TODO: make this not an "any" + const output:any = formation.getDescriptor({ + "aicCallsign" : "Magic", + "bullseyeName" : "Bullseye", + "control" : controlTypeValue, + "numGroups" : formation.numGroups + }); + + this.updateTeleprompt( output ); + + }); + + // Add to DOM + $aicFormationList?.appendChild( $wrapperDiv ); + + }); + + } + } @@ -67,4 +109,80 @@ export class AIC { } + + toggleHelp() { + document.getElementById( "aic-help" )?.classList.toggle( "hide" ); + } + +//* + updateTeleprompt( descriptor:T[] ) { + + let $teleprompt = document.getElementById( "aic-teleprompt" ); + + if ( $teleprompt instanceof HTMLElement ) { + + // Clean slate + while ( $teleprompt.childNodes.length > 0 ) { + $teleprompt.childNodes[0].remove(); + } + + function newDiv() { + return document.createElement( "div" ); + } + + // Wrapper + let $descriptor = newDiv(); + $descriptor.id = "aic-descriptor"; + + for ( const section of descriptor ) { + + if ( section.omitSection ) { + continue; + } + + let $section = newDiv(); + $section.classList.add( "aic-descriptor-section" ); + + let $sectionLabel = newDiv(); + $sectionLabel.classList.add( "aic-descriptor-section-label" ); + $sectionLabel.innerText = section.label; + $section.appendChild( $sectionLabel ); + + + let $components = newDiv(); + $components.classList.add( "aic-descriptor-components" ); + + for ( const component of section.getComponents() ) { + + let $component = newDiv(); + $component.classList.add( "aic-descriptor-component" ); + + let $componentLabel = newDiv(); + $componentLabel.classList.add( "aic-descriptor-component-label" ); + $componentLabel.innerText = component.label; + + let $componentValue = newDiv(); + $componentValue.classList.add( "aic-descriptor-component-value" ); + $componentValue.innerText = component.value; + + $component.appendChild( $componentLabel ); + $component.appendChild( $componentValue ); + + $components.appendChild( $component ); + + } + + $section.appendChild( $components ); + $descriptor.appendChild( $section ); + + } + + $teleprompt.appendChild( $descriptor ); + + } + + + } +//*/ + } \ No newline at end of file diff --git a/client/src/index.ts b/client/src/index.ts index e1bc758a..2a826c75 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -11,8 +11,8 @@ import { MissionData } from "./missiondata/missiondata"; import { UnitControlPanel } from "./panels/unitcontrolpanel"; import { MouseInfoPanel } from "./panels/mouseInfoPanel"; import { Slider } from "./controls/slider"; +import { AIC } from "./aic/AIC"; -import { AIC } from "./aic/aic"; /* TODO: should this be a class? */ var map: Map; @@ -41,6 +41,7 @@ var deadVisibilityButton: Button; var aic: AIC; var aicToggleButton: Button; +var aicHelpButton: Button; var altitudeSlider: Slider; var airspeedSlider: Slider; @@ -84,11 +85,31 @@ function setup() { /* AIC */ aic = new AIC(); - - setupAICFormations( aic ); - aicToggleButton = new Button( "toggle-aic-button", ["images/buttons/ai-full.svg"], () => { + aicToggleButton = new Button( "toggle-aic-button", ["images/buttons/radar.svg"], () => { aic.toggleStatus(); + }); + + aicHelpButton = new Button( "aic-help-button", [ "images/buttons/question-mark.svg" ], () => { + aic.toggleHelp(); + }); + + + /* Generic clicks */ + + document.addEventListener( "click", ( ev ) => { + + if ( ev instanceof PointerEvent && ev.target instanceof HTMLElement ) { + + if ( ev.target.classList.contains( "olympus-dialog-close" ) ) { + ev.target.closest( "div.olympus-dialog" )?.classList.add( "hide" ); + } + + } + + + + }); /* Default values */ @@ -226,55 +247,4 @@ export function getUnitControlSliders() { } -function setupAICFormations( aic:AIC ) { - let $aicFormationList = document.getElementById( "aic-formation-list" ); - - if ( $aicFormationList ) { - - /* // Example display -
-
- -
-
Azimuth
-
(instructions)
-
- //*/ - - aic.getFormations().forEach( formation => { - - // Image - let imageDiv = document.createElement( "div" ); - imageDiv.classList.add( "aic-formation-image" ); - - let img = document.createElement( "img" ); - img.src = "images/formations/" + formation.icon; - - imageDiv.appendChild( img ); - - // Name - let nameDiv = document.createElement( "div" ); - nameDiv.classList.add( "aic-formation-name" ); - nameDiv.innerText = formation.label; - - // Descriptor - let descriptorDiv = document.createElement( "div" ); - descriptorDiv.classList.add( "aic-formation-descriptor" ); - descriptorDiv.innerText = formation.descriptor; - - // Wrapper - let wrapperDiv = document.createElement( "div" ); - wrapperDiv.dataset.formationName = formation.name; - wrapperDiv.appendChild( imageDiv ) - wrapperDiv.appendChild( nameDiv ); - wrapperDiv.appendChild( descriptorDiv ); - - // Add to DOM - $aicFormationList?.appendChild( wrapperDiv ); - - }); - - } -} - window.onload = setup; \ No newline at end of file diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 4b169360..45ad1439 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -1,3 +1,37 @@ +export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { + const φ1 = deg2rad(lat1); // φ, λ in radians + const φ2 = deg2rad(lat2); + const λ1 = deg2rad(lon1); // φ, λ in radians + const λ2 = deg2rad(lon2); + const y = Math.sin(λ2 - λ1) * Math.cos(φ2); + const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); + const θ = Math.atan2(y, x); + const brng = (rad2deg(θ) + 360) % 360; // in degrees + + return brng; +} + + +export function ConvertDDToDMS(D: number, lng: boolean) { + var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N"; + var deg = 0 | (D < 0 ? (D = -D) : D); + var min = 0 | (((D += 1e-9) % 1) * 60); + var sec = (0 | (((D * 60) % 1) * 6000)) / 100; + var dec = Math.round((sec - Math.floor(sec)) * 100); + var sec = Math.floor(sec); + if (lng) + return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; + else + return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; +} + + +export function deg2rad(deg: number) { + var pi = Math.PI; + return deg * (pi / 180); +} + + export function distance(lat1: number, lon1: number, lat2: number, lon2: number) { const R = 6371e3; // metres const φ1 = deg2rad(lat1); // φ, λ in radians @@ -13,27 +47,24 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number) return d; } -export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) { - const φ1 = deg2rad(lat1); // φ, λ in radians - const φ2 = deg2rad(lat2); - const λ1 = deg2rad(lon1); // φ, λ in radians - const λ2 = deg2rad(lon2); - const y = Math.sin(λ2 - λ1) * Math.cos(φ2); - const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1); - const θ = Math.atan2(y, x); - const brng = (rad2deg(θ) + 360) % 360; // in degrees - return brng; +export function rad2deg(rad: number) { + var pi = Math.PI; + return rad / (pi / 180); } -export const zeroPad = function (num: number, places: number) { - var string = String(num); - while (string.length < places) { - string += "0"; + +export function reciprocalHeading( heading:number ): number { + + if ( heading > 180 ) { + return heading - 180; } - return string; + + return heading + 180; + } + export const zeroAppend = function (num: number, places: number) { var string = String(num); while (string.length < places) { @@ -42,25 +73,11 @@ export const zeroAppend = function (num: number, places: number) { return string; } -export function ConvertDDToDMS(D: number, lng: boolean) { - var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N"; - var deg = 0 | (D < 0 ? (D = -D) : D); - var min = 0 | (((D += 1e-9) % 1) * 60); - var sec = (0 | (((D * 60) % 1) * 6000)) / 100; - var dec = Math.round((sec - Math.floor(sec)) * 100); - var sec = Math.floor(sec); - if (lng) - return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; - else - return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\""; -} -export function deg2rad(deg: number) { - var pi = Math.PI; - return deg * (pi / 180); -} - -export function rad2deg(rad: number) { - var pi = Math.PI; - return rad / (pi / 180); +export const zeroPad = function (num: number, places: number) { + var string = String(num); + while (string.length < places) { + string += "0"; + } + return string; } \ No newline at end of file diff --git a/client/views/aiccontrolpanel.ejs b/client/views/aiccontrolpanel.ejs index 1a3282cd..e4f11833 100644 --- a/client/views/aiccontrolpanel.ejs +++ b/client/views/aiccontrolpanel.ejs @@ -1,3 +1,17 @@
-
\ No newline at end of file +
+ + +
+
×
+
AIC Help
+
+

How to be a good AIC and get people to do stuff good, too.

+
+
[DCS with Volvo video]
+
+
+
+ +
\ No newline at end of file diff --git a/client/views/aicformationpanel.ejs b/client/views/aicformationpanel.ejs index 3dae4b29..5d6f2d5b 100644 --- a/client/views/aicformationpanel.ejs +++ b/client/views/aicformationpanel.ejs @@ -1,7 +1,33 @@ -
+
+ +
+

My callsign

+
Magic
+
+ +
+ + +
-
Formations
+
+

Control

+
+ + +
+
+ + +
+
+ +
+ +

Formations

+ +
+ +
-
- -
\ No newline at end of file +