From d86a250575f41b402241a47f9ef77255167c2252 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 26 Mar 2023 11:00:20 +0100 Subject: [PATCH 1/8] 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/8] 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/8] 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/8] 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/8] 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 From 8d951410e5a356ecb4c94cbfdb5cc14c20c116e4 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 9 Apr 2023 10:49:40 +0100 Subject: [PATCH 6/8] String management for loadout items. --- client/public/stylesheets/unitinfopanel.css | 15 ++++------ client/views/uikit.ejs | 31 +++++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/client/public/stylesheets/unitinfopanel.css b/client/public/stylesheets/unitinfopanel.css index 6fcaafa6..1fa1e3ee 100644 --- a/client/public/stylesheets/unitinfopanel.css +++ b/client/public/stylesheets/unitinfopanel.css @@ -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 { diff --git a/client/views/uikit.ejs b/client/views/uikit.ejs index 3bfa3697..f919cfb0 100644 --- a/client/views/uikit.ejs +++ b/client/views/uikit.ejs @@ -1124,6 +1124,37 @@
+
+
+
+
+ +
+ +
+ + + + + +
+ +
+ +
+ +
+ +
+
+
+
+
+
+
+
+ +
From 14ae0d083fcf5a9972a8f2f6fbfd01f48d22b8bf Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 9 Apr 2023 15:22:27 +0100 Subject: [PATCH 7/8] Dropdowns limited to 382px. Ground unit types are automatically sorted --- client/public/stylesheets/olympus.css | 4 ++-- client/src/controls/dropdown.ts | 2 ++ client/src/controls/mapcontextmenu.ts | 12 ++++++++++-- client/src/index.ts | 4 ++++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 3e506469..ee1b97c8 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -173,7 +173,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; @@ -194,7 +194,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%; } diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index 2870db8c..fb807855 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -25,6 +25,8 @@ export class Dropdown { }); + this.#options.classList.add( "ol-scrollable" ); + this.#element.addEventListener("mouseleave", ev => { this.#close(); }); diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index c96241b7..20c4c4c5 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -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 { (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(); } diff --git a/client/src/index.ts b/client/src/index.ts index 4a5c9891..14f0037f 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -12,6 +12,7 @@ import { LogPanel } from "./panels/logpanel"; import { getAirbases, getBullseye as getBullseyes, getConfig, getMission, getUnits, setAddress, toggleDemoEnabled } from "./server/server"; import { UnitDataTable } from "./units/unitdatatable"; import { keyEventWasInInput } from "./other/utils"; +import { Dropdown } from "./controls/dropdown"; var map: Map; @@ -67,6 +68,9 @@ function setup() { // TODO: add back buttons } + + new Dropdown( "app-icon", () => {} ); + /* Setup event handlers */ setupEvents(); From df2aef498f3102ff64363d792f31013d49780b04 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Tue, 11 Apr 2023 20:30:19 +0200 Subject: [PATCH 8/8] Updated version numbers --- client/views/navbar.ejs | 2 +- installer/olympus.iss | 4 ++-- scripts/OlympusCommand.lua | 2 +- scripts/OlympusHook.lua | 2 +- src/core/src/unit.cpp | 2 +- src/shared/include/defines.h | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index d7dad433..5dbc92ae 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -7,7 +7,7 @@

Olympus

-
v0.1.1
+
v0.1.2
Discord diff --git a/installer/olympus.iss b/installer/olympus.iss index 5aa5bc1a..074bb849 100644 --- a/installer/olympus.iss +++ b/installer/olympus.iss @@ -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} diff --git a/scripts/OlympusCommand.lua b/scripts/OlympusCommand.lua index 429b6bb2..72eb98d1 100644 --- a/scripts/OlympusCommand.lua +++ b/scripts/OlympusCommand.lua @@ -1,4 +1,4 @@ -local version = "v0.1.1-alpha" +local version = "v0.1.2-alpha" local debug = false diff --git a/scripts/OlympusHook.lua b/scripts/OlympusHook.lua index 3948f693..87836c0b 100644 --- a/scripts/OlympusHook.lua +++ b/scripts/OlympusHook.lua @@ -1,4 +1,4 @@ -local version = 'v0.1.1-alpha' +local version = 'v0.1.2-alpha' Olympus = {} Olympus.OlympusDLL = nil diff --git a/src/core/src/unit.cpp b/src/core/src/unit.cpp index 5c14c6bd..8d4c80af 100644 --- a/src/core/src/unit.cpp +++ b/src/core/src/unit.cpp @@ -134,7 +134,7 @@ json::value Unit::getData(long long time) /********** Task data **********/ json[L"taskData"] = json::value::object(); - for (auto key : { L"currentTask", L"targetSpeed", L"targetAltitude", L"activePath" }) + for (auto key : { L"currentTask", L"targetSpeed", L"targetAltitude", L"activePath"}) { if (measures.find(key) != measures.end() && measures[key]->getTime() > time) json[L"taskData"][key] = measures[key]->getValue(); diff --git a/src/shared/include/defines.h b/src/shared/include/defines.h index bb868841..ed04577a 100644 --- a/src/shared/include/defines.h +++ b/src/shared/include/defines.h @@ -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"