ATC Ground board PoC.

This commit is contained in:
PeekabooSteam 2023-03-29 21:15:33 +01:00
parent 14147168f9
commit 6f64bd1622
10 changed files with 502 additions and 43 deletions

View File

@ -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 );
}

View File

@ -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;

View File

@ -1,5 +1,6 @@
@import url("layout.css");
@import url("airbase.css");
@import url("atc.css");
@import url("connectionstatuspanel.css");
@import url("contextmenus.css");
@import url("mouseinfopanel.css");
@ -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;
}

View File

@ -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 ) {

View File

@ -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 ) );

View File

@ -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 = <HTMLElement>this.getBoardElement().querySelector( ".atc-strip-board" );
this.#stripBoardElement = <HTMLElement>this.getBoardElement().querySelector( ".ol-strip-board-strips" );
this.#clockElement = <HTMLElement>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();
}
}

View File

@ -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 = <HTMLInputElement>elements[0];
if ( flightName.value === "" ) {
return;
}
fetch( '/api/atc/flight/', {
method: 'POST',
headers: {
'Accept': '*/*',
'Content-Type': 'application/json'
},
"body": JSON.stringify({
"name": flightName.value
})
});
form.reset();
}
});
}
});
}
@ -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 = `<div data-flight-id="${flight.id}">
const template = `<div class="ol-strip-board-strip" data-flight-id="${flight.id}" data-flight-status="${flight.status}">
<div data-point="name">${flight.name}</div>
<div data-point="status">${flight.status}</div>
<div id="flight-status-${flight.id}" class="ol-select narrow" data-point="status">
<div class="ol-select-value">${flight.status}</div>
<div class="ol-select-options"></div>
</div>
<div data-point="takeoffTime"><input type="text" name="takeoffTime" value="${this.timestampToLocaleTime( flight.takeoffTime )}" /></div>
<div data-point="timeToGo">${this.timeToGo( flight.takeoffTime )}</div>
<button data-on-click="deleteFlightStrip" data-on-click-params='{"id":"${flight.id}"}'>Delete</button>
</div>`;
@ -56,6 +106,115 @@ export class ATCBoardFlight extends ATCBoard {
strip = <HTMLElement>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
})
});
}
}

View File

@ -13,8 +13,29 @@ export class Dropdown {
this.#value = <HTMLElement>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;
}));

View File

@ -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() {

View File

@ -1,5 +1,26 @@
<div class="ol-panel ol-dialog ol-atc-board" data-feature-switch="atc">
<div class="ol-panel ol-dialog ol-strip-board" data-feature-switch="atc">
<div class="ol-dialog-close" data-on-click="closeDialog"></div>
<div class="atc-strip-board"></div>
<div class="ol-dialog-header">
<div class="ol-strip-board-clock"></div>
</div>
<div class="ol-dialog-content">
<div class="ol-strip-board-headers">
<div>Flight</div>
<div>Status</div>
<div>T/O Time</div>
<div>TTG</div>
</div>
<div class="ol-strip-board-strips"></div>
</div>
<div class="ol-dialog-footer">
<form class="ol-strip-board-add-flight">
<input type="text" name="flightName" placeholder="Add a flight" />
<button type="submit">Add</button>
</form>
</div>
</div>