From 87079827694641a47e30e17a4d19da45450500b5 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Fri, 14 Apr 2023 23:11:55 +0100 Subject: [PATCH 1/4] Flights now listed by board. --- client/public/stylesheets/atc.css | 28 ++++++++++--- client/routes/api/atc.js | 25 ++++++++++-- client/src/atc/atc.ts | 33 +++++++++++++-- client/src/atc/atcboard.ts | 40 +++++++++++++++---- client/src/atc/board/{flight.ts => ground.ts} | 18 ++------- client/src/other/utils.ts | 8 ++++ client/views/atc.ejs | 29 +++++++++++++- client/views/navbar.ejs | 2 +- 8 files changed, 148 insertions(+), 35 deletions(-) rename client/src/atc/board/{flight.ts => ground.ts} (92%) diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index 7eb22afb..652d866e 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -67,17 +67,26 @@ 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) { +[data-board-type="ground"] .ol-strip-board-headers :nth-child(1), +[data-board-type="ground"] .ol-strip-board-headers :nth-child(2), +[data-board-type="ground"] .ol-strip-board-strip :nth-child(1), +[data-board-type="ground"] .ol-strip-board-strip :nth-child(2) { width:120px; } -.ol-strip-board-strip :nth-child(4) { +[data-board-type="ground"] .ol-strip-board-strip :nth-child(4) { text-align: center; } + + +[data-board-type="tower"] .ol-strip-board-strip :nth-child(2) input { + width:25px; +} + + + + .ol-strip-board-strip > [data-point="name"] { text-overflow: ellipsis; overflow:hidden; @@ -111,4 +120,13 @@ .ol-strip-board-add-flight input { border-bottom-left-radius: var( --border-radius-sm ); border-top-left-radius: var( --border-radius-sm ); +} + + +[data-board-type="ground"] { + translate: 0 100%; +} + +[data-board-type="tower"] { + translate: 0 -100%; } \ No newline at end of file diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js index 517533cb..82e8865a 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -25,8 +25,9 @@ function uuidv4() { -function Flight( name ) { +function Flight( name, boardId ) { this.id = uuidv4(); + this.boardId = boardId; this.name = name; this.status = "unknown"; this.takeoffTime = -1; @@ -35,6 +36,7 @@ function Flight( name ) { Flight.prototype.getData = function() { return { "id" : this.id, + "boardId" : this.boardId, "name" : this.name, "status" : this.status, "takeoffTime" : this.takeoffTime @@ -116,7 +118,20 @@ const dataHandler = new ATCDataHandler( { app.get( "/flight", ( req, res ) => { - res.json( dataHandler.getFlights() ); + let flights = Object.values( dataHandler.getFlights() ); + + if ( flights && req.query.boardId ) { + + flights = flights.reduce( ( acc, flight ) => { + if ( flight.boardId === req.query.boardId ) { + acc[ flight.id ] = flight; + } + return acc; + }, {} ); + + } + + res.json( flights ); }); @@ -157,11 +172,15 @@ app.patch( "/flight/:flightId", ( req, res ) => { app.post( "/flight", ( req, res ) => { + if ( !req.body.boardId ) { + res.status( 400 ).send( "Invalid/missing boardId" ); + } + if ( !req.body.name ) { res.status( 400 ).send( "Invalid/missing flight name" ); } - const flight = new Flight( req.body.name ); + const flight = new Flight( req.body.name, req.body.boardId ); dataHandler.addFlight( flight ); diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts index d63634bf..af8a682e 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -1,8 +1,10 @@ import { ATCBoard } from "./atcboard"; -import { ATCBoardFlight } from "./board/flight"; +import { ATCBoardGround } from "./board/ground"; +import { ATCBoardTower } from "./board/tower"; export interface FlightInterface { id : string; + boardId : string; name : string; status : "unknown"; takeoffTime : number; @@ -25,8 +27,16 @@ class ATCDataHandler { } - getFlights() { - return this.#flights; + getFlights( boardId:string ) { + + return Object.values( this.#flights ).reduce( ( acc:{[key:string]: FlightInterface}, flight ) => { + + if ( flight.boardId === boardId ) { + acc[ flight.id ] = flight; + } + + return acc; + }, {} ); } @@ -119,7 +129,22 @@ export class ATC { document.querySelectorAll( ".ol-strip-board" ).forEach( board => { if ( board instanceof HTMLElement ) { - this.addBoard( new ATCBoardFlight( this, board ) ); + + switch ( board.dataset.boardType ) { + + case "ground": + this.addBoard( new ATCBoardGround( this, board ) ); + return; + + case "tower": + this.addBoard( new ATCBoardTower( this, board ) ); + return; + + default: + console.warn( "Unknown board type for ATC board, got: " + board.dataset.boardType ); + + } + } }); diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 0ab9f52d..5a54fd92 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -1,6 +1,6 @@ import { Draggable } from "leaflet"; import { Dropdown } from "../controls/dropdown"; -import { zeroAppend } from "../other/utils"; +import { generateUUIDv4, zeroAppend } from "../other/utils"; import { ATC } from "./atc"; export interface StripBoardStripInterface { @@ -12,6 +12,7 @@ export interface StripBoardStripInterface { export abstract class ATCBoard { #atc:ATC; + #boardId:string = ""; #templates: {[key:string]: string} = {}; // Elements @@ -27,7 +28,9 @@ export abstract class ATCBoard { #updateIntervalDelay:number = 1000; - constructor( atc:ATC, boardElement:HTMLElement ) { + constructor( atc:ATC, boardElement:HTMLElement, options?:{[key:string]: any} ) { + + options = options || {}; this.#atc = atc; this.#boardElement = boardElement; @@ -51,6 +54,24 @@ export abstract class ATCBoard { this.updateClock(); }, 1000 ); + + } + + + addFlight( flightName:string ) { + + return fetch( '/api/atc/flight/', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : flightName + }) + }); + } @@ -108,6 +129,11 @@ export abstract class ATCBoard { } + getBoardId(): string { + return this.getBoardElement().id; + } + + getStripBoardElement() { return this.#stripBoardElement; } @@ -130,11 +156,6 @@ export abstract class ATCBoard { } - protected update() { - console.warn( "No custom update method defined." ); - } - - setTemplates( templates:{[key:string]: string} ) { this.#templates = templates; } @@ -174,6 +195,11 @@ export abstract class ATCBoard { } + protected update() { + console.warn( "No custom update method defined." ); + } + + updateClock() { const now = this.#atc.getMissionDateTime(); diff --git a/client/src/atc/board/flight.ts b/client/src/atc/board/ground.ts similarity index 92% rename from client/src/atc/board/flight.ts rename to client/src/atc/board/ground.ts index b9830fc8..236f01a4 100644 --- a/client/src/atc/board/flight.ts +++ b/client/src/atc/board/ground.ts @@ -1,10 +1,9 @@ -import { getMissionData } from "../.."; import { Dropdown } from "../../controls/dropdown"; import { ATC } from "../atc"; -import { ATCBoard, StripBoardStripInterface } from "../atcboard"; +import { ATCBoard } from "../atcboard"; -export class ATCBoardFlight extends ATCBoard { +export class ATCBoardGround extends ATCBoard { constructor( atc:ATC, element:HTMLElement ) { @@ -45,16 +44,7 @@ export class ATCBoardFlight extends ATCBoard { return; } - fetch( '/api/atc/flight/', { - method: 'POST', - headers: { - 'Accept': '*/*', - 'Content-Type': 'application/json' - }, - "body": JSON.stringify({ - "name": flightName.value - }) - }); + this.addFlight( flightName.value ); form.reset(); @@ -71,7 +61,7 @@ export class ATCBoardFlight extends ATCBoard { update() { - const flights = Object.values( this.getATC().getDataHandler().getFlights() ); + const flights = Object.values( this.getATC().getDataHandler().getFlights( this.getBoardId() ) ); const stripBoard = this.getStripBoardElement(); const missionTime = this.getATC().getMissionDateTime().getTime(); diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts index 5e591a94..69c164c7 100644 --- a/client/src/other/utils.ts +++ b/client/src/other/utils.ts @@ -69,6 +69,14 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number) } +export function generateUUIDv4() { + 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); + }); +} + + export function keyEventWasInInput( event:KeyboardEvent ) { const target = event.target; diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 062f5738..fb007578 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,8 +1,35 @@ -
+
+

Tower

+
+
+ +
+
+
Flight
+
+
+
+ + + +
+ + +
+ +
+ +
+

Ground

diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index d7dad433..04296162 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -56,7 +56,7 @@
ATC
- +
\ No newline at end of file From e84fa7caaaaad6798a3036b00adf5f23cbf020f0 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 16 Apr 2023 12:58:14 +0100 Subject: [PATCH 2/4] Can add flights by click. --- client/public/stylesheets/atc.css | 19 +++- client/routes/api/atc.js | 12 +- client/src/atc/atc.ts | 1 + client/src/atc/atcboard.ts | 151 +++++++++++++++++++++++--- client/src/atc/board/ground.ts | 53 +-------- client/src/panels/unitcontrolpanel.ts | 2 - client/views/atc.ejs | 62 ++--------- 7 files changed, 180 insertions(+), 120 deletions(-) diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index 652d866e..baab81f5 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -80,10 +80,22 @@ +[data-board-type="tower"] .ol-strip-board-strip > * { + text-align: center; +} + +[data-board-type="tower"] .ol-strip-board-strip > :first-child { + text-align: left; +} + [data-board-type="tower"] .ol-strip-board-strip :nth-child(2) input { width:25px; } +[data-board-type="tower"] .ol-strip-board-strip :nth-child(2) { + font-size:10px; +} + @@ -109,7 +121,12 @@ padding:4px 8px; } -.ol-strip-board-add-flight button { +.add-flight-by-click img { + filter:invert(); + height: 12px; +} + +.ol-strip-board-add-flight button[type="submit"] { background-color: darkgreen; border-bottom-right-radius: var( --border-radius-sm ); border-bottom-left-radius: 0; diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js index 82e8865a..f29e88e6 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -25,12 +25,13 @@ function uuidv4() { -function Flight( name, boardId ) { +function Flight( name, boardId, unitId ) { this.id = uuidv4(); this.boardId = boardId; this.name = name; this.status = "unknown"; this.takeoffTime = -1; + this.unitId = parseInt( unitId ); } Flight.prototype.getData = function() { @@ -39,7 +40,8 @@ Flight.prototype.getData = function() { "boardId" : this.boardId, "name" : this.name, "status" : this.status, - "takeoffTime" : this.takeoffTime + "takeoffTime" : this.takeoffTime, + "unitId" : this.unitId }; } @@ -180,7 +182,11 @@ app.post( "/flight", ( req, res ) => { res.status( 400 ).send( "Invalid/missing flight name" ); } - const flight = new Flight( req.body.name, req.body.boardId ); + if ( !req.body.unitId || isNaN( req.body.unitId ) ) { + res.status( 400 ).send( "Invalid/missing unitId" ); + } + + const flight = new Flight( req.body.name, req.body.boardId, req.body.unitId ); dataHandler.addFlight( flight ); diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts index af8a682e..158a2f1a 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -8,6 +8,7 @@ export interface FlightInterface { name : string; status : "unknown"; takeoffTime : number; + unitId : number; } diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 5a54fd92..79c066a4 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -1,12 +1,14 @@ -import { Draggable } from "leaflet"; import { Dropdown } from "../controls/dropdown"; -import { generateUUIDv4, zeroAppend } from "../other/utils"; +import { zeroAppend } from "../other/utils"; import { ATC } from "./atc"; +import { Unit } from "../units/unit"; export interface StripBoardStripInterface { "id": string, "element": HTMLElement, - "dropdowns": {[key:string]: Dropdown} + "dropdowns": {[key:string]: Dropdown}, + "isDeleted"?: boolean, + "unitId": number } export abstract class ATCBoard { @@ -15,13 +17,16 @@ export abstract class ATCBoard { #boardId:string = ""; #templates: {[key:string]: string} = {}; + // Elements #boardElement:HTMLElement; #clockElement:HTMLElement; #stripBoardElement:HTMLElement; // Content + #isAddFlightByClickEnabled:boolean = false; #strips:{[key:string]: StripBoardStripInterface} = {}; + #unitIdsBeingMonitored:number[] = []; // Update timing #updateInterval:number|undefined = undefined; @@ -37,6 +42,12 @@ export abstract class ATCBoard { this.#stripBoardElement = this.getBoardElement().querySelector( ".ol-strip-board-strips" ); this.#clockElement = this.getBoardElement().querySelector( ".ol-strip-board-clock" ); + + setInterval( () => { + this.updateClock(); + }, 1000 ); + + if ( this.#boardElement.classList.contains( "ol-draggable" ) ) { let options:any = {}; @@ -50,15 +61,37 @@ export abstract class ATCBoard { } - setInterval( () => { - this.updateClock(); - }, 1000 ); - + this.#setupAddFlight(); } - addFlight( flightName:string ) { + addFlight( unit:Unit ) { + + const baseData = unit.getBaseData(); + + const unitCanBeAdded = () => { + + if ( baseData.category !== "Aircraft" ) { + return false; + } + + if ( baseData.AI === true ) { + // return false; + } + + if ( this.#unitIdsBeingMonitored.includes( unit.ID ) ) { + return false; + } + + return true; + } + + if ( !unitCanBeAdded() ) { + return; + } + + this.#unitIdsBeingMonitored.push( unit.ID ); return fetch( '/api/atc/flight/', { method: 'POST', @@ -68,7 +101,8 @@ export abstract class ATCBoard { }, "body": JSON.stringify({ "boardId" : this.getBoardId(), - "name" : flightName + "name" : baseData.unitName, + "unitId" : unit.ID }) }); @@ -76,7 +110,16 @@ export abstract class ATCBoard { addStrip( strip:StripBoardStripInterface ) { + this.#strips[ strip.id ] = strip; + + strip.element.querySelectorAll( "button.deleteFlight" ).forEach( btn => { + btn.addEventListener( "click", ev => { + ev.preventDefault(); + this.deleteFlight( strip.id ); + }); + }); + } @@ -109,16 +152,37 @@ export abstract class ATCBoard { } - deleteStrip( id:string ) { + deleteStrip( flightId:string ) { + + if ( this.#strips.hasOwnProperty( flightId ) ) { + + this.#strips[ flightId ].element.remove(); + this.#strips[ flightId ].isDeleted = true; + + setTimeout( () => { + delete this.#strips[ flightId ]; + }, 10000 ); - if ( this.#strips.hasOwnProperty( id ) ) { - this.#strips[ id ].element.remove(); - delete this.#strips[ id ]; } } + deleteFlight( flightId:string ) { + + this.deleteStrip( flightId ); + + fetch( '/api/atc/flight/' + flightId, { + method: 'DELETE', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + } + }); + + } + + getATC() { return this.#atc; } @@ -161,6 +225,67 @@ export abstract class ATCBoard { } + #setupAddFlight() { + + const toggleIsAddFlightByClickEnabled = () => { + this.#isAddFlightByClickEnabled = ( !this.#isAddFlightByClickEnabled ); + this.getBoardElement().classList.toggle( "add-flight-by-click", this.#isAddFlightByClickEnabled ); + } + + + document.addEventListener( "unitSelection", ( ev:CustomEventInit ) => { + + if ( this.#isAddFlightByClickEnabled !== true ) { + return; + } + + this.addFlight( ev.detail ); + + toggleIsAddFlightByClickEnabled(); + + }); + + + 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[1]; + + if ( flightName.value === "" ) { + return; + } + + // this.addFlight( -1, flightName.value ); + + form.reset(); + + } + + }); + + } + + }); + + + this.getBoardElement().querySelectorAll( ".add-flight-by-click" ).forEach( el => { + el.addEventListener( "click", () => { + toggleIsAddFlightByClickEnabled(); + }); + }); + + } + + startUpdates() { this.#updateInterval = setInterval( () => { diff --git a/client/src/atc/board/ground.ts b/client/src/atc/board/ground.ts index 236f01a4..e722e03f 100644 --- a/client/src/atc/board/ground.ts +++ b/client/src/atc/board/ground.ts @@ -9,53 +9,6 @@ export class ATCBoardGround extends ATCBoard { super( atc, element ); - document.addEventListener( "deleteFlightStrip", ( ev:CustomEventInit ) => { - - if ( ev.detail.id ) { - - fetch( '/api/atc/flight/' + ev.detail.id, { - method: 'DELETE', - headers: { - 'Accept': '*/*', - 'Content-Type': 'application/json' - } - }); - - } - - }); - - - this.getBoardElement().querySelectorAll( "form.ol-strip-board-add-flight" ).forEach( form => { - - if ( form instanceof HTMLFormElement ) { - - form.addEventListener( "submit", ev => { - - ev.preventDefault(); - - - if ( ev.target instanceof HTMLFormElement ) { - - const elements = ev.target.elements; - const flightName = elements[0]; - - if ( flightName.value === "" ) { - return; - } - - this.addFlight( flightName.value ); - - form.reset(); - - } - - }); - - } - - }); - } @@ -89,7 +42,7 @@ export class ATCBoardGround extends ATCBoard {
${this.timeToGo( flight.takeoffTime )}
- +
`; stripBoard.insertAdjacentHTML( "beforeend", template ); @@ -98,7 +51,8 @@ export class ATCBoardGround extends ATCBoard { strip = { "id": flight.id, "element": stripBoard.lastElementChild, - "dropdowns": {} + "dropdowns": {}, + "unitId": -1 }; strip.element.querySelectorAll( ".ol-select" ).forEach( select => { @@ -217,6 +171,7 @@ export class ATCBoardGround extends ATCBoard { }); + stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => { this.deleteStrip( strip.getAttribute( "data-flight-id" ) || "" ); }); diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index 91550e75..e56c9ca7 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -115,8 +115,6 @@ export class UnitControlPanel extends Panel { else database = null; // TODO add databases for other unit types - console.log( unit.getBaseData() ); - var button = document.createElement("button"); var callsign = unit.getBaseData().unitName || ""; diff --git a/client/views/atc.ejs b/client/views/atc.ejs index fb007578..3e969e33 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,53 +1,11 @@ -
+<%- include('atc/board.ejs', { + "boardId": "strip-board-tower", + "boardType": "tower", + "headers": [ "Flight", "a-Alt", "alt" ] +}) %> -
- -
-

Tower

-
-
- -
-
-
Flight
-
-
-
- - - -
- - -
- -
- -
-

Ground

-
-
- -
-
-
Flight
-
Status
-
T/O Time
-
TTG
-
-
-
- - - -
\ No newline at end of file +<%- include('atc/board.ejs', { + "boardId": "strip-board-ground", + "boardType": "ground", + "headers": [ "Flight", "Status", "T/O Time", "TTG" ] +}) %> \ No newline at end of file From d77735581ad2c5b41889b3be79d236c78a6111dd Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Sun, 16 Apr 2023 23:34:00 +0100 Subject: [PATCH 3/4] Flights now sortable. --- client/public/stylesheets/atc.css | 67 ++++++--- client/public/stylesheets/olympus.css | 11 +- client/routes/api/atc.js | 30 ++++ client/src/atc/atc.ts | 1 + client/src/atc/atcboard.ts | 192 ++++++++++++++++++++++---- client/src/atc/board/ground.ts | 5 +- client/src/units/unitsmanager.ts | 18 +++ 7 files changed, 276 insertions(+), 48 deletions(-) diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index baab81f5..9258da8c 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -42,7 +42,7 @@ text-align: center; } -.ol-strip-board-headers > *, .ol-strip-board-strip > * { +.ol-strip-board-headers > *, .ol-strip-board-strip > [data-point] { padding: 4px; text-overflow: ellipsis; white-space: nowrap; @@ -67,19 +67,33 @@ border:1px solid #cc0000; } -[data-board-type="ground"] .ol-strip-board-headers :nth-child(1), -[data-board-type="ground"] .ol-strip-board-headers :nth-child(2), -[data-board-type="ground"] .ol-strip-board-strip :nth-child(1), -[data-board-type="ground"] .ol-strip-board-strip :nth-child(2) { - width:120px; +.ol-strip-board-headers :nth-child(1) { + width:12px; } -[data-board-type="ground"] .ol-strip-board-strip :nth-child(4) { +.ol-strip-board-headers :nth-child(2), +.ol-strip-board-strip :nth-child(2), +[data-board-type="ground"] .ol-strip-board-headers :nth-child(3), +[data-board-type="ground"] .ol-strip-board-strip :nth-child(3) { + width:130px; +} + +[data-board-type="ground"] .ol-strip-board-strip :nth-child(5) { text-align: center; } +.ol-strip-board-headers :nth-child(6), +.ol-strip-board-strip :nth-child(6) { + width:20px; +} + + + + + + [data-board-type="tower"] .ol-strip-board-strip > * { text-align: center; } @@ -88,11 +102,11 @@ text-align: left; } -[data-board-type="tower"] .ol-strip-board-strip :nth-child(2) input { +[data-board-type="tower"] .ol-strip-board-strip :nth-child(3) input { width:25px; } -[data-board-type="tower"] .ol-strip-board-strip :nth-child(2) { +[data-board-type="tower"] .ol-strip-board-strip :nth-child(3) { font-size:10px; } @@ -112,6 +126,7 @@ .ol-strip-board-add-flight { display:flex; flex-flow: row nowrap; + position:relative; } @@ -126,24 +141,36 @@ height: 12px; } -.ol-strip-board-add-flight button[type="submit"] { - 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-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 ); +.ol-strip-board-add-flight .ol-auto-suggest { + background:white; + border-radius: var(--border-radius-sm ); + color:black; + display:none; + flex-direction: column; + left:0; + margin:0; + position:absolute; + translate:0 -100%; + top:0; +} + +.ol-strip-board-add-flight .ol-auto-suggest[data-has-suggestions] { + display:flex; +} + +.ol-strip-board-add-flight .ol-auto-suggest[data-has-suggestions] a { + cursor: pointer; } [data-board-type="ground"] { - translate: 0 100%; + bottom:20px; } [data-board-type="tower"] { - translate: 0 -100%; + top:100px; } \ No newline at end of file diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index 2864d9cc..cc006d2e 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -149,7 +149,7 @@ form > div { .ol-select.narrow:not(.ol-select-image)>.ol-select-value { opacity: .9; - padding:6px 30px 6px 15px; + padding:4px 30px 4px 15px; } .ol-select:not(.ol-select-image)>.ol-select-value svg { @@ -534,6 +534,15 @@ nav.ol-panel> :last-child { } +.ol-sortable .handle { + background-image: url( "/images/icons/grip-lines-solid.svg" ); + cursor:ns-resize; + filter:invert(); + height:12px; + width:12px; +} + + #unit-selection { display: flex; diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js index f29e88e6..a89df3b0 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -46,6 +46,15 @@ Flight.prototype.getData = function() { } +Flight.prototype.setOrder = function( order ) { + + this.order = order; + + return true; + +} + + Flight.prototype.setStatus = function( status ) { if ( [ "unknown", "checkedin", "readytotaxi", "clearedtotaxi", "halted", "terminated" ].indexOf( status ) < 0 ) { @@ -172,6 +181,27 @@ app.patch( "/flight/:flightId", ( req, res ) => { }); +app.post( "/flight/order", ( req, res ) => { + + if ( !req.body.boardId ) { + res.status( 400 ).send( "Invalid/missing boardId" ); + } + + if ( !req.body.order || !Array.isArray( req.body.order ) ) { + res.status( 400 ).send( "Invalid/missing boardId" ); + } + + req.body.order.forEach( ( flightId, i ) => { + + dataHandler.getFlight( flightId ).setOrder( i ); + + }); + + res.send( "" ); + +}); + + app.post( "/flight", ( req, res ) => { if ( !req.body.boardId ) { diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts index 158a2f1a..4855eaaf 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -6,6 +6,7 @@ export interface FlightInterface { id : string; boardId : string; name : string; + order : number; status : "unknown"; takeoffTime : number; unitId : number; diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 79c066a4..52877996 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -2,6 +2,9 @@ import { Dropdown } from "../controls/dropdown"; import { zeroAppend } from "../other/utils"; import { ATC } from "./atc"; import { Unit } from "../units/unit"; +import { getUnitsManager } from ".."; +import Sortable from "sortablejs"; +import { FlightInterface } from "./atc"; export interface StripBoardStripInterface { "id": string, @@ -43,6 +46,30 @@ export abstract class ATCBoard { this.#clockElement = this.getBoardElement().querySelector( ".ol-strip-board-clock" ); + new Sortable( this.getStripBoardElement(), { + "handle": ".handle", + "onUpdate": ev => { + + const order = [].slice.call( this.getStripBoardElement().children ).map( ( strip:HTMLElement ) => { + return strip.dataset.flightId + }); + + fetch( '/api/atc/flight/order', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "order" : order + }) + }); + + } + }); + + setInterval( () => { this.updateClock(); }, 1000 ); @@ -63,6 +90,8 @@ export abstract class ATCBoard { this.#setupAddFlight(); + //this.#_setupDemoData(); + } @@ -177,7 +206,10 @@ export abstract class ATCBoard { headers: { 'Accept': '*/*', 'Content-Type': 'application/json' - } + }, + "body": JSON.stringify({ + "boardId": this.getBoardId() + }) }); } @@ -220,6 +252,13 @@ export abstract class ATCBoard { } + getUnitIdsBeingMonitored() { + + return this.#unitIdsBeingMonitored; + + } + + setTemplates( templates:{[key:string]: string} ) { this.#templates = templates; } @@ -246,39 +285,82 @@ export abstract class ATCBoard { }); - this.getBoardElement().querySelectorAll( "form.ol-strip-board-add-flight" ).forEach( form => { + const form = this.getBoardElement().querySelector( "form.ol-strip-board-add-flight" ); + const suggestions = form.querySelector( ".ol-auto-suggest" ); + const unitName = form.querySelector( "input[name='unitName']" ); - if ( form instanceof HTMLFormElement ) { + const toggleSuggestions = ( bool:boolean ) => { + suggestions.toggleAttribute( "data-has-suggestions", bool ); + } - form.addEventListener( "submit", ev => { + let searchTimeout:number|null; + + unitName.addEventListener( "keyup", ev => { + + if ( searchTimeout ) { + clearTimeout( searchTimeout ); + } + + const resetSuggestions = () => { + suggestions.innerHTML = ""; + toggleSuggestions( false ); + } + + resetSuggestions(); + + searchTimeout = setTimeout( () => { + + const searchString = unitName.value.toLowerCase(); + + if ( searchString === "" ) { + return; + } + + const units = getUnitsManager().getSelectableAircraft(); + const unitIdsBeingMonitored = this.getUnitIdsBeingMonitored(); + + const results = Object.keys( units ).reduce( ( acc:Unit[], unitId:any ) => { - ev.preventDefault(); - - - if ( ev.target instanceof HTMLFormElement ) { - - const elements = ev.target.elements; - const flightName = elements[1]; - - if ( flightName.value === "" ) { - return; - } - - // this.addFlight( -1, flightName.value ); - - form.reset(); - + const unit = units[ unitId ]; + const baseData = unit.getBaseData(); + + if ( !unitIdsBeingMonitored.includes( parseInt( unitId ) ) && baseData.unitName.toLowerCase().indexOf( searchString ) > -1 ) { + acc.push( unit ); } - + + return acc; + + }, [] ); + + toggleSuggestions( results.length > 0 ); + + results.forEach( unit => { + + const baseData = unit.getBaseData(); + + const a = document.createElement( "a" ); + a.innerText = baseData.unitName; + + a.addEventListener( "click", ev => { + this.addFlight( unit ); + resetSuggestions(); + unitName.value = ""; + }); + + suggestions.appendChild( a ); + }); - } + + + }, 1000 ); + }); - - this.getBoardElement().querySelectorAll( ".add-flight-by-click" ).forEach( el => { - el.addEventListener( "click", () => { + form.querySelectorAll( ".add-flight-by-click" ).forEach( el => { + el.addEventListener( "click", ev => { + ev.preventDefault(); toggleIsAddFlightByClickEnabled(); }); }); @@ -286,6 +368,22 @@ export abstract class ATCBoard { } + sortFlights( flights:FlightInterface[] ) { + + flights.sort( ( a, b ) => { + + const aVal = a.order; + const bVal = b.order; + + return ( aVal > bVal ) ? 1 : -1; + + }); + + return flights; + + } + + startUpdates() { this.#updateInterval = setInterval( () => { @@ -332,5 +430,49 @@ export abstract class ATCBoard { } + + #_setupDemoData() { + + fetch( '/api/atc/flight/', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : this.getBoardId() + " 1", + "unitId" : 1 + }) + }); + + fetch( '/api/atc/flight/', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : this.getBoardId() + " 2", + "unitId" : 1 + }) + }); + + fetch( '/api/atc/flight/', { + method: 'POST', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : this.getBoardId() + " 3", + "unitId" : 1 + }) + }); + + } + } \ No newline at end of file diff --git a/client/src/atc/board/ground.ts b/client/src/atc/board/ground.ts index e722e03f..2baf4862 100644 --- a/client/src/atc/board/ground.ts +++ b/client/src/atc/board/ground.ts @@ -14,7 +14,7 @@ export class ATCBoardGround extends ATCBoard { update() { - const flights = Object.values( this.getATC().getDataHandler().getFlights( this.getBoardId() ) ); + const flights = this.sortFlights( Object.values( this.getATC().getDataHandler().getFlights( this.getBoardId() ) ) ); const stripBoard = this.getStripBoardElement(); const missionTime = this.getATC().getMissionDateTime().getTime(); @@ -31,6 +31,7 @@ export class ATCBoardGround extends ATCBoard { if ( !strip ) { const template = `
+
${flight.name}
@@ -42,7 +43,7 @@ export class ATCBoardGround extends ATCBoard {
${this.timeToGo( flight.takeoffTime )}
- +
`; stripBoard.insertAdjacentHTML( "beforeend", template ); diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index b8badd5e..a3319a85 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -23,6 +23,24 @@ export class UnitsManager { document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete() ) } + getSelectableAircraft() { + + const units = this.getUnits(); + + return Object.keys( units ).reduce( ( acc:{[key:number]: Unit}, unitId:any ) => { + + const baseData = units[ unitId ].getBaseData(); + + if ( baseData.category === "Aircraft" && baseData.alive === true ) { + acc[ unitId ] = units[ unitId ]; + } + + return acc; + + }, {}); + + } + getUnits() { return this.#units; } From 3124a0f6b551fd5bb47fafa5254994fca5c0c1f1 Mon Sep 17 00:00:00 2001 From: PeekabooSteam Date: Thu, 20 Apr 2023 14:22:16 +0100 Subject: [PATCH 4/4] Added assigned speeds, alts, select unit. --- client/public/stylesheets/atc.css | 31 ++++++++--- client/routes/api/atc.js | 74 +++++++++++++++++++++----- client/src/atc/atc.ts | 55 +++++++++++-------- client/src/atc/atcboard.ts | 88 ++++++++++++++++++++++--------- client/views/atc.ejs | 2 +- 5 files changed, 184 insertions(+), 66 deletions(-) diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index 9258da8c..c19e19fd 100644 --- a/client/public/stylesheets/atc.css +++ b/client/public/stylesheets/atc.css @@ -84,8 +84,8 @@ -.ol-strip-board-headers :nth-child(6), -.ol-strip-board-strip :nth-child(6) { +.ol-strip-board-headers :last-child, +.ol-strip-board-strip :last-child { width:20px; } @@ -98,12 +98,17 @@ text-align: center; } -[data-board-type="tower"] .ol-strip-board-strip > :first-child { +[data-board-type="tower"] .ol-strip-board-strip a { + color:white; +} + +[data-board-type="tower"] .ol-strip-board-strip > :nth-child(2) { text-align: left; } -[data-board-type="tower"] .ol-strip-board-strip :nth-child(3) input { - width:25px; +[data-board-type="tower"] .ol-strip-board-strip :nth-child(3) input, +[data-board-type="tower"] .ol-strip-board-strip :nth-child(5) input { + width:30px; } [data-board-type="tower"] .ol-strip-board-strip :nth-child(3) { @@ -111,6 +116,19 @@ } +[data-altitude-assigned] [data-point="assignedAltitude"] input, +[data-speed-assigned] [data-point="assignedSpeed"] input { + background-color:#ffffffbb; + color: black; + font-weight: var( --font-weight-bolder ); +} + +[data-warning-altitude] [data-point="altitude"], +[data-warning-speed] [data-point="speed"] { + background:#cc0000; + border-radius: var( --border-radius-sm ); +} + .ol-strip-board-strip > [data-point="name"] { @@ -172,5 +190,6 @@ } [data-board-type="tower"] { - top:100px; + right:10px; + top:10px; } \ No newline at end of file diff --git a/client/routes/api/atc.js b/client/routes/api/atc.js index a89df3b0..2429cdce 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -26,26 +26,56 @@ function uuidv4() { function Flight( name, boardId, unitId ) { - this.id = uuidv4(); - this.boardId = boardId; - this.name = name; - this.status = "unknown"; - this.takeoffTime = -1; - this.unitId = parseInt( unitId ); + this.assignedAltitude = 0; + this.assignedSpeed = 0; + this.id = uuidv4(); + this.boardId = boardId; + this.name = name; + this.status = "unknown"; + this.takeoffTime = -1; + this.unitId = parseInt( unitId ); } Flight.prototype.getData = function() { return { - "id" : this.id, - "boardId" : this.boardId, - "name" : this.name, - "status" : this.status, - "takeoffTime" : this.takeoffTime, - "unitId" : this.unitId + "assignedAltitude" : this.assignedAltitude, + "assignedSpeed" : this.assignedSpeed, + "id" : this.id, + "boardId" : this.boardId, + "name" : this.name, + "status" : this.status, + "takeoffTime" : this.takeoffTime, + "unitId" : this.unitId }; } +Flight.prototype.setAssignedAltitude = function( assignedAltitude ) { + + if ( isNaN( assignedAltitude ) ) { + return "Altitude must be a number" + } + + this.assignedAltitude = parseInt( assignedAltitude ); + + return true; + +} + + +Flight.prototype.setAssignedSpeed = function( assignedSpeed ) { + + if ( isNaN( assignedSpeed ) ) { + return "Speed must be a number" + } + + this.assignedSpeed = parseInt( assignedSpeed ); + + return true; + +} + + Flight.prototype.setOrder = function( order ) { this.order = order; @@ -156,6 +186,26 @@ app.patch( "/flight/:flightId", ( req, res ) => { res.status( 400 ).send( `Unrecognised flight ID (given: "${req.params.flightId}")` ); } + if ( req.body.hasOwnProperty( "assignedAltitude" ) ) { + + const altitudeChangeSuccess = flight.setAssignedAltitude( req.body.assignedAltitude ); + + if ( altitudeChangeSuccess !== true ) { + res.status( 400 ).send( altitudeChangeSuccess ); + } + + } + + if ( req.body.hasOwnProperty( "assignedSpeed" ) ) { + + const speedChangeSuccess = flight.setAssignedSpeed( req.body.assignedSpeed ); + + if ( speedChangeSuccess !== true ) { + res.status( 400 ).send( speedChangeSuccess ); + } + + } + if ( req.body.status ) { const statusChangeSuccess = flight.setStatus( req.body.status ); diff --git a/client/src/atc/atc.ts b/client/src/atc/atc.ts index 4855eaaf..856380ec 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -3,13 +3,15 @@ import { ATCBoardGround } from "./board/ground"; import { ATCBoardTower } from "./board/tower"; export interface FlightInterface { - id : string; - boardId : string; - name : string; - order : number; - status : "unknown"; - takeoffTime : number; - unitId : number; + assignedSpeed: any; + assignedAltitude : any; + id : string; + boardId : string; + name : string; + order : number; + status : "unknown"; + takeoffTime : number; + unitId : number; } @@ -19,7 +21,7 @@ class ATCDataHandler { #flights:{[key:string]: FlightInterface} = {}; #updateInterval:number|undefined = undefined; - #updateIntervalDelay:number = 1000; + #updateIntervalDelay:number = 2500; // Wait between unit update requests constructor( atc:ATC ) { @@ -43,23 +45,29 @@ class ATCDataHandler { startUpdates() { - + this.#updateInterval = setInterval( () => { - fetch( '/api/atc/flight', { - method: 'GET', - headers: { - 'Accept': '*/*', - 'Content-Type': 'application/json' - } - }) - .then( response => response.json() ) - .then( data => { - this.setFlights( data ); - }); + const aBoardIsVisible = this.#atc.getBoards().some( board => board.boardIsVisible() ); + + if ( aBoardIsVisible ) { + + fetch( '/api/atc/flight', { + method: 'GET', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + } + }) + .then( response => response.json() ) + .then( data => { + this.setFlights( data ); + }); + + } }, this.#updateIntervalDelay ); - + } @@ -106,6 +114,11 @@ export class ATC { } + getBoards() { + return this.#boards; + } + + getDataHandler() { return this.#dataHandler; } diff --git a/client/src/atc/atcboard.ts b/client/src/atc/atcboard.ts index 52877996..205cd6a1 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -45,6 +45,19 @@ export abstract class ATCBoard { this.#stripBoardElement = this.getBoardElement().querySelector( ".ol-strip-board-strips" ); this.#clockElement = this.getBoardElement().querySelector( ".ol-strip-board-clock" ); + + new MutationObserver( () => { + if ( this.boardIsVisible() ) { + this.startUpdates(); + } else { + this.stopUpdates(); + } + }).observe( this.getBoardElement(), { + "attributes": true, + "childList": false, + "subtree": false + }); + new Sortable( this.getStripBoardElement(), { "handle": ".handle", @@ -90,7 +103,7 @@ export abstract class ATCBoard { this.#setupAddFlight(); - //this.#_setupDemoData(); + // this.#_setupDemoData(); } @@ -152,6 +165,11 @@ export abstract class ATCBoard { } + boardIsVisible() { + return ( !this.getBoardElement().classList.contains( "hide" ) ); + } + + calculateTimeToGo( fromTimestamp:number, toTimestamp:number ) { let timestamp = ( toTimestamp - fromTimestamp ) / 1000; @@ -386,6 +404,10 @@ export abstract class ATCBoard { startUpdates() { + if ( !this.boardIsVisible() ) { + return; + } + this.#updateInterval = setInterval( () => { this.update(); @@ -417,7 +439,6 @@ export abstract class ATCBoard { } - protected update() { console.warn( "No custom update method defined." ); } @@ -431,6 +452,20 @@ export abstract class ATCBoard { } + updateFlight( flightId:string, reqBody:object ) { + + return fetch( '/api/atc/flight/' + flightId, { + method: 'PATCH', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify( reqBody ) + }); + + } + + #_setupDemoData() { fetch( '/api/atc/flight/', { @@ -446,31 +481,32 @@ export abstract class ATCBoard { }) }); - fetch( '/api/atc/flight/', { - method: 'POST', - headers: { - 'Accept': '*/*', - 'Content-Type': 'application/json' - }, - "body": JSON.stringify({ - "boardId" : this.getBoardId(), - "name" : this.getBoardId() + " 2", - "unitId" : 1 - }) - }); + + // fetch( '/api/atc/flight/', { + // method: 'POST', + // headers: { + // 'Accept': '*/*', + // 'Content-Type': 'application/json' + // }, + // "body": JSON.stringify({ + // "boardId" : this.getBoardId(), + // "name" : this.getBoardId() + " 2", + // "unitId" : 2 + // }) + // }); - fetch( '/api/atc/flight/', { - method: 'POST', - headers: { - 'Accept': '*/*', - 'Content-Type': 'application/json' - }, - "body": JSON.stringify({ - "boardId" : this.getBoardId(), - "name" : this.getBoardId() + " 3", - "unitId" : 1 - }) - }); + // fetch( '/api/atc/flight/', { + // method: 'POST', + // headers: { + // 'Accept': '*/*', + // 'Content-Type': 'application/json' + // }, + // "body": JSON.stringify({ + // "boardId" : this.getBoardId(), + // "name" : this.getBoardId() + " 3", + // "unitId" : 9 + // }) + // }); } diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 3e969e33..19046c70 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,7 +1,7 @@ <%- include('atc/board.ejs', { "boardId": "strip-board-tower", "boardType": "tower", - "headers": [ "Flight", "a-Alt", "alt" ] + "headers": [ "Flight", "a. Alt", "alt", "a. Speed", "Speed" ] }) %> <%- include('atc/board.ejs', {