From d86a250575f41b402241a47f9ef77255167c2252 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 26 Mar 2023 11:00:20 +0100 Subject: [PATCH 1/5] Remove old ATC code. --- client/app.js | 2 + client/public/stylesheets/atc.css | 211 --------------------------- client/routes/api/atc.js | 120 +++++++++++++++ client/src/atc/atc.ts | 87 ----------- client/src/atc/atcmockapi.ts | 7 - client/src/atc/atcmockapi/flights.ts | 40 ----- client/src/atc/flightlist.ts | 18 --- client/views/atc.ejs | 109 +------------- 8 files changed, 125 insertions(+), 469 deletions(-) create mode 100644 client/routes/api/atc.js delete mode 100644 client/src/atc/atc.ts delete mode 100644 client/src/atc/atcmockapi.ts delete mode 100644 client/src/atc/atcmockapi/flights.ts delete mode 100644 client/src/atc/flightlist.ts diff --git a/client/app.js b/client/app.js index 228c1c07..316038fb 100644 --- a/client/app.js +++ b/client/app.js @@ -3,6 +3,7 @@ var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); +var atcRouter = require('./routes/api/atc'); var indexRouter = require('./routes/index'); var uikitRouter = require('./routes/uikit'); var usersRouter = require('./routes/users'); @@ -16,6 +17,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); diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index 61f79d75..e69de29b 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -1,211 +0,0 @@ -/*** 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 { - display:flex; - flex-direction: column; -} - -.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 { - align-items: center; - 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 { - text-align: center; - width: 75px; -} - -.atc-strip-board-header > .name { - width:150px; -} - -.atc-strip-board-strips > div > .handle { - background: black; - border-radius: 50%; - cursor:grab; - height:10px; - width:10px; -} - -.atc-strip-board-strips > div > .name { - text-align: left; - width:150px; -} - -.atc-strip-board-strips > div > .warning { - background:red; - color: white; - font-weight: bold; -} - -.atc-strip-board-strips > div > .link-warning { - border: 1px solid red; - color: red; - font-weight: bold; - -} - */ diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js new file mode 100644 index 00000000..616d1361 --- /dev/null +++ b/client/routes/api/atc.js @@ -0,0 +1,120 @@ +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; +} + +Flight.prototype.getData = function() { + return { + "id": this.id, + "name": this.name + }; +} + +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.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; \ No newline at end of file diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts deleted file mode 100644 index 14710071..00000000 --- a/client/src/atc/atc.ts +++ /dev/null @@ -1,87 +0,0 @@ -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() ); - - } - -} \ No newline at end of file diff --git a/client/src/atc/atcmockapi.ts b/client/src/atc/atcmockapi.ts deleted file mode 100644 index 9720e2d0..00000000 --- a/client/src/atc/atcmockapi.ts +++ /dev/null @@ -1,7 +0,0 @@ -export abstract class ATCMockAPI { - - constructor() {} - - generateMockData() {} - -} \ No newline at end of file diff --git a/client/src/atc/atcmockapi/flights.ts b/client/src/atc/atcmockapi/flights.ts deleted file mode 100644 index 6d6b6435..00000000 --- a/client/src/atc/atcmockapi/flights.ts +++ /dev/null @@ -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 ); - - } - -} \ No newline at end of file diff --git a/client/src/atc/flightlist.ts b/client/src/atc/flightlist.ts deleted file mode 100644 index 36185eef..00000000 --- a/client/src/atc/flightlist.ts +++ /dev/null @@ -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 ); - } - -} \ No newline at end of file diff --git a/client/views/atc.ejs b/client/views/atc.ejs index ee082b92..68537f08 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,106 +1,3 @@ -
-
-
- - -
- - - - - - - - - - - -
FlightT/OTTGStatus
-
- - -
- -
-
Name
-
BR
-
t. Alt
-
Alt
-
t. Spd
-
Speed
-
RWY
-
Line
-
- - -
-
-
-
-
Shark 3
-
250 / 28
-
-
-
10000
-
-
-
421
-
-
-
-
-
-
-
-
-
-
Shark 2
-
250 / 24
-
6000
-
6000
-
-
-
400
-
-
-
-
-
-
-
-
-
-
Shark 1
- -
5000
-
5100
-
-
-
367
-
-
-
-
-
-
-
-
-
-
Dolphin 1
-
250 / 4
- -
4100
-
-
-
511
-
25L
-
2nd
-
-
-
-
-
-
Whale 1
-
070 / 2
-
1500
-
1650
- -
312
-
25L
-
1st
-
-
-
- - -
- +
+ ATC +
\ No newline at end of file From da008c220d27bf491baa004565e1c19e495c3b90 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 26 Mar 2023 15:39:21 +0100 Subject: [PATCH 2/5] Added beginnings of new ATC framework. --- client/src/atc/atc.ts | 109 +++++++++++++++++++++++++++++++++ client/src/atc/atcboard.ts | 15 +++++ client/src/atc/board/flight.ts | 11 ++++ client/src/index.ts | 2 +- client/views/atc.ejs | 6 +- 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 client/src/atc/atc.ts create mode 100644 client/src/atc/atcboard.ts create mode 100644 client/src/atc/board/flight.ts diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts new file mode 100644 index 00000000..8a668975 --- /dev/null +++ b/client/src/atc/atc.ts @@ -0,0 +1,109 @@ +import { ATCBoard } from "./atcboard"; +import { ATCBoardFlight } from "./board/flight"; + +export interface FlightInterface { + id : string; + name : string; +} + + +class ATCDataHandler { + + #atc:ATC; + + #updateInterval:number|undefined = undefined; + #updateIntervalDelay:number = 1000; + + + constructor( atc:ATC ) { + + this.#atc = atc; + + } + + startUpdates() { + + this.#updateInterval = setInterval( () => { + + fetch( '/api/atc/flight', { + method: 'GET', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + } + }) + .then( response => response.json() ) + .then( data => { + this.#atc.setFlights( data ); + }); + + }, this.#updateIntervalDelay ); + + } + + stopUpdates() { + + clearInterval( this.#updateInterval ); + + } + +} + + + + +export class ATC { + + #boards:[] = []; + #dataHandler:ATCDataHandler; + #flights:{[key:string]: FlightInterface} = {}; + + + constructor() { + + this.#dataHandler = new ATCDataHandler( this ); + + this.lookForBoards(); + + } + + + addBoard( board:T ) { + + } + + + lookForBoards() { + + document.querySelectorAll( "ol-atc-board" ).forEach( board => { + + if ( board instanceof HTMLElement ) { + this.addBoard( new ATCBoardFlight( board ) ); + } + + }); + + } + + + setFlights( flights:{[key:string]: any} ) { + + this.#flights = flights; + + } + + + startUpdates() { + + this.#dataHandler.startUpdates(); + + } + + + stopUpdates() { + + this.#dataHandler.stopUpdates(); + + } + +} \ No newline at end of file diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts new file mode 100644 index 00000000..32010b3e --- /dev/null +++ b/client/src/atc/atcboard.ts @@ -0,0 +1,15 @@ +export abstract class ATCBoard { + + #boardElement:HTMLElement; + + constructor( boardElement:HTMLElement ) { + + this.#boardElement = boardElement; + + } + + getBoardElement() { + return this.#boardElement; + } + +} \ No newline at end of file diff --git a/client/src/atc/board/flight.ts b/client/src/atc/board/flight.ts new file mode 100644 index 00000000..6935dfe7 --- /dev/null +++ b/client/src/atc/board/flight.ts @@ -0,0 +1,11 @@ +import { ATCBoard } from "../atcboard"; + +export class ATCBoardFlight extends ATCBoard { + + constructor( element:HTMLElement ) { + + super( element ); + + } + +} \ No newline at end of file diff --git a/client/src/index.ts b/client/src/index.ts index 2a8b52d5..da4940ba 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -63,7 +63,7 @@ function setup() { let atcFeatureSwitch = featureSwitches.getSwitch("atc"); if (atcFeatureSwitch?.isEnabled()) { atc = new ATC(); - // TODO: add back buttons + atc.startUpdates(); } /* Setup event handlers */ diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 68537f08..0117cc0e 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,3 +1,5 @@ -
- ATC +
+ +
+
\ No newline at end of file From 14147168f97e29fdf56b82272aa1995ee70f5005 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 26 Mar 2023 20:00:23 +0100 Subject: [PATCH 3/5] ATC stuff. --- client/routes/api/atc.js | 5 ++- client/src/atc/atc.ts | 46 +++++++++++++++-------- client/src/atc/atcboard.ts | 67 +++++++++++++++++++++++++++++++++- client/src/atc/board/flight.ts | 66 +++++++++++++++++++++++++++++++-- client/views/atc.ejs | 2 +- 5 files changed, 164 insertions(+), 22 deletions(-) diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js index 616d1361..fa6efabf 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -26,8 +26,9 @@ function uuidv4() { function Flight( name ) { - this.id = uuidv4(); - this.name = name; + this.id = uuidv4(); + this.name = name; + this.status = "unknown"; } Flight.prototype.getData = function() { diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts index 8a668975..d3010987 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -2,14 +2,16 @@ import { ATCBoard } from "./atcboard"; import { ATCBoardFlight } from "./board/flight"; export interface FlightInterface { - id : string; - name : string; + id : string; + name : string; + status : "unknown"; } class ATCDataHandler { #atc:ATC; + #flights:{[key:string]: FlightInterface} = {}; #updateInterval:number|undefined = undefined; #updateIntervalDelay:number = 1000; @@ -21,6 +23,12 @@ class ATCDataHandler { } + + getFlights() { + return this.#flights; + } + + startUpdates() { this.#updateInterval = setInterval( () => { @@ -34,13 +42,21 @@ class ATCDataHandler { }) .then( response => response.json() ) .then( data => { - this.#atc.setFlights( data ); + this.setFlights( data ); }); }, this.#updateIntervalDelay ); } + + setFlights( flights:{[key:string]: any} ) { + + this.#flights = flights; + + } + + stopUpdates() { clearInterval( this.#updateInterval ); @@ -54,9 +70,8 @@ class ATCDataHandler { export class ATC { - #boards:[] = []; + #boards:ATCBoard[] = []; #dataHandler:ATCDataHandler; - #flights:{[key:string]: FlightInterface} = {}; constructor() { @@ -69,30 +84,31 @@ export class ATC { addBoard( board:T ) { + + board.startUpdates(); + + this.#boards.push( board ); } + getDataHandler() { + return this.#dataHandler; + } + + lookForBoards() { - document.querySelectorAll( "ol-atc-board" ).forEach( board => { + document.querySelectorAll( ".ol-atc-board" ).forEach( board => { if ( board instanceof HTMLElement ) { - this.addBoard( new ATCBoardFlight( board ) ); + this.addBoard( new ATCBoardFlight( this, board ) ); } }); } - - setFlights( flights:{[key:string]: any} ) { - - this.#flights = flights; - - } - - startUpdates() { this.#dataHandler.startUpdates(); diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 32010b3e..780cfed6 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -1,15 +1,80 @@ +import { ATC } from "./atc"; + +export interface ATCTemplateInterface { + "template": string +} + export abstract class ATCBoard { + #atc:ATC; + #templates: {[key:string]: string} = {}; + + // Elements #boardElement:HTMLElement; + #stripBoardElement:HTMLElement; + + // Update timing + #updateInterval:number|undefined = undefined; + #updateIntervalDelay:number = 1000; - constructor( boardElement:HTMLElement ) { + constructor( atc:ATC, boardElement:HTMLElement ) { + + this.#atc = atc; this.#boardElement = boardElement; + this.#stripBoardElement = this.getBoardElement().querySelector( ".atc-strip-board" ); } + + getATC() { + return this.#atc; + } + + getBoardElement() { return this.#boardElement; } + + + getStripBoardElement() { + return this.#stripBoardElement; + } + + + 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 ); + + } + } \ No newline at end of file diff --git a/client/src/atc/board/flight.ts b/client/src/atc/board/flight.ts index 6935dfe7..8bd4b1ba 100644 --- a/client/src/atc/board/flight.ts +++ b/client/src/atc/board/flight.ts @@ -1,11 +1,71 @@ +import { getMissionData } from "../.."; +import { ATC } from "../atc"; import { ATCBoard } from "../atcboard"; export class ATCBoardFlight extends ATCBoard { - constructor( element:HTMLElement ) { + constructor( atc:ATC, element:HTMLElement ) { - super( element ); + super( atc, element ); + + console.log( getMissionData() ); + + document.addEventListener( "deleteFlightStrip", ( ev:CustomEventInit ) => { + + if ( ev.detail.id ) { + + fetch( '/api/atc/flight/' + ev.detail.id, { + method: 'DELETE', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + } + }); + + } + + }); } - + + + update() { + + const flights = Object.values( this.getATC().getDataHandler().getFlights() ); + const stripBoard = this.getStripBoardElement(); + + + for( const strip of stripBoard.children ) { + strip.toggleAttribute( "data-updating", true ); + } + + + flights.forEach( flight => { + + let strip = stripBoard.querySelector( `[data-flight-id="${flight.id}"]`); + + if ( !strip ) { + + const template = `
+
${flight.name}
+
${flight.status}
+ +
`; + + stripBoard.insertAdjacentHTML( "beforeend", template ); + + strip = stripBoard.lastElementChild; + + } + + strip.toggleAttribute( "data-updating", false ); + + }); + + stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => { + strip.remove(); + }); + + } + } \ No newline at end of file diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 0117cc0e..66c36e5f 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,5 +1,5 @@
-
+
\ No newline at end of file From 6f64bd1622315353c4bc436304606c73a51f3ee3 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Wed, 29 Mar 2023 21:15:33 +0100 Subject: [PATCH 4/5] ATC Ground board PoC. --- client/public/stylesheets/atc.css | 114 ++++++++++++++++ client/public/stylesheets/layout.css | 8 +- client/public/stylesheets/olympus.css | 10 +- client/routes/api/atc.js | 79 ++++++++++- client/src/atc/atc.ts | 25 +++- client/src/atc/atcboard.ts | 53 +++++++- client/src/atc/board/flight.ts | 185 +++++++++++++++++++++++++- client/src/controls/dropdown.ts | 25 +++- client/src/index.ts | 21 +-- client/views/atc.ejs | 25 +++- 10 files changed, 502 insertions(+), 43 deletions(-) diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index e69de29b..9c779eb3 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -0,0 +1,114 @@ + + +.ol-strip-board-strips { + display:flex; + flex-direction: column; + row-gap: 4px; +} + +.ol-strip-board-strip { + align-items: center; + border-radius: var( --border-radius-sm ); + column-gap: 4px; + display:flex; + flex-flow: row nowrap; + padding:4px; +} + +.ol-strip-board-strip[data-flight-status="checkedin"] { + background-color: #ffffff2A; +} + +.ol-strip-board-strip[data-flight-status="readytotaxi"] { + background-color: #ffff002A; +} + +.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:70px; +} + +.ol-strip-board-strip input[type="text"] { + appearance: none; + background-color: transparent; + border:1px solid #ffffff30; + border-radius: var( --border-radius-sm ); + color:white; + font-size:12px; + font-weight:normal; + outline:none; + padding: 4px 0; + text-align: center; + width:100%; +} + +.ol-strip-board-strip[data-time-warning="level-1"] [data-point="timeToGo"] { + border:1px solid #cc0000; +} + +.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; +} + +.ol-strip-board-strip :nth-child(4) { + text-align: center; +} + +.ol-strip-board-strip > [data-point="name"] { + text-overflow: ellipsis; + overflow:hidden; +} + +.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 ); +} \ No newline at end of file diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index ed682be4..4c713e0e 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -124,8 +124,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 { @@ -137,6 +137,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; diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 02b41a03..a273c0ef 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -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"); @@ -123,14 +124,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; } diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js index fa6efabf..517533cb 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -26,18 +26,53 @@ function uuidv4() { function Flight( name ) { - this.id = uuidv4(); - this.name = name; - this.status = "unknown"; + this.id = uuidv4(); + this.name = name; + this.status = "unknown"; + this.takeoffTime = -1; } Flight.prototype.getData = function() { return { - "id": this.id, - "name": this.name + "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; } @@ -86,6 +121,40 @@ app.get( "/flight", ( req, res ) => { }); +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 ) { diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts index d3010987..d63634bf 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -2,9 +2,10 @@ import { ATCBoard } from "./atcboard"; import { ATCBoardFlight } from "./board/flight"; export interface FlightInterface { - id : string; - name : string; - status : "unknown"; + id : string; + name : string; + status : "unknown"; + takeoffTime : number; } @@ -73,6 +74,7 @@ export class ATC { #boards:ATCBoard[] = []; #dataHandler:ATCDataHandler; + #initDate:Date = new Date(); constructor() { @@ -97,9 +99,24 @@ export class ATC { } + 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-atc-board" ).forEach( board => { + document.querySelectorAll( ".ol-strip-board" ).forEach( board => { if ( board instanceof HTMLElement ) { this.addBoard( new ATCBoardFlight( this, board ) ); diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 780cfed6..50766884 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -1,3 +1,4 @@ +import { zeroAppend } from "../other/utils"; import { ATC } from "./atc"; export interface ATCTemplateInterface { @@ -11,6 +12,7 @@ export abstract class ATCBoard { // Elements #boardElement:HTMLElement; + #clockElement:HTMLElement; #stripBoardElement:HTMLElement; // Update timing @@ -22,7 +24,34 @@ export abstract class ATCBoard { this.#atc = atc; this.#boardElement = boardElement; - this.#stripBoardElement = this.getBoardElement().querySelector( ".atc-strip-board" ); + this.#stripBoardElement = this.getBoardElement().querySelector( ".ol-strip-board-strips" ); + this.#clockElement = this.getBoardElement().querySelector( ".ol-strip-board-clock" ); + + + setInterval( () => { + this.updateClock(); + }, 1000 ); + + } + + + calculateTimeToGo( fromTimestamp:number, toTimestamp:number ) { + + let timestamp = ( toTimestamp - fromTimestamp ) / 1000; + + const hours = zeroAppend( Math.floor( timestamp / 3600 ), 2 ); + const rMinutes = timestamp % 3600; + + const minutes = zeroAppend( Math.floor( rMinutes / 60 ), 2 ); + const seconds = zeroAppend( Math.floor( rMinutes % 60 ), 2 ); + + return { + "hours": hours, + "minutes": minutes, + "seconds": seconds, + "time": `${hours}:${minutes}:${seconds}`, + "totalSeconds": timestamp + }; } @@ -76,5 +105,27 @@ export abstract class ATCBoard { } + + timestampToLocaleTime( timestamp:number ) { + + return ( timestamp === -1 ) ? "-" : new Date( timestamp ).toLocaleTimeString(); + + } + + + timeToGo( timestamp:number ) { + + return ( timestamp === -1 ) ? "-" : this.calculateTimeToGo( this.getATC().getMissionDateTime().getTime(), timestamp ).time; + + } + + + updateClock() { + + const now = this.#atc.getMissionDateTime(); + this.#clockElement.innerText = now.toLocaleTimeString(); + + } + } \ No newline at end of file diff --git a/client/src/atc/board/flight.ts b/client/src/atc/board/flight.ts index 8bd4b1ba..a348f38c 100644 --- a/client/src/atc/board/flight.ts +++ b/client/src/atc/board/flight.ts @@ -1,15 +1,15 @@ import { getMissionData } from "../.."; +import { Dropdown } from "../../controls/dropdown"; import { ATC } from "../atc"; import { ATCBoard } from "../atcboard"; + export class ATCBoardFlight extends ATCBoard { constructor( atc:ATC, element:HTMLElement ) { super( atc, element ); - console.log( getMissionData() ); - document.addEventListener( "deleteFlightStrip", ( ev:CustomEventInit ) => { if ( ev.detail.id ) { @@ -26,6 +26,46 @@ export class ATCBoardFlight extends ATCBoard { }); + + 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 = 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(); + + } + + }); + + } + + }); + } @@ -34,6 +74,7 @@ export class ATCBoardFlight extends ATCBoard { 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 ); @@ -46,9 +87,18 @@ export class ATCBoardFlight extends ATCBoard { if ( !strip ) { - const template = `
+ const template = `
${flight.name}
-
${flight.status}
+ +
+
${flight.status}
+
+
+ +
+ +
${this.timeToGo( flight.takeoffTime )}
+
`; @@ -56,6 +106,115 @@ export class ATCBoardFlight extends ATCBoard { strip = stripBoard.lastElementChild; + strip.querySelectorAll( ".ol-select" ).forEach( select => { + + switch( select.getAttribute( "data-point" ) ) { + + case "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.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 + + } + + } + + }); + + } + + }); + + } else { + + // TODO: change status dropdown if status is different + strip.setAttribute( "data-flight-status", flight.status ); + + strip.querySelectorAll( `input[name="takeoffTime"]:not(:focus)` ).forEach( el => { + if ( el instanceof HTMLInputElement ) { + el.value = this.timestampToLocaleTime( flight.takeoffTime ); + el.dataset.previousValue = el.value; + } + }); + + strip.querySelectorAll( `[data-point="timeToGo"]` ).forEach( el => { + + if ( flight.takeoffTime > 0 && this.calculateTimeToGo( missionTime, flight.takeoffTime ).totalSeconds <= 120 ) { + strip?.setAttribute( "data-time-warning", "level-1" ); + } + + if ( el instanceof HTMLElement ) { + el.innerText = this.timeToGo( flight.takeoffTime ); + } + + }); + + } strip.toggleAttribute( "data-updating", false ); @@ -68,4 +227,22 @@ export class ATCBoardFlight extends ATCBoard { } + + + + #updateTakeoffTime = function( flightId:string, time:number ) { + + fetch( '/api/atc/flight/' + flightId, { + method: 'PATCH', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "takeoffTime": time + }) + }); + + } + } \ No newline at end of file diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index 52716b01..04f656d8 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -13,8 +13,29 @@ export class Dropdown { this.#value = this.#element.querySelector(".ol-select-value"); this.#defaultValue = this.#value.innerText; this.#callback = callback; - if (options != null) + if (options != null) { this.setOptions(options); + } + + + + // Do open/close toggle + this.#element.addEventListener("click", ev => { + + if ( ev.target instanceof HTMLElement && ev.target.nodeName !== "A" ) { + ev.preventDefault(); + } + + ev.stopPropagation(); + this.#element.classList.toggle("is-open"); + + }); + + // Autoclose on mouseleave + this.#element.addEventListener("mouseleave", ev => { + this.#element.classList.remove("is-open"); + }); + } setOptions(optionsList: string[]) @@ -27,7 +48,7 @@ export class Dropdown { div.appendChild(button); button.addEventListener("click", (e: MouseEvent) => { this.#value.innerText = option; - this.#callback(option); + this.#callback( option, e ); }); return div; })); diff --git a/client/src/index.ts b/client/src/index.ts index da4940ba..1e8e02e0 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -124,6 +124,7 @@ function checkSessionHash(newSessionHash: string) { function setupEvents() { /* Generic clicks */ document.addEventListener("click", (ev) => { + if (ev instanceof PointerEvent && ev.target instanceof HTMLElement) { const target = ev.target; if (target.classList.contains("olympus-dialog-close")) { @@ -194,26 +195,6 @@ function setupEvents() { }) }); - /** Olympus UI ***/ - document.querySelectorAll(".ol-select").forEach(select => { - - // Do open/close toggle - select.addEventListener("click", ev => { - - if ( ev.target instanceof HTMLElement && ev.target.nodeName !== "A" ) { - ev.preventDefault(); - } - - ev.stopPropagation(); - select.classList.toggle("is-open"); - }); - - // Autoclose on mouseleave - select.addEventListener("mouseleave", ev => { - select.classList.remove("is-open"); - }); - - }); } export function getMap() { diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 66c36e5f..7f94557a 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,5 +1,26 @@ -
+
+ +
-
+
+
+
+ +
+
+
Flight
+
Status
+
T/O Time
+
TTG
+
+
+
+ +
\ No newline at end of file From 071942b632b4e451ed4713785b8a4ebc1671cd10 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Tue, 4 Apr 2023 23:38:08 +0100 Subject: [PATCH 5/5] Slightly nicer PoC. --- client/public/stylesheets/atc.css | 6 +-- client/public/stylesheets/layout.css | 2 + client/public/stylesheets/olympus.css | 41 +++++++---------- client/src/atc/atcboard.ts | 64 ++++++++++++++++++++++++--- client/src/atc/board/flight.ts | 38 +++++++++------- client/src/controls/dropdown.ts | 9 ++++ client/views/atc.ejs | 2 +- client/views/navbar.ejs | 9 ++++ 8 files changed, 122 insertions(+), 49 deletions(-) diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index 9c779eb3..7eb22afb 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -12,7 +12,7 @@ column-gap: 4px; display:flex; flex-flow: row nowrap; - padding:4px; + row-gap:4px; } .ol-strip-board-strip[data-flight-status="checkedin"] { @@ -20,7 +20,7 @@ } .ol-strip-board-strip[data-flight-status="readytotaxi"] { - background-color: #ffff002A; + background-color: #ffff0063; } .ol-strip-board-strip[data-flight-status="clearedtotaxi"] { @@ -46,7 +46,7 @@ padding: 4px; text-overflow: ellipsis; white-space: nowrap; - width:70px; + width:80px; } .ol-strip-board-strip input[type="text"] { diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css index 4c713e0e..fe45ecaf 100644 --- a/client/public/stylesheets/layout.css +++ b/client/public/stylesheets/layout.css @@ -7,6 +7,8 @@ } #primary-toolbar { + align-items: center; + display:flex; position: absolute; left: 10px; top: 10px; diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index a273c0ef..dd94619a 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -346,6 +346,11 @@ nav.ol-panel> :last-child { row-gap: 4px; } +.ol-group-header { + text-align: center; + width: 100%; +} + .ol-panel .ol-group.wrap { flex-wrap: wrap; } @@ -545,30 +550,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; } @@ -619,6 +600,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; diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 50766884..0ab9f52d 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -1,8 +1,12 @@ +import { Draggable } from "leaflet"; +import { Dropdown } from "../controls/dropdown"; import { zeroAppend } from "../other/utils"; import { ATC } from "./atc"; -export interface ATCTemplateInterface { - "template": string +export interface StripBoardStripInterface { + "id": string, + "element": HTMLElement, + "dropdowns": {[key:string]: Dropdown} } export abstract class ATCBoard { @@ -14,6 +18,9 @@ export abstract class ATCBoard { #boardElement:HTMLElement; #clockElement:HTMLElement; #stripBoardElement:HTMLElement; + + // Content + #strips:{[key:string]: StripBoardStripInterface} = {}; // Update timing #updateInterval:number|undefined = undefined; @@ -27,6 +34,18 @@ export abstract class ATCBoard { this.#stripBoardElement = this.getBoardElement().querySelector( ".ol-strip-board-strips" ); this.#clockElement = 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(); @@ -35,17 +54,30 @@ export abstract class ATCBoard { } + addStrip( strip:StripBoardStripInterface ) { + this.#strips[ strip.id ] = strip; + } + + calculateTimeToGo( fromTimestamp:number, toTimestamp:number ) { let timestamp = ( toTimestamp - fromTimestamp ) / 1000; - const hours = zeroAppend( Math.floor( timestamp / 3600 ), 2 ); + 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 = zeroAppend( Math.floor( rMinutes / 60 ), 2 ); + 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, @@ -56,6 +88,16 @@ export abstract class ATCBoard { } + deleteStrip( id:string ) { + + if ( this.#strips.hasOwnProperty( id ) ) { + this.#strips[ id ].element.remove(); + delete this.#strips[ id ]; + } + + } + + getATC() { return this.#atc; } @@ -71,6 +113,16 @@ export abstract class ATCBoard { } + getStrips() { + return this.#strips; + } + + + getStrip( id:string ) { + return this.#strips[ id ] || false; + } + + getTemplate( key:string ) { return this.#templates[ key ] || false; @@ -115,7 +167,9 @@ export abstract class ATCBoard { timeToGo( timestamp:number ) { - return ( timestamp === -1 ) ? "-" : this.calculateTimeToGo( this.getATC().getMissionDateTime().getTime(), timestamp ).time; + const timeData = this.calculateTimeToGo( this.getATC().getMissionDateTime().getTime(), timestamp ); + + return ( timestamp === -1 ) ? "-" : timeData.elapsedMarker + timeData.time; } diff --git a/client/src/atc/board/flight.ts b/client/src/atc/board/flight.ts index a348f38c..b9830fc8 100644 --- a/client/src/atc/board/flight.ts +++ b/client/src/atc/board/flight.ts @@ -1,7 +1,7 @@ import { getMissionData } from "../.."; import { Dropdown } from "../../controls/dropdown"; import { ATC } from "../atc"; -import { ATCBoard } from "../atcboard"; +import { ATCBoard, StripBoardStripInterface } from "../atcboard"; export class ATCBoardFlight extends ATCBoard { @@ -83,7 +83,7 @@ export class ATCBoardFlight extends ATCBoard { flights.forEach( flight => { - let strip = stripBoard.querySelector( `[data-flight-id="${flight.id}"]`); + let strip = this.getStrip( flight.id ); if ( !strip ) { @@ -104,15 +104,20 @@ export class ATCBoardFlight extends ATCBoard { stripBoard.insertAdjacentHTML( "beforeend", template ); - strip = stripBoard.lastElementChild; - strip.querySelectorAll( ".ol-select" ).forEach( select => { + strip = { + "id": flight.id, + "element": stripBoard.lastElementChild, + "dropdowns": {} + }; + + strip.element.querySelectorAll( ".ol-select" ).forEach( select => { switch( select.getAttribute( "data-point" ) ) { case "status": - new Dropdown( select.id, ( value:string, ev:MouseEvent ) => { + strip.dropdowns.status = new Dropdown( select.id, ( value:string, ev:MouseEvent ) => { fetch( '/api/atc/flight/' + flight.id, { method: 'PATCH', @@ -135,10 +140,7 @@ export class ATCBoardFlight extends ATCBoard { }); - - - - strip.querySelectorAll( `input[type="text"]` ).forEach( input => { + strip.element.querySelectorAll( `input[type="text"]` ).forEach( input => { if ( input instanceof HTMLInputElement ) { @@ -190,22 +192,26 @@ export class ATCBoardFlight extends ATCBoard { }); + this.addStrip( strip ); + } else { - // TODO: change status dropdown if status is different - strip.setAttribute( "data-flight-status", flight.status ); + if ( flight.status !== strip.element.getAttribute( "data-flight-status" ) ) { + strip.element.setAttribute( "data-flight-status", flight.status ); + strip.dropdowns.status.selectText( flight.status ); + } - strip.querySelectorAll( `input[name="takeoffTime"]:not(:focus)` ).forEach( el => { + 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.querySelectorAll( `[data-point="timeToGo"]` ).forEach( el => { + strip.element.querySelectorAll( `[data-point="timeToGo"]` ).forEach( el => { if ( flight.takeoffTime > 0 && this.calculateTimeToGo( missionTime, flight.takeoffTime ).totalSeconds <= 120 ) { - strip?.setAttribute( "data-time-warning", "level-1" ); + strip.element.setAttribute( "data-time-warning", "level-1" ); } if ( el instanceof HTMLElement ) { @@ -217,12 +223,12 @@ export class ATCBoardFlight extends ATCBoard { } - strip.toggleAttribute( "data-updating", false ); + strip.element.toggleAttribute( "data-updating", false ); }); stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => { - strip.remove(); + this.deleteStrip( strip.getAttribute( "data-flight-id" ) || "" ); }); } diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index 04f656d8..ad1a5d93 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -54,6 +54,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) diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 7f94557a..062f5738 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,4 +1,4 @@ -
+
diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index fd0960d1..9a385f22 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -50,4 +50,13 @@
+ + +
+
ATC
+
+ + +
+
\ No newline at end of file