diff --git a/client/public/stylesheets/atc.css b/client/public/stylesheets/atc.css index 7eb22afb..c19e19fd 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,17 +67,70 @@ 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-headers :nth-child(1) { + width:12px; } -.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 :last-child, +.ol-strip-board-strip :last-child { + width:20px; +} + + + + + + +[data-board-type="tower"] .ol-strip-board-strip > * { + text-align: center; +} + +[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, +[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) { + font-size:10px; +} + + +[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"] { text-overflow: ellipsis; overflow:hidden; @@ -91,6 +144,7 @@ .ol-strip-board-add-flight { display:flex; flex-flow: row nowrap; + position:relative; } @@ -100,15 +154,42 @@ 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 ); +.add-flight-by-click img { + filter:invert(); + height: 12px; } .ol-strip-board-add-flight input { - border-bottom-left-radius: var( --border-radius-sm ); - border-top-left-radius: var( --border-radius-sm ); + border-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"] { + bottom:20px; +} + +[data-board-type="tower"] { + right:10px; + top:10px; } \ No newline at end of file diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index cd07653c..873e7a37 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -153,7 +153,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 { @@ -537,6 +537,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 517533cb..2429cdce 100644 --- a/client/routes/api/atc.js +++ b/client/routes/api/atc.js @@ -25,23 +25,66 @@ function uuidv4() { -function Flight( name ) { - this.id = uuidv4(); - this.name = name; - this.status = "unknown"; - this.takeoffTime = -1; +function Flight( name, boardId, 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, - "name" : this.name, - "status" : this.status, - "takeoffTime" : this.takeoffTime + "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; + + return true; + +} + + Flight.prototype.setStatus = function( status ) { if ( [ "unknown", "checkedin", "readytotaxi", "clearedtotaxi", "halted", "terminated" ].indexOf( status ) < 0 ) { @@ -116,7 +159,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 ); }); @@ -130,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 ); @@ -155,13 +231,42 @@ 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 ) { + 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 ); + 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 d63634bf..856380ec 100644 --- a/client/src/atc/atc.ts +++ b/client/src/atc/atc.ts @@ -1,11 +1,17 @@ import { ATCBoard } from "./atcboard"; -import { ATCBoardFlight } from "./board/flight"; +import { ATCBoardGround } from "./board/ground"; +import { ATCBoardTower } from "./board/tower"; export interface FlightInterface { - id : string; - name : string; - status : "unknown"; - takeoffTime : number; + assignedSpeed: any; + assignedAltitude : any; + id : string; + boardId : string; + name : string; + order : number; + status : "unknown"; + takeoffTime : number; + unitId : number; } @@ -15,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 ) { @@ -25,29 +31,43 @@ 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; + }, {} ); } 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 ); - + } @@ -94,6 +114,11 @@ export class ATC { } + getBoards() { + return this.#boards; + } + + getDataHandler() { return this.#dataHandler; } @@ -119,7 +144,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..205cd6a1 100644 --- a/client/src/atc/atcboard.ts +++ b/client/src/atc/atcboard.ts @@ -1,39 +1,93 @@ -import { Draggable } from "leaflet"; 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, "element": HTMLElement, - "dropdowns": {[key:string]: Dropdown} + "dropdowns": {[key:string]: Dropdown}, + "isDeleted"?: boolean, + "unitId": number } export abstract class ATCBoard { #atc:ATC; + #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; #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; 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", + "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 ); + + if ( this.#boardElement.classList.contains( "ol-draggable" ) ) { let options:any = {}; @@ -47,15 +101,72 @@ export abstract class ATCBoard { } - setInterval( () => { - this.updateClock(); - }, 1000 ); + this.#setupAddFlight(); + + // this.#_setupDemoData(); + + } + + + 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', + headers: { + 'Accept': '*/*', + 'Content-Type': 'application/json' + }, + "body": JSON.stringify({ + "boardId" : this.getBoardId(), + "name" : baseData.unitName, + "unitId" : unit.ID + }) + }); } 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 ); + }); + }); + + } + + + boardIsVisible() { + return ( !this.getBoardElement().classList.contains( "hide" ) ); } @@ -88,16 +199,40 @@ 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' + }, + "body": JSON.stringify({ + "boardId": this.getBoardId() + }) + }); + + } + + getATC() { return this.#atc; } @@ -108,6 +243,11 @@ export abstract class ATCBoard { } + getBoardId(): string { + return this.getBoardElement().id; + } + + getStripBoardElement() { return this.#stripBoardElement; } @@ -130,8 +270,10 @@ export abstract class ATCBoard { } - protected update() { - console.warn( "No custom update method defined." ); + getUnitIdsBeingMonitored() { + + return this.#unitIdsBeingMonitored; + } @@ -140,8 +282,132 @@ 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(); + + }); + + + 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']" ); + + const toggleSuggestions = ( bool:boolean ) => { + suggestions.toggleAttribute( "data-has-suggestions", bool ); + } + + 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 ) => { + + 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 ); + + + }); + + form.querySelectorAll( ".add-flight-by-click" ).forEach( el => { + el.addEventListener( "click", ev => { + ev.preventDefault(); + toggleIsAddFlightByClickEnabled(); + }); + }); + + } + + + sortFlights( flights:FlightInterface[] ) { + + flights.sort( ( a, b ) => { + + const aVal = a.order; + const bVal = b.order; + + return ( aVal > bVal ) ? 1 : -1; + + }); + + return flights; + + } + + startUpdates() { + if ( !this.boardIsVisible() ) { + return; + } + this.#updateInterval = setInterval( () => { this.update(); @@ -173,6 +439,10 @@ export abstract class ATCBoard { } + protected update() { + console.warn( "No custom update method defined." ); + } + updateClock() { @@ -181,5 +451,64 @@ 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/', { + 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" : 2 + // }) + // }); + + // fetch( '/api/atc/flight/', { + // method: 'POST', + // headers: { + // 'Accept': '*/*', + // 'Content-Type': 'application/json' + // }, + // "body": JSON.stringify({ + // "boardId" : this.getBoardId(), + // "name" : this.getBoardId() + " 3", + // "unitId" : 9 + // }) + // }); + + } + } \ No newline at end of file diff --git a/client/src/atc/board/flight.ts b/client/src/atc/board/ground.ts similarity index 75% rename from client/src/atc/board/flight.ts rename to client/src/atc/board/ground.ts index b9830fc8..2baf4862 100644 --- a/client/src/atc/board/flight.ts +++ b/client/src/atc/board/ground.ts @@ -1,77 +1,20 @@ -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 ) { 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; - } - - fetch( '/api/atc/flight/', { - method: 'POST', - headers: { - 'Accept': '*/*', - 'Content-Type': 'application/json' - }, - "body": JSON.stringify({ - "name": flightName.value - }) - }); - - form.reset(); - - } - - }); - - } - - }); - } update() { - const flights = Object.values( this.getATC().getDataHandler().getFlights() ); + const flights = this.sortFlights( Object.values( this.getATC().getDataHandler().getFlights( this.getBoardId() ) ) ); const stripBoard = this.getStripBoardElement(); const missionTime = this.getATC().getMissionDateTime().getTime(); @@ -88,6 +31,7 @@ export class ATCBoardFlight extends ATCBoard { if ( !strip ) { const template = `
+
${flight.name}
@@ -99,7 +43,7 @@ export class ATCBoardFlight extends ATCBoard {
${this.timeToGo( flight.takeoffTime )}
- +
`; stripBoard.insertAdjacentHTML( "beforeend", template ); @@ -108,7 +52,8 @@ export class ATCBoardFlight extends ATCBoard { strip = { "id": flight.id, "element": stripBoard.lastElementChild, - "dropdowns": {} + "dropdowns": {}, + "unitId": -1 }; strip.element.querySelectorAll( ".ol-select" ).forEach( select => { @@ -227,6 +172,7 @@ export class ATCBoardFlight extends ATCBoard { }); + stripBoard.querySelectorAll( `[data-updating]` ).forEach( strip => { this.deleteStrip( strip.getAttribute( "data-flight-id" ) || "" ); }); 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/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index becb74c5..8a8778e4 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; } diff --git a/client/views/atc.ejs b/client/views/atc.ejs index 062f5738..19046c70 100644 --- a/client/views/atc.ejs +++ b/client/views/atc.ejs @@ -1,26 +1,11 @@ -
+<%- include('atc/board.ejs', { + "boardId": "strip-board-tower", + "boardType": "tower", + "headers": [ "Flight", "a. Alt", "alt", "a. Speed", "Speed" ] +}) %> -
- -
-
-
- -
-
-
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 diff --git a/client/views/navbar.ejs b/client/views/navbar.ejs index 4030d38f..aad54404 100644 --- a/client/views/navbar.ejs +++ b/client/views/navbar.ejs @@ -56,7 +56,7 @@
ATC
- +
\ No newline at end of file