Merge branch 'main' into 73-add-tankers-and-awacs-spawning

This commit is contained in:
Pax1601 2023-04-19 16:35:37 +02:00
commit 41be6402cd
22 changed files with 972 additions and 464 deletions

View File

@ -4,6 +4,7 @@ var cookieParser = require('cookie-parser');
var logger = require('morgan');
var fs = require('fs');
var atcRouter = require('./routes/api/atc');
var indexRouter = require('./routes/index');
var uikitRouter = require('./routes/uikit');
var usersRouter = require('./routes/users');
@ -17,6 +18,7 @@ app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
app.use('/api/atc', atcRouter);
app.use('/users', usersRouter);
app.use('/uikit', uikitRouter);

View File

@ -1,211 +1,114 @@
/*** Control panel ***/
#atc-control-panel {
align-self: flex-end;
background: white;
border-radius: 10px;
display:flex;
margin: 0 0 50px 100px;
padding:5px;
position: absolute;
z-index: 9999;
}
.atc-tool {
align-self: center;
border-radius: 10px;
display:none;
justify-self: center;
padding: 10px;
position: absolute;
z-index: 9999;
}
.atc-enabled .atc-tool {
display:flex;
}
#atc-flight-list {
flex-direction: column;
}
#atc-flight-list table {
color:white;
}
#atc-flight-list table td {
padding:0 10px;
text-align: center;
}
#atc-flight-list table td:first-of-type {
text-align: left;
}
#atc-flight-list table tr[data-status='checkedIn'] td {
background-color:goldenrod;
}
#atc-flight-list table tr[data-status='readyToTaxi'] td {
background-color:darkgreen;
}
#atc-flight-list table button {
background-color: #666;
border:1px solid white;
color:white;
font-weight: bold;
margin:2px 0;
}
.atc-strip-board {
align-self: center;
display:flex;
justify-self: center;
position: absolute;
z-index: 9999 ;
}
.atc-strip-board-header {
display:none;
}
.atc-strip-board-strips {
.ol-strip-board-strips {
display:flex;
flex-direction: column;
row-gap: 4px;
}
.atc-strip-board-strip {
display:flex;
flex-direction: row;
}
/*
.atc-strip-board-header {
background:black;
color:white;
display:none;
justify-content: right;
}
.atc-strip-board {
display:flex;
flex-direction: column;
row-gap: 5px;
}
.atc-strip-board-strips {
display:flex;
flex-direction: column;
padding:10px;
row-gap: 5px;
}
.atc-strip-board-strips > div {
.ol-strip-board-strip {
align-items: center;
border-radius: var( --border-radius-sm );
column-gap: 4px;
display:flex;
flex-flow: row nowrap;
row-gap:4px;
}
.ol-strip-board-strip[data-flight-status="checkedin"] {
background-color: #ffffff2A;
}
.ol-strip-board-strip[data-flight-status="readytotaxi"] {
background-color: #ffff0063;
}
.ol-strip-board-strip[data-flight-status="clearedtotaxi"] {
background-color: #00ff0030;
}
.ol-strip-board-strip[data-flight-status="halted"] {
background-color: #FF000040;
}
.ol-strip-board-strip[data-flight-status="terminated"] {
background-color: black;
}
.ol-strip-board-headers {
column-gap: 4px;
display:flex;
flex-flow:row nowrap;
text-align: center;
}
.ol-strip-board-headers > *, .ol-strip-board-strip > * {
padding: 4px;
text-overflow: ellipsis;
white-space: nowrap;
width:80px;
}
.ol-strip-board-strip input[type="text"] {
appearance: none;
background-color: transparent;
border:1px solid #ffffff30;
border-radius: var( --border-radius-sm );
color:white;
column-gap: 2px;
display: flex;
flex-direction: row;
padding: 5px;
}
.atc-strip-board-header > div, .atc-strip-board-strips > div > div {
text-align: center;
width: 75px;
}
.atc-strip-board-header > .name {
width:150px;
}
.atc-strip-board-header > div, .atc-strip-board-strips > div > div {
text-align: center;
width: 75px;
}
.atc-strip-board-strips > div > .name {
text-align: left;
width:150px;
}
.atc-strip-board-strips > div {
align-items: center;
column-gap: 5px;
display: flex;
flex-direction: row;
font-size:12px;
font-weight: 600;
padding: 5px;
row-gap: 5px;
}
/*
.atc-strip-board-header, .atc-strip-board-strips > div {
align-items: center;
background:#FFF3;
color:white;
column-gap: 5px;
display: flex;
flex-direction: row;
font-size:12px;
font-weight: 600;
padding: 5px;
row-gap: 5px;
}
.atc-strip-board-header {
background:black;
color:white;
display:none;
justify-content: right;
}
.atc-strip-board-strips > div {
border-bottom:1px solid black;
}
.atc-strip-board-header > div, .atc-strip-board-strips > div > div {
font-weight:normal;
outline:none;
padding: 4px 0;
text-align: center;
width: 75px;
width:100%;
}
.atc-strip-board-header > .name {
width:150px;
.ol-strip-board-strip[data-time-warning="level-1"] [data-point="timeToGo"] {
border:1px solid #cc0000;
}
.atc-strip-board-strips > div > .handle {
background: black;
border-radius: 50%;
cursor:grab;
height:10px;
width:10px;
.ol-strip-board-headers :nth-child(1),
.ol-strip-board-headers :nth-child(2),
.ol-strip-board-strip :nth-child(1),
.ol-strip-board-strip :nth-child(2) {
width:120px;
}
.atc-strip-board-strips > div > .name {
text-align: left;
width:150px;
.ol-strip-board-strip :nth-child(4) {
text-align: center;
}
.atc-strip-board-strips > div > .warning {
background:red;
color: white;
font-weight: bold;
.ol-strip-board-strip > [data-point="name"] {
text-overflow: ellipsis;
overflow:hidden;
}
.atc-strip-board-strips > div > .link-warning {
border: 1px solid red;
color: red;
font-weight: bold;
.ol-strip-board-strip .ol-select-value {
opacity: .85;
}
*/
.ol-strip-board-add-flight {
display:flex;
flex-flow: row nowrap;
}
.ol-strip-board-add-flight > * {
border:none;
outline: none;
padding:4px 8px;
}
.ol-strip-board-add-flight button {
background-color: darkgreen;
border-bottom-right-radius: var( --border-radius-sm );
border-bottom-left-radius: 0;
border-top-left-radius: 0;
border-top-right-radius: var( --border-radius-sm );
}
.ol-strip-board-add-flight input {
border-bottom-left-radius: var( --border-radius-sm );
border-top-left-radius: var( --border-radius-sm );
}

View File

@ -6,6 +6,8 @@
}
#primary-toolbar {
align-items: center;
display:flex;
position: absolute;
left: 10px;
top: 10px;
@ -114,8 +116,8 @@ dl.ol-data-grid dd {
font-size: 16px;
font-weight: var(--font-weight-bolder);
position: absolute;
right: 25px;
top: 25px;
right: 20px;
top: 10px;
}
.ol-dialog-close::before {
@ -127,6 +129,10 @@ dl.ol-data-grid dd {
padding-bottom: 10px;
}
.ol-dialog-content {
margin:4px 0;
}
.ol-dialog-footer {
border-top: 1px solid var(--background-grey);
padding-top: 15px;

View File

@ -1,5 +1,6 @@
@import url("layout.css");
@import url("airbase.css");
@import url("atc.css");
@import url("connectionstatuspanel.css");
@import url("contextmenus.css");
@import url("mouseinfopanel.css");
@ -144,14 +145,17 @@ form > div {
align-items: center;
background-color: var(--background-grey);
border-radius: var(--border-radius-sm);
padding: 1em;
padding: 1em 30px 1em 20px;
width: 100%;
padding-left: 20px;
padding-right: 30px;
overflow: hidden;
text-overflow: ellipsis;
}
.ol-select.narrow:not(.ol-select-image)>.ol-select-value {
opacity: .9;
padding:6px 30px 6px 15px;
}
.ol-select:not(.ol-select-image)>.ol-select-value svg {
margin-right: 10px;
}
@ -177,7 +181,7 @@ form > div {
.ol-select.is-open > .ol-select-options {
max-height: fit-content;
max-height: 382px;
overflow: visible;
overflow-y: auto;
padding: 8px 0;
@ -198,7 +202,7 @@ form > div {
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
display: flex;
justify-content: left;
padding: 6px 25px;
padding: 4px 25px;
width: 100%;
}
@ -355,6 +359,11 @@ nav.ol-panel> :last-child {
align-items: center;
}
.ol-group-header {
text-align: center;
width: 100%;
}
.ol-panel .ol-group.wrap {
flex-wrap: wrap;
}
@ -552,30 +561,6 @@ nav.ol-panel> :last-child {
width: 28px;
}
#unit-selection #unit-identification [data-object|="unit"] .unit-short-label {
font-size: 12px;
}
#unit-selection #unit-identification #unit-name {
background-color: transparent;
border: none;
color: white;
font-size: 16px;
font-weight: var(--font-weight-bolder);
outline: none;
overflow: hidden;
white-space: nowrap;
width: 150px;
}
#edit-unit-name {
background-image: url("/images/buttons/edit.svg");
background-repeat: no-repeat;
height: 14px;
margin-left: 10px;
width: 15px;
}
#unit-visibility-control {
align-items: center;
}
@ -626,6 +611,18 @@ body[data-hide-navyunit] #unit-visibility-control-navyunit {
background-image: var(--visibility-control-navyunit-hidden-url);
}
#atc-navbar-control {
align-items: center;
display:flex;
flex-direction: column;
}
#atc-navbar-control button {
background:#ffffff20;
border-radius: var( --border-radius-sm );
padding:4px;
}
.toggle {
--width: 40px;

View File

@ -26,19 +26,16 @@
}
#loadout-container {
width:250px;
}
#loadout {
display:flex;
overflow: visible;
}
#loadout-silhouette {
align-items: center;
display:flex;
justify-content: center;
width:55%;
width:100px;
}
#loadout-silhouette::before {
@ -58,7 +55,6 @@
display:flex;
flex-flow: column nowrap;
row-gap: 8px;
width:45%;
}
@ -82,11 +78,13 @@
#loadout-items > *::after {
content: attr( data-item );
width:52px;
overflow: hidden;
position:relative;
text-overflow: ellipsis;
width:80px;
}
#fuel-percentage {
align-items: center;
display:flex;
@ -112,7 +110,6 @@
height:6px;
margin-top:4px;
overflow: hidden;
width:90%;
}
#fuel-display #fuel-bar {

190
client/routes/api/atc.js Normal file
View File

@ -0,0 +1,190 @@
var express = require('express');
var app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.urlencoded({ extended: false}));
app.use(bodyParser.json());
/*
Flight:
"name"
"take-off time"
"priority"
"status"
//*/
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function Flight( name ) {
this.id = uuidv4();
this.name = name;
this.status = "unknown";
this.takeoffTime = -1;
}
Flight.prototype.getData = function() {
return {
"id" : this.id,
"name" : this.name,
"status" : this.status,
"takeoffTime" : this.takeoffTime
};
}
Flight.prototype.setStatus = function( status ) {
if ( [ "unknown", "checkedin", "readytotaxi", "clearedtotaxi", "halted", "terminated" ].indexOf( status ) < 0 ) {
return "Invalid status";
}
this.status = status;
return true;
}
Flight.prototype.setTakeoffTime = function( takeoffTime ) {
if ( takeoffTime === "" || takeoffTime === -1 ) {
this.takeoffTime = -1;
}
if ( isNaN( takeoffTime ) ) {
return "Invalid takeoff time"
}
this.takeoffTime = parseInt( takeoffTime );
return true;
}
function ATCDataHandler( data ) {
this.data = data;
}
ATCDataHandler.prototype.addFlight = function( flight ) {
if ( flight instanceof Flight === false ) {
throw new Error( "Given flight is not an instance of Flight" );
}
this.data.flights[ flight.id ] = flight;
}
ATCDataHandler.prototype.deleteFlight = function( flightId ) {
delete this.data.flights[ flightId ];
}
ATCDataHandler.prototype.getFlight = function( flightId ) {
return this.data.flights[ flightId ] || false;
}
ATCDataHandler.prototype.getFlights = function() {
return this.data.flights;
}
const dataHandler = new ATCDataHandler( {
"flights": {}
} );
/**************************************************************************************************************/
// Endpoints
/**************************************************************************************************************/
app.get( "/flight", ( req, res ) => {
res.json( dataHandler.getFlights() );
});
app.patch( "/flight/:flightId", ( req, res ) => {
const flightId = req.params.flightId;
const flight = dataHandler.getFlight( flightId );
if ( !flight ) {
res.status( 400 ).send( `Unrecognised flight ID (given: "${req.params.flightId}")` );
}
if ( req.body.status ) {
const statusChangeSuccess = flight.setStatus( req.body.status );
if ( statusChangeSuccess !== true ) {
res.status( 400 ).send( statusChangeSuccess );
}
}
if ( req.body.hasOwnProperty( "takeoffTime" ) ) {
const takeoffChangeSuccess = flight.setTakeoffTime( req.body.takeoffTime );
if ( takeoffChangeSuccess !== true ) {
res.status( 400 ).send( takeoffChangeSuccess );
}
}
res.json( flight.getData() );
});
app.post( "/flight", ( req, res ) => {
if ( !req.body.name ) {
res.status( 400 ).send( "Invalid/missing flight name" );
}
const flight = new Flight( req.body.name );
dataHandler.addFlight( flight );
res.status( 201 );
res.json( flight.getData() );
});
app.delete( "/flight/:flightId", ( req, res ) => {
const flight = dataHandler.getFlight( req.params.flightId );
if ( !flight ) {
res.status( 400 ).send( `Unrecognised flight ID (given: "${req.params.flightId}")` );
}
dataHandler.deleteFlight( req.params.flightId );
res.status( 204 ).send( "" );
});
module.exports = app;

View File

@ -1,86 +1,141 @@
import { ToggleableFeature } from "../toggleablefeature";
import Sortable from 'sortablejs';
import { ATCFLightList } from "./flightlist";
import { ATCBoard } from "./atcboard";
import { ATCBoardFlight } from "./board/flight";
export class ATC extends ToggleableFeature {
export interface FlightInterface {
id : string;
name : string;
status : "unknown";
takeoffTime : number;
}
class ATCDataHandler {
#atc:ATC;
#flights:{[key:string]: FlightInterface} = {};
#updateInterval:number|undefined = undefined;
#updateIntervalDelay:number = 1000;
constructor( atc:ATC ) {
this.#atc = atc;
}
getFlights() {
return this.#flights;
}
startUpdates() {
this.#updateInterval = setInterval( () => {
fetch( '/api/atc/flight', {
method: 'GET',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json'
}
})
.then( response => response.json() )
.then( data => {
this.setFlights( data );
});
}, this.#updateIntervalDelay );
}
setFlights( flights:{[key:string]: any} ) {
this.#flights = flights;
}
stopUpdates() {
clearInterval( this.#updateInterval );
}
}
export class ATC {
#boards:ATCBoard[] = [];
#dataHandler:ATCDataHandler;
#initDate:Date = new Date();
constructor() {
super( true );
this.#dataHandler = new ATCDataHandler( this );
//this.#generateFlightList();
let $list = document.getElementById( "atc-strip-board-arrivals" );
if ( $list instanceof HTMLElement ) {
Sortable.create( $list, {
"handle": ".handle"
});
}
this.lookForBoards();
}
#generateFlightList() {
addBoard<T extends ATCBoard>( board:T ) {
const flightList = new ATCFLightList();
const flights:any = flightList.getFlights( true );
board.startUpdates();
const $tbody = document.getElementById( "atc-flight-list-table-body" );
if ( $tbody instanceof HTMLElement ) {
if ( flights.length > 0 ) {
let flight:any = {};
let $button, i;
this.#boards.push( board );
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 );
getDataHandler() {
return this.#dataHandler;
}
$row.appendChild( $td );
$tbody.appendChild( $row );
}
getMissionElapsedSeconds() : number {
return new Date().getTime() - this.#initDate.getTime();
}
getMissionStartDateTime() : Date {
return new Date( 1990, 3, 1, 18, 0, 0 );
}
getMissionDateTime() : Date {
return new Date( this.getMissionStartDateTime().getTime() + this.getMissionElapsedSeconds() );
}
lookForBoards() {
document.querySelectorAll( ".ol-strip-board" ).forEach( board => {
if ( board instanceof HTMLElement ) {
this.addBoard( new ATCBoardFlight( this, board ) );
}
}
});
}
startUpdates() {
this.#dataHandler.startUpdates();
}
protected onStatusUpdate(): void {
document.body.classList.toggle( "atc-enabled", this.getStatus() );
stopUpdates() {
this.#dataHandler.stopUpdates();
}

185
client/src/atc/atcboard.ts Normal file
View File

@ -0,0 +1,185 @@
import { Draggable } from "leaflet";
import { Dropdown } from "../controls/dropdown";
import { zeroAppend } from "../other/utils";
import { ATC } from "./atc";
export interface StripBoardStripInterface {
"id": string,
"element": HTMLElement,
"dropdowns": {[key:string]: Dropdown}
}
export abstract class ATCBoard {
#atc:ATC;
#templates: {[key:string]: string} = {};
// Elements
#boardElement:HTMLElement;
#clockElement:HTMLElement;
#stripBoardElement:HTMLElement;
// Content
#strips:{[key:string]: StripBoardStripInterface} = {};
// Update timing
#updateInterval:number|undefined = undefined;
#updateIntervalDelay:number = 1000;
constructor( atc:ATC, boardElement:HTMLElement ) {
this.#atc = atc;
this.#boardElement = boardElement;
this.#stripBoardElement = <HTMLElement>this.getBoardElement().querySelector( ".ol-strip-board-strips" );
this.#clockElement = <HTMLElement>this.getBoardElement().querySelector( ".ol-strip-board-clock" );
if ( this.#boardElement.classList.contains( "ol-draggable" ) ) {
let options:any = {};
let handle = this.#boardElement.querySelector( ".handle" );
if ( handle instanceof HTMLElement ) {
options.handle = handle;
}
}
setInterval( () => {
this.updateClock();
}, 1000 );
}
addStrip( strip:StripBoardStripInterface ) {
this.#strips[ strip.id ] = strip;
}
calculateTimeToGo( fromTimestamp:number, toTimestamp:number ) {
let timestamp = ( toTimestamp - fromTimestamp ) / 1000;
const hasElapsed = ( timestamp < 0 ) ? true : false;
if ( hasElapsed ) {
timestamp = -( timestamp );
}
const hours = ( timestamp < 3600 ) ? "00" : zeroAppend( Math.floor( timestamp / 3600 ), 2 );
const rMinutes = timestamp % 3600;
const minutes = ( timestamp < 60 ) ? "00" : zeroAppend( Math.floor( rMinutes / 60 ), 2 );
const seconds = zeroAppend( Math.floor( rMinutes % 60 ), 2 );
return {
"elapsedMarker": ( hasElapsed ) ? "+" : "-",
"hasElapsed": hasElapsed,
"hours": hours,
"minutes": minutes,
"seconds": seconds,
"time": `${hours}:${minutes}:${seconds}`,
"totalSeconds": timestamp
};
}
deleteStrip( id:string ) {
if ( this.#strips.hasOwnProperty( id ) ) {
this.#strips[ id ].element.remove();
delete this.#strips[ id ];
}
}
getATC() {
return this.#atc;
}
getBoardElement() {
return this.#boardElement;
}
getStripBoardElement() {
return this.#stripBoardElement;
}
getStrips() {
return this.#strips;
}
getStrip( id:string ) {
return this.#strips[ id ] || false;
}
getTemplate( key:string ) {
return this.#templates[ key ] || false;
}
protected update() {
console.warn( "No custom update method defined." );
}
setTemplates( templates:{[key:string]: string} ) {
this.#templates = templates;
}
startUpdates() {
this.#updateInterval = setInterval( () => {
this.update();
}, this.#updateIntervalDelay );
}
stopUpdates() {
clearInterval( this.#updateInterval );
}
timestampToLocaleTime( timestamp:number ) {
return ( timestamp === -1 ) ? "-" : new Date( timestamp ).toLocaleTimeString();
}
timeToGo( timestamp:number ) {
const timeData = this.calculateTimeToGo( this.getATC().getMissionDateTime().getTime(), timestamp );
return ( timestamp === -1 ) ? "-" : timeData.elapsedMarker + timeData.time;
}
updateClock() {
const now = this.#atc.getMissionDateTime();
this.#clockElement.innerText = now.toLocaleTimeString();
}
}

View File

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

View File

@ -1,40 +0,0 @@
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,254 @@
import { getMissionData } from "../..";
import { Dropdown } from "../../controls/dropdown";
import { ATC } from "../atc";
import { ATCBoard, StripBoardStripInterface } from "../atcboard";
export class ATCBoardFlight extends ATCBoard {
constructor( atc:ATC, element:HTMLElement ) {
super( atc, element );
document.addEventListener( "deleteFlightStrip", ( ev:CustomEventInit ) => {
if ( ev.detail.id ) {
fetch( '/api/atc/flight/' + ev.detail.id, {
method: 'DELETE',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json'
}
});
}
});
this.getBoardElement().querySelectorAll( "form.ol-strip-board-add-flight" ).forEach( form => {
if ( form instanceof HTMLFormElement ) {
form.addEventListener( "submit", ev => {
ev.preventDefault();
if ( ev.target instanceof HTMLFormElement ) {
const elements = ev.target.elements;
const flightName = <HTMLInputElement>elements[0];
if ( flightName.value === "" ) {
return;
}
fetch( '/api/atc/flight/', {
method: 'POST',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json'
},
"body": JSON.stringify({
"name": flightName.value
})
});
form.reset();
}
});
}
});
}
update() {
const flights = Object.values( this.getATC().getDataHandler().getFlights() );
const stripBoard = this.getStripBoardElement();
const missionTime = this.getATC().getMissionDateTime().getTime();
for( const strip of stripBoard.children ) {
strip.toggleAttribute( "data-updating", true );
}
flights.forEach( flight => {
let strip = this.getStrip( flight.id );
if ( !strip ) {
const template = `<div class="ol-strip-board-strip" data-flight-id="${flight.id}" data-flight-status="${flight.status}">
<div data-point="name">${flight.name}</div>
<div id="flight-status-${flight.id}" class="ol-select narrow" data-point="status">
<div class="ol-select-value">${flight.status}</div>
<div class="ol-select-options"></div>
</div>
<div data-point="takeoffTime"><input type="text" name="takeoffTime" value="${this.timestampToLocaleTime( flight.takeoffTime )}" /></div>
<div data-point="timeToGo">${this.timeToGo( flight.takeoffTime )}</div>
<button data-on-click="deleteFlightStrip" data-on-click-params='{"id":"${flight.id}"}'>Delete</button>
</div>`;
stripBoard.insertAdjacentHTML( "beforeend", template );
strip = {
"id": flight.id,
"element": <HTMLElement>stripBoard.lastElementChild,
"dropdowns": {}
};
strip.element.querySelectorAll( ".ol-select" ).forEach( select => {
switch( select.getAttribute( "data-point" ) ) {
case "status":
strip.dropdowns.status = new Dropdown( select.id, ( value:string, ev:MouseEvent ) => {
fetch( '/api/atc/flight/' + flight.id, {
method: 'PATCH',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json'
},
"body": JSON.stringify({
"status": value
})
});
}, [
"unknown", "checkedin", "readytotaxi", "clearedtotaxi", "halted", "terminated"
]);
break;
}
});
strip.element.querySelectorAll( `input[type="text"]` ).forEach( input => {
if ( input instanceof HTMLInputElement ) {
input.addEventListener( "blur", ( ev ) => {
const target = ev.target;
if ( target instanceof HTMLInputElement ) {
if ( /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test( target.value ) ) {
target.value += ":00";
}
const value = target.value;
if ( value === target.dataset.previousValue ) {
return;
} else if ( value === "" ) {
this.#updateTakeoffTime( flight.id, -1 );
} else if ( /^([0-1]?[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/.test( value ) ) {
let [ hours, minutes, seconds ] = value.split( ":" ).map( str => parseInt( str ) );
const missionStart = this.getATC().getMissionStartDateTime();
this.#updateTakeoffTime( flight.id, new Date(
missionStart.getFullYear(),
missionStart.getMonth(),
missionStart.getDate(),
hours,
minutes,
seconds
).getTime() );
} else {
target.value === target.dataset.previousValue
}
}
});
}
});
this.addStrip( strip );
} else {
if ( flight.status !== strip.element.getAttribute( "data-flight-status" ) ) {
strip.element.setAttribute( "data-flight-status", flight.status );
strip.dropdowns.status.selectText( flight.status );
}
strip.element.querySelectorAll( `input[name="takeoffTime"]:not(:focus)` ).forEach( el => {
if ( el instanceof HTMLInputElement ) {
el.value = this.timestampToLocaleTime( flight.takeoffTime );
el.dataset.previousValue = el.value;
}
});
strip.element.querySelectorAll( `[data-point="timeToGo"]` ).forEach( el => {
if ( flight.takeoffTime > 0 && this.calculateTimeToGo( missionTime, flight.takeoffTime ).totalSeconds <= 120 ) {
strip.element.setAttribute( "data-time-warning", "level-1" );
}
if ( el instanceof HTMLElement ) {
el.innerText = this.timeToGo( flight.takeoffTime );
}
});
}
strip.element.toggleAttribute( "data-updating", false );
});
stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => {
this.deleteStrip( strip.getAttribute( "data-flight-id" ) || "" );
});
}
#updateTakeoffTime = function( flightId:string, time:number ) {
fetch( '/api/atc/flight/' + flightId, {
method: 'PATCH',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json'
},
"body": JSON.stringify({
"takeoffTime": time
})
});
}
}

View File

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

View File

@ -24,6 +24,8 @@ export class Dropdown {
this.#clip();
});
this.#options.classList.add( "ol-scrollable" );
// Commented out since it is a bit frustrating, particularly when the dropdown opens towards the top and not to the bottom
//this.#element.addEventListener("mouseleave", ev => {
// this.#close();
@ -53,6 +55,15 @@ export class Dropdown {
}));
}
selectText( text:string ) {
const index = [].slice.call( this.#options.children ).findIndex( ( opt:Element ) => opt.querySelector( "button" )?.innerText === text );
if ( index > -1 ) {
this.selectValue( index );
}
}
selectValue(idx: number)
{
if (idx < this.#optionsList.length)

View File

@ -169,7 +169,11 @@ export class MapContextMenu extends ContextMenu {
#setGroundUnitRole(role: string) {
this.#spawnOptions.role = role;
this.#resetGroundUnitType();
this.#groundUnitTypeDropdown.setOptions(groundUnitsDatabase.getByRole(role).map((blueprint) => { return blueprint.label }));
const types = groundUnitsDatabase.getByRole(role).map((blueprint) => { return blueprint.label } );
types.sort();
this.#groundUnitTypeDropdown.setOptions( types );
this.#groundUnitTypeDropdown.selectValue(0);
this.clip();
}
@ -179,7 +183,11 @@ export class MapContextMenu extends ContextMenu {
(<HTMLButtonElement>this.getContainer()?.querySelector("#loadout-list")).replaceChildren();
this.#groundUnitRoleDropdown.reset();
this.#groundUnitTypeDropdown.reset();
this.#groundUnitRoleDropdown.setOptions(groundUnitsDatabase.getRoles());
const roles = groundUnitsDatabase.getRoles();
roles.sort();
this.#groundUnitRoleDropdown.setOptions( roles );
this.clip();
}

View File

@ -13,6 +13,7 @@ import { getAirbases, getBullseye as getBullseyes, getConfig, getMission, getUni
import { UnitDataTable } from "./units/unitdatatable";
import { keyEventWasInInput } from "./other/utils";
import { Popup } from "./popups/popup";
import { Dropdown } from "./controls/dropdown";
var map: Map;
@ -71,9 +72,12 @@ function setup() {
let atcFeatureSwitch = featureSwitches.getSwitch("atc");
if (atcFeatureSwitch?.isEnabled()) {
atc = new ATC();
// TODO: add back buttons
atc.startUpdates();
}
new Dropdown( "app-icon", () => {} );
/* Setup event handlers */
setupEvents();
@ -227,6 +231,7 @@ function setupEvents() {
el.classList.toggle( "hide" );
})
});
}
export function getMap() {

View File

@ -1,106 +1,26 @@
<div id="atc-control-panel" data-feature-switch="atc">
<div class="ol-button" id="atc-toggle-button"></div>
</div>
<div id="strip-board-ground" class="ol-panel ol-dialog ol-strip-board ol-draggable" data-feature-switch="atc">
<div id="atc-flight-list" class="atc-tool hide" data-feature-switch="atc">
<table>
<thead>
<tr>
<th>Flight</th>
<th>T/O</th>
<th>TTG</th>
<th>Status</th>
<th> </th>
</tr>
</thead>
<tbody id="atc-flight-list-table-body"></tbody>
</table>
</div>
<div class="atc-strip-board" data-feature-switch="atc">
<div class="atc-strip-board-header">
<div class="name">Name</div>
<div class="bearing-range">BR</div>
<div class="target-altitude">t. Alt</div>
<div class="current-altitude">Alt</div>
<div class="target-speed">t. Spd</div>
<div class="current-speed">Speed</div>
<div class="runway">RWY</div>
<div class="line">Line</div>
</div>
<div class="ol-dialog-close" data-on-click="closeDialog"></div>
<div id="atc-strip-board-arrivals" class="atc-strip-board-strips ol-panel">
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Shark 3</div>
<div class="bearing-range">250 / 28</div>
<div class="target-altitude">-</div>
<div class="current-altitude">10000</div>
<div class="target-speed">-</div>
<div class="current-speed">421</div>
<div class="runway">-</div>
<div class="line">-</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Shark 2</div>
<div class="bearing-range">250 / 24</div>
<div class="target-altitude">6000</div>
<div class="current-altitude">6000</div>
<div class="target-speed">-</div>
<div class="current-speed">400</div>
<div class="runway">-</div>
<div class="line">-</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Shark 1</div>
<div class="bearing-range link-warning">262 / 12</div>
<div class="target-altitude">5000</div>
<div class="current-altitude">5100</div>
<div class="target-speed">-</div>
<div class="current-speed">367</div>
<div class="runway warning">-</div>
<div class="line">-</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Dolphin 1</div>
<div class="bearing-range">250 / 4</div>
<div class="target-altitude link-warning">3000</div>
<div class="current-altitude warning">4100</div>
<div class="target-speed">-</div>
<div class="current-speed">511</div>
<div class="runway">25L</div>
<div class="line">2nd</div>
</div>
</div>
<div class="atc-strip-board-strip">
<div class="handle"></div>
<div class="rectangular-container">
<div class="name">Whale 1</div>
<div class="bearing-range">070 / 2</div>
<div class="target-altitude">1500</div>
<div class="current-altitude">1650</div>
<div class="target-speed link-warning">350</div>
<div class="current-speed warning">312</div>
<div class="runway">25L</div>
<div class="line">1st</div>
</div>
</div>
<div class="ol-dialog-header">
<div class="ol-strip-board-clock"></div>
</div>
<div class="ol-dialog-content">
<div class="ol-strip-board-headers">
<div>Flight</div>
<div>Status</div>
<div>T/O Time</div>
<div>TTG</div>
</div>
<div class="ol-strip-board-strips"></div>
</div>
</div>
<div class="ol-dialog-footer">
<form class="ol-strip-board-add-flight">
<input type="text" name="flightName" placeholder="Add a flight" />
<button type="submit">Add</button>
</form>
</div>
</div>

View File

@ -7,7 +7,7 @@
<div class="ol-select-options">
<div id="olympus-toolbar-summary">
<h3>Olympus</h3>
<div class="accent-green app-version-number">v0.1.1</div>
<div class="accent-green app-version-number">v0.1.2</div>
</div>
<div>
<a href="https://www.discord.com" target="_blank">Discord</a>
@ -50,4 +50,13 @@
</div>
</div>
<div id="atc-navbar-control" class="ol-group-container" data-feature-switch="atc">
<div class="ol-group-header">ATC</div>
<div class="ol-group">
<button data-on-click="toggleElements" data-on-click-params='{"selector": "#strip-board-ground"}'>GND</button>
<button>TWR</button>
</div>
</div>
</nav>

View File

@ -1124,6 +1124,37 @@
</div>
<div id="fuel-percentage" data-percentage="45"></div>
<div id="fuel-display">
<div id="fuel-bar" class="highlight-coalition" data-coalition="blue" style="width:0%;"></div>
</div>
</div>
</div>
</div>
</div>
<div class="example">
<div id="unit-info-panel" class="ol-panel" style="position:relative;">
<div class="ol-panel-board">
<div id="loadout-container" class="panel-section">
<div id="loadout">
<div id="loadout-silhouette" style="--loadout-background-image:url('/images/units/f-15.png');"></div>
<div id="loadout-items">
<div data-qty="1150" data-item="30mm AP"></div>
<div data-qty="2" data-item="AIM-9M with a really long name for no reason"></div>
<div data-qty="6" data-item="Mk-82"></div>
</div>
</div>
<div id="fuel-percentage" data-percentage="45"></div>
<div id="fuel-display">
<div id="fuel-bar" class="highlight-coalition" data-coalition="blue" style="width:0%;"></div>

View File

@ -1,9 +1,9 @@
#define nwjsFolder "C:\Users\dpass\Documents\nwjs\"
#define version "v0.1.1-alpha"
#define version "v0.1.2-alpha"
[Setup]
AppName=DCS Olympus
AppVerName={#version}
AppVerName=DCS Olympus {#version}
DefaultDirName={usersavedgames}\DCS.openbeta
DefaultGroupName=DCSOlympus
OutputBaseFilename=DCSOlympus_{#version}

View File

@ -1,4 +1,4 @@
local version = "v0.1.1-alpha"
local version = "v0.1.2-alpha"
local debug = true

View File

@ -1,4 +1,4 @@
local version = 'v0.1.1-alpha'
local version = 'v0.1.2-alpha'
Olympus = {}
Olympus.OlympusDLL = nil

View File

@ -1,6 +1,6 @@
#pragma once
#define VERSION "v0.1.1"
#define VERSION "v0.1.2"
#define LOG_NAME "Olympus_log.txt"
#define REST_ADDRESS L"http://localhost:30000"
#define REST_URI L"olympus"