Pax1601 main (#52)

* GA initial data

* First commit of crude functionality.

* More AIC work so I don't lose it. (Best commit message ever.)

* Restructured to use 'phrases'.

* Set to a working state.

* Committing so I don't lose work.

* Added ai-formation feature swtich and UI kit stuff.

* Added plane units to UI kit.
This commit is contained in:
PeekabooSteam
2023-02-25 17:03:03 +00:00
committed by GitHub
parent abf5f40020
commit 1c1e60146d
50 changed files with 17524 additions and 43 deletions

View File

@@ -0,0 +1,134 @@
export interface FeatureSwitchInterface {
"defaultEnabled": boolean, // default on/off state (if allowed by masterSwitch)
"label": string,
"masterSwitch": boolean, // on/off regardless of user preference
"name": string,
"options"?: object,
"removeArtifactsIfDisabled"?: boolean
}
class FeatureSwitch {
// From config param
defaultEnabled;
label;
masterSwitch;
name;
removeArtifactsIfDisabled = true;
// Self-set
userPreference;
constructor( config:FeatureSwitchInterface ) {
this.defaultEnabled = config.defaultEnabled;
this.label = config.label;
this.masterSwitch = config.masterSwitch;
this.name = config.name;
this.userPreference = this.getUserPreference();
}
getUserPreference() {
let preferences = JSON.parse( localStorage.getItem( "featureSwitches" ) || "{}" );
return ( preferences.hasOwnProperty( this.name ) ) ? preferences[ this.name ] : this.defaultEnabled;
}
isEnabled() {
if ( !this.masterSwitch ) {
return false;
}
return this.userPreference;
}
}
export class FeatureSwitches {
#featureSwitches:FeatureSwitch[] = [
new FeatureSwitch({
"defaultEnabled": false,
"label": "AIC",
"masterSwitch": true,
"name": "aic"
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "AI Formations",
"masterSwitch": true,
"name": "ai-formations",
"removeArtifactsIfDisabled": false
}),
new FeatureSwitch({
"defaultEnabled": false,
"label": "ATC",
"masterSwitch": true,
"name": "atc"
})
];
constructor() {
this.#removeArtifacts();
this.savePreferences();
}
getSwitch( switchName:string ) {
return this.#featureSwitches.find( featureSwitch => featureSwitch.name === switchName );
}
#removeArtifacts() {
for ( const featureSwitch of this.#featureSwitches ) {
if ( !featureSwitch.isEnabled() ) {
document.querySelectorAll( "[data-feature-switch='" + featureSwitch.name + "']" ).forEach( el => {
if ( featureSwitch.removeArtifactsIfDisabled === false ) {
el.remove();
} else {
el.classList.add( "hide" );
}
});
}
}
}
savePreferences() {
let preferences:any = {};
for ( const featureSwitch of this.#featureSwitches ) {
preferences[ featureSwitch.name ] = featureSwitch.isEnabled();
}
localStorage.setItem( "featureSwitches", JSON.stringify( preferences ) );
}
}

View File

@@ -0,0 +1,35 @@
export abstract class ToggleableFeature {
#status:boolean = false;
constructor( defaultStatus:boolean ) {
this.#status = defaultStatus;
this.onStatusUpdate();
}
getStatus() : boolean {
return this.#status;
}
protected onStatusUpdate() {}
toggleStatus( force?:boolean ) : void {
if ( force ) {
this.#status = force;
} else {
this.#status = !this.#status;
}
this.onStatusUpdate();
}
}

View File

@@ -0,0 +1,54 @@
import { AICFormationContextDataInterface, AICFormationDescriptor } from "./AICFormationDescriptor";
import { AICFormationDescriptorPhrase } from "./AICFormationDescriptorPhrase";
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
export interface AICFormationInterface {
"icon" : string,
"label" : string,
"name" : string,
"numGroups" : number,
"summary" : string,
"unitBreakdown" : string[]
}
export abstract class AICFormation {
"icon" = "";
"label" = "";
"name" = "";
"numGroups" = 1;
"summary" = "";
"unitBreakdown":string[] = []
constructor() {
this.unitBreakdown = [];
}
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
return phrase;
}
getDescriptor( contextData: AICFormationContextDataInterface ) {
return new AICFormationDescriptor().generate( this, contextData );
}
hasUnitBreakdown() {
return this.unitBreakdown.length > 0;
}
showFormationNameInDescriptor() {
return true;
}
}

View File

@@ -0,0 +1,38 @@
import { AICFormation, AICFormationInterface } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormation_Azimuth extends AICFormation implements AICFormationInterface {
"icon" = "azimuth.png";
"label" = "Azimuth";
"name" = "azimuth";
"numGroups" = 2;
"summary" = "Two contacts, side-by-side in a line perpedicular to the perspective.";
"unitBreakdown" = [ "<compass> group", "<compass> group" ];
constructor() {
super();
}
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
switch ( section.name ) {
case "formation":
phrase.addComponent( new AICFormationDescriptorComponent( "<distance>" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>" ) );
}
return phrase;
}
}

View File

@@ -0,0 +1,38 @@
import { AICFormation, AICFormationInterface } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormation_Range extends AICFormation implements AICFormationInterface {
"icon" = "range.png";
"label" = "Range";
"name" = "range";
"numGroups" = 2;
"summary" = "Two contacts, one behind the other";
"unitBreakdown" = [ "Lead group", "Trail group" ];
constructor() {
super();
}
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
switch ( section.name ) {
case "formation":
phrase.addComponent( new AICFormationDescriptorComponent( "<distance>" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>" ) );
}
return phrase;
}
}

View File

@@ -0,0 +1,24 @@
import { AICFormation, AICFormationInterface } from "../AICFormation";
import { AICFormationContextDataInterface, AICFormationDescriptor } from "../AICFormationDescriptor";
export class AICFormation_Single extends AICFormation implements AICFormationInterface {
"icon" = "single.png";
"label" = "Single";
"name" = "single";
"numGroups" = 1;
"summary" = "One contact on its own";
"unitBreakdown" = [];
constructor() {
super();
}
showFormationNameInDescriptor() {
return false;
}
}

View File

@@ -0,0 +1,55 @@
import { AICFormation } from "./AICFormation";
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
import { AICFormationDescriptorSection_Formation } from "./AICFormationDescriptorSection/Formation";
import { AICFormationDescriptorSection_Unit } from "./AICFormationDescriptorSection/Unit";
import { AICFormationDescriptorSection_NumGroups } from "./AICFormationDescriptorSection/NumGroups";
import { AICFormationDescriptorSection_Who } from "./AICFormationDescriptorSection/Who";
export interface AICFormationContextDataInterface {
"aicCallsign" : string,
"bullseyeName" : string,
"control" : "broadcast" | "tactical",
"numGroups" : number
}
export class AICFormationDescriptor {
#sections:AICFormationDescriptorSection[] = [
new AICFormationDescriptorSection_Who(),
new AICFormationDescriptorSection_NumGroups(),
new AICFormationDescriptorSection_Formation(),
new AICFormationDescriptorSection_Unit()
]
constructor() {
}
addSection( section:AICFormationDescriptorSection ) {
this.#sections.push( section );
}
getSections() {
return this.#sections;
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
let output:object[] = [];
for ( const section of this.#sections ) {
output.push(
section.generate( formation, contextData )
);
}
return output;
}
}

View File

@@ -0,0 +1,18 @@
interface ComponentInterface {
"label" : string;
"value" : string;
}
export class AICFormationDescriptorComponent implements ComponentInterface {
label = "(not set)";
value = "(not set)";
constructor( value:any, label?:string ) {
this.label = label || "(not set)";
this.value = value;
}
}

View File

@@ -0,0 +1,9 @@
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
export abstract class AICFormactionDescriptorComponent_Distance extends AICFormationDescriptorComponent {
constructor( value:string, label?:string ) {
super( value, label );
}
}

View File

@@ -0,0 +1,9 @@
import { AICFormactionDescriptorComponent_Distance } from "../Distance";
export class AICFormationDescriptorComponent_Distance_Range extends AICFormactionDescriptorComponent_Distance {
constructor( value:string, label?:string ) {
super( value, label );
}
}

View File

@@ -0,0 +1,40 @@
import { AICFormation } from "./AICFormation";
import { AICFormationContextDataInterface } from "./AICFormationDescriptor";
import { AICFormationDescriptorComponent } from "./AICFormationDescriptorComponent";
export interface AICFormationDescriptorPhraseInterface {
"generate" : CallableFunction,
"label" : string,
"name" : string
}
export class AICFormationDescriptorPhrase {
#components : AICFormationDescriptorComponent[] = [];
label = "";
name = "";
constructor() {
}
addComponent( component:AICFormationDescriptorComponent ) {
this.#components.push( component );
return this;
}
getComponents() {
return this.#components;
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
return this;
}
}

View File

@@ -0,0 +1,40 @@
import { AICFormation } from "./AICFormation";
import { AICFormationContextDataInterface } from "./AICFormationDescriptor";
import { AICFormationDescriptorPhrase } from "./AICFormationDescriptorPhrase";
export interface AICFormationDescriptorSectionInterface {
"generate" : CallableFunction,
"label" : string,
"name" : string,
"omitSection" : boolean
}
export abstract class AICFormationDescriptorSection {
#phrases : AICFormationDescriptorPhrase[] = [];
label = "";
name = "";
omitSection = false;
constructor() {
}
addPhrase( phrase:AICFormationDescriptorPhrase ) {
this.#phrases.push( phrase );
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
return this;
}
getPhrases() {
return this.#phrases;
}
}

View File

@@ -0,0 +1,39 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormationDescriptorSection_Formation extends AICFormationDescriptorSection {
label = "Formation";
name = "formation";
constructor() {
super();
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
if ( !formation.showFormationNameInDescriptor() ) {
this.omitSection = true;
return this;
}
let phrase = new AICFormationDescriptorPhrase();
phrase.addComponent( new AICFormationDescriptorComponent( formation.label, "Formation" ) );
phrase = formation.addToDescriptorPhrase( this, phrase, contextData );
this.addPhrase( phrase );
return this;
}
}

View File

@@ -0,0 +1,35 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormationDescriptorSection_NumGroups extends AICFormationDescriptorSection {
label = "Groups";
name = "numgroups";
constructor() {
super();
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
let value = "Single group";
if ( contextData.numGroups > 1 ) {
value = contextData.numGroups + " groups";
}
let phrase = new AICFormationDescriptorPhrase();
phrase.addComponent( new AICFormationDescriptorComponent( value, "Number of groups" ) );
this.addPhrase( phrase );
return this;
}
}

View File

@@ -0,0 +1,83 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
interface addUnitInformationInterface {
omitTrack?: boolean
}
export class AICFormationDescriptorSection_Unit extends AICFormationDescriptorSection {
label = "Unit";
name = "unit";
constructor() {
super();
}
addUnitInformation( formation:AICFormation, contextData: AICFormationContextDataInterface, phrase: AICFormationDescriptorPhrase, options?:addUnitInformationInterface ) {
options = options || {};
const originPoint = ( contextData.control === "broadcast" ) ? contextData.bullseyeName : "BRAA";
phrase.addComponent( new AICFormationDescriptorComponent( originPoint, "Bearing origin point" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "<bearing>", "Bearing" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "<range>", "Range" ) );
phrase.addComponent( new AICFormationDescriptorComponent( "<altitude>", "Altitude" ) );
if ( contextData.control === "broadcast" ) {
if ( !options.hasOwnProperty( "omitTrack" ) || options.omitTrack !== true ) {
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>", "Tracking" ) );
}
} else {
phrase.addComponent( new AICFormationDescriptorComponent( "[hot|flanking [left|right]|beam <compass>|cold]", "Azimuth" ) );
}
return phrase;
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
if ( formation.hasUnitBreakdown() ) {
for ( const [ i, unitRef ] of formation.unitBreakdown.entries() ) {
let phrase = new AICFormationDescriptorPhrase();
phrase.addComponent( new AICFormationDescriptorComponent( unitRef, "Unit reference" ) );
if ( i === 0 ) {
this.addUnitInformation( formation, contextData, phrase, { "omitTrack": true } );
} else {
phrase.addComponent( new AICFormationDescriptorComponent( "<altitude>" ) );
}
phrase.addComponent( new AICFormationDescriptorComponent( "hostile" ) );
this.addPhrase( phrase );
}
} else {
this.addPhrase(
this.addUnitInformation( formation, contextData, new AICFormationDescriptorPhrase() )
);
}
return this;
}
}

View File

@@ -0,0 +1,35 @@
import { AICFormation } from "../AICFormation";
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
export class AICFormationDescriptorSection_Who extends AICFormationDescriptorSection {
label = "Who";
name = "who";
constructor() {
super();
}
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
let phrase = new AICFormationDescriptorPhrase();
if ( contextData.control === "tactical" ) {
phrase.addComponent( new AICFormationDescriptorComponent( "<their callsign>", "Their callsign" ) );
}
phrase.addComponent( new AICFormationDescriptorComponent( contextData.aicCallsign, "Your callsign" ) );
this.addPhrase( phrase );
return this;
}
}

172
client/src/aic/aic.ts Normal file
View File

@@ -0,0 +1,172 @@
import { ToggleableFeature } from "../ToggleableFeature";
import { AICFormation_Azimuth } from "./AICFormation/Azimuth";
import { AICFormation_Range } from "./AICFormation/Range";
import { AICFormation_Single } from "./AICFormation/Single";
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
export class AIC extends ToggleableFeature {
#formations = [
new AICFormation_Single(),
new AICFormation_Range(),
new AICFormation_Azimuth()
];
constructor() {
super( false );
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 );
});
}
}
getFormations() {
return this.#formations;
}
onStatusUpdate() {
// Update the DOM
document.body.classList.toggle( "aic-enabled", this.getStatus() );
}
toggleHelp() {
document.getElementById( "aic-help" )?.classList.toggle( "hide" );
}
//*
updateTeleprompt<T extends AICFormationDescriptorSection>( 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 );
for ( const phrase of section.getPhrases() ) {
let $phrase = newDiv();
$phrase.classList.add( "aic-descriptor-phrase" );
for ( const component of phrase.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 );
$phrase.appendChild( $component );
}
$section.appendChild( $phrase );
}
$descriptor.appendChild( $section );
}
$teleprompt.appendChild( $descriptor );
}
}
//*/
}

87
client/src/atc/ATC.ts Normal file
View File

@@ -0,0 +1,87 @@
import { ToggleableFeature } from "../ToggleableFeature";
import Sortable from 'sortablejs';
import { ATCFLightList } from "./FlightList";
export class ATC extends ToggleableFeature {
constructor() {
super( true );
//this.#generateFlightList();
let $list = document.getElementById( "atc-strip-board-arrivals" );
if ( $list instanceof HTMLElement ) {
Sortable.create( $list, {
"handle": ".handle"
});
}
}
#generateFlightList() {
const flightList = new ATCFLightList();
const flights:any = flightList.getFlights( true );
const $tbody = document.getElementById( "atc-flight-list-table-body" );
if ( $tbody instanceof HTMLElement ) {
if ( flights.length > 0 ) {
let flight:any = {};
let $button, i;
for ( [ i, flight ] of flights.entries() ) {
const $row = document.createElement( "tr" );
$row.dataset.status = flight.status
let $td = document.createElement( "td" );
$td.innerText = flight.name;
$row.appendChild( $td );
$td = document.createElement( "td" );
$td.innerText = flight.takeOffTime;
$row.appendChild( $td );
$td = document.createElement( "td" );
$td.innerText = "00:0" + ( 5 + i );
$row.appendChild( $td );
$td = document.createElement( "td" );
$td.innerText = flight.status;
$row.appendChild( $td );
$td = document.createElement( "td" );
$button = document.createElement( "button" );
$button.innerText = "...";
$td.appendChild( $button );
$row.appendChild( $td );
$tbody.appendChild( $row );
}
}
}
}
protected onStatusUpdate(): void {
document.body.classList.toggle( "atc-enabled", this.getStatus() );
}
}

View File

@@ -0,0 +1,7 @@
export abstract class ATCMockAPI {
constructor() {}
generateMockData() {}
}

View File

@@ -0,0 +1,40 @@
import { ATCMockAPI } from "../ATCMockAPI";
export class ATCMockAPI_Flights extends ATCMockAPI {
generateMockData() {
let data = [];
const statuses = [ "unknown", "checkedIn", "readyToTaxi" ]
for ( const [ i, flightName ] of [ "Shark", "Whale", "Dolphin" ].entries() ) {
data.push({
"name": flightName,
"status": statuses[ i ],
"takeOffTime": "18:0" + i
});
}
localStorage.setItem( "flightList", JSON.stringify( data ) );
}
get( generateMockDataIfEmpty?:boolean ) : object {
generateMockDataIfEmpty = generateMockDataIfEmpty || false;
let data = localStorage.getItem( "flightList" ) || "[]";
if ( data === "[]" && generateMockDataIfEmpty ) {
this.generateMockData();
}
return JSON.parse( data );
}
}

View File

@@ -0,0 +1,18 @@
import { ATCMockAPI_Flights } from "./ATCMockAPI/Flights";
export class ATCFLightList {
constructor() {
}
getFlights( generateMockDataIfEmpty?:boolean ) {
let api = new ATCMockAPI_Flights();
return api.get( generateMockDataIfEmpty );
}
}

View File

@@ -9,8 +9,14 @@ import { ConnectionStatusPanel } from "./panels/connectionstatuspanel";
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 { VisibilityControlPanel } from "./panels/visibilitycontrolpanel";
import { ATC } from "./atc/ATC";
import { FeatureSwitches } from "./FeatureSwitches";
import { LogPanel } from "./panels/logpanel";
import { Button } from "./controls/button";
/* TODO: should this be a class? */
var map: Map;
@@ -29,10 +35,31 @@ var logPanel: LogPanel;
var mapSourceDropdown: Dropdown;
var slowButton: Button;
var fastButton: Button;
var climbButton: Button;
var descendButton: Button;
var aic: AIC;
var aicToggleButton: Button;
var aicHelpButton: Button;
var atc: ATC;
var atcToggleButton: Button;
var altitudeSlider: Slider;
var airspeedSlider: Slider;
var connected: boolean;
var activeCoalition: string;
var featureSwitches;
function setup() {
featureSwitches = new FeatureSwitches();
/* Initialize */
map = new Map('map-container');
unitsManager = new UnitsManager();
@@ -43,11 +70,71 @@ function setup() {
unitInfoPanel = new UnitInfoPanel("unit-info-panel");
unitControlPanel = new UnitControlPanel("unit-control-panel");
//scenarioDropdown = new Dropdown("scenario-dropdown", ["Caucasus", "Marianas", "Nevada", "South Atlantic", "Syria", "The Channel"], () => { });
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel");
mouseInfoPanel = new MouseInfoPanel("mouse-info-panel");
visibilityControlPanel = new VisibilityControlPanel("visibility-control-panel");
logPanel = new LogPanel("log-panel");
missionData = new MissionData();
/* Unit control buttons */
slowButton = new Button("slow-button", ["images/buttons/slow.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("slow"); });
fastButton = new Button("fast-button", ["images/buttons/fast.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("fast"); });
climbButton = new Button("climb-button", ["images/buttons/climb.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("climb"); });
descendButton = new Button("descend-button", ["images/buttons/descend.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("descend"); });
/* Unit control sliders */
altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => getUnitsManager().selectedUnitsSetAltitude(value * 0.3048));
airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => getUnitsManager().selectedUnitsSetSpeed(value / 1.94384));
/* AIC */
let aicFeatureSwitch = featureSwitches.getSwitch( "aic" );
if ( aicFeatureSwitch?.isEnabled() ) {
aic = new AIC();
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" );
}
}
});
/*** ATC ***/
let atcFeatureSwitch = featureSwitches.getSwitch( "atc" );
if ( atcFeatureSwitch?.isEnabled() ) {
atc = new ATC();
atcToggleButton = new Button( "atc-toggle-button", [ "images/buttons/atc.svg" ], () => {
atc.toggleStatus();
} );
}
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
/* Default values */
@@ -59,12 +146,15 @@ function setup() {
function requestUpdate() {
getDataFromDCS(update);
/* Main update rate = 250ms is minimum time, equal to server update time. */
setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000);
connectionStatusPanel.update(getConnected());
}
export function update(data: JSON) {
console.log( data );
unitsManager.update(data);
missionData.update(data);
logPanel.update(data);
@@ -118,4 +208,9 @@ export function getConnected() {
return connected;
}
export function getUnitControlSliders() {
return {altitude: altitudeSlider, airspeed: airspeedSlider}
}
window.onload = setup;

View File

@@ -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;
}