mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
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:
134
client/src/FeatureSwitches.ts
Normal file
134
client/src/FeatureSwitches.ts
Normal 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 ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
client/src/ToggleableFeature.ts
Normal file
35
client/src/ToggleableFeature.ts
Normal 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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
54
client/src/aic/AICFormation.ts
Normal file
54
client/src/aic/AICFormation.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
38
client/src/aic/AICFormation/Azimuth.ts
Normal file
38
client/src/aic/AICFormation/Azimuth.ts
Normal 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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
38
client/src/aic/AICFormation/Range.ts
Normal file
38
client/src/aic/AICFormation/Range.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
24
client/src/aic/AICFormation/Single.ts
Normal file
24
client/src/aic/AICFormation/Single.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
55
client/src/aic/AICFormationDescriptor.ts
Normal file
55
client/src/aic/AICFormationDescriptor.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
client/src/aic/AICFormationDescriptorComponent.ts
Normal file
18
client/src/aic/AICFormationDescriptorComponent.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
|
||||
export abstract class AICFormactionDescriptorComponent_Distance extends AICFormationDescriptorComponent {
|
||||
|
||||
constructor( value:string, label?:string ) {
|
||||
super( value, label );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
}
|
||||
40
client/src/aic/AICFormationDescriptorPhrase.ts
Normal file
40
client/src/aic/AICFormationDescriptorPhrase.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
40
client/src/aic/AICFormationDescriptorSection.ts
Normal file
40
client/src/aic/AICFormationDescriptorSection.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
39
client/src/aic/AICFormationDescriptorSection/Formation.ts
Normal file
39
client/src/aic/AICFormationDescriptorSection/Formation.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
client/src/aic/AICFormationDescriptorSection/NumGroups.ts
Normal file
35
client/src/aic/AICFormationDescriptorSection/NumGroups.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
83
client/src/aic/AICFormationDescriptorSection/Unit.ts
Normal file
83
client/src/aic/AICFormationDescriptorSection/Unit.ts
Normal 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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
35
client/src/aic/AICFormationDescriptorSection/Who.ts
Normal file
35
client/src/aic/AICFormationDescriptorSection/Who.ts
Normal 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
172
client/src/aic/aic.ts
Normal 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
87
client/src/atc/ATC.ts
Normal 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() );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
7
client/src/atc/ATCMockAPI.ts
Normal file
7
client/src/atc/ATCMockAPI.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export abstract class ATCMockAPI {
|
||||
|
||||
constructor() {}
|
||||
|
||||
generateMockData() {}
|
||||
|
||||
}
|
||||
40
client/src/atc/ATCMockAPI/Flights.ts
Normal file
40
client/src/atc/ATCMockAPI/Flights.ts
Normal 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 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
18
client/src/atc/FlightList.ts
Normal file
18
client/src/atc/FlightList.ts
Normal 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 );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user