Pax1601 main (#52)
* GA initial data * First commit of crude functionality. * More AIC work so I don't lose it. (Best commit message ever.) * Restructured to use 'phrases'. * Set to a working state. * Committing so I don't lose work. * Added ai-formation feature swtich and UI kit stuff. * Added plane units to UI kit.
2
client/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
39
client/package-lock.json
generated
@ -20,9 +20,12 @@
|
||||
"save": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gtag.js": "^0.0.12",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"browserify": "^17.0.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tsify": "^5.0.4",
|
||||
"typescript": "^4.9.4",
|
||||
"watchify": "^4.0.0"
|
||||
@ -33,6 +36,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
||||
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
|
||||
},
|
||||
"node_modules/@types/gtag.js": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz",
|
||||
"integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz",
|
||||
@ -41,6 +50,12 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-qrhtM7M41EhH4tZQTNw2/RJkxllBx3reiJpTbgWCM2Dx0U1sZ6LwKp9lfNln9uqE26ZMKUaPEYaD4rzvOWYtZw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@ -2614,6 +2629,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
@ -3227,6 +3248,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz",
|
||||
"integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA=="
|
||||
},
|
||||
"@types/gtag.js": {
|
||||
"version": "0.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz",
|
||||
"integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/leaflet": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.0.tgz",
|
||||
@ -3235,6 +3262,12 @@
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"@types/sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-qrhtM7M41EhH4tZQTNw2/RJkxllBx3reiJpTbgWCM2Dx0U1sZ6LwKp9lfNln9uqE26ZMKUaPEYaD4rzvOWYtZw==",
|
||||
"dev": true
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@ -5316,6 +5349,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sortablejs": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.0.tgz",
|
||||
"integrity": "sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"",
|
||||
"copy": "copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet.css",
|
||||
"start": "npm run copy & concurrently --kill-others \"npm run watch\" \"nodemon ./bin/www\"",
|
||||
"watch": "watchify .\\src\\index.ts --debug -p [ tsify --noImplicitAny ] -o .\\public\\javascripts\\bundle.js"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -22,9 +22,12 @@
|
||||
"save": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gtag.js": "^0.0.12",
|
||||
"@types/sortablejs": "^1.15.0",
|
||||
"browserify": "^17.0.0",
|
||||
"concurrently": "^7.6.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"sortablejs": "^1.15.0",
|
||||
"tsify": "^5.0.4",
|
||||
"typescript": "^4.9.4",
|
||||
"watchify": "^4.0.0"
|
||||
|
||||
27
client/public/images/buttons/atc.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="64.000000pt" height="64.000000pt" viewBox="0 0 64.000000 64.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,64.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M285 590 c-11 -12 -31 -20 -51 -20 -23 0 -37 -6 -44 -20 -6 -11 -17
|
||||
-20 -25 -20 -7 0 -16 -4 -20 -10 -10 -16 14 -174 28 -185 7 -5 16 -18 20 -27
|
||||
3 -10 16 -18 27 -18 13 0 20 -7 20 -20 0 -11 5 -20 10 -20 6 0 10 -43 10 -110
|
||||
l0 -110 60 0 60 0 0 110 c0 67 4 110 10 110 6 0 10 9 10 20 0 13 7 20 20 20
|
||||
11 0 24 8 27 18 4 9 13 22 20 27 14 11 38 169 28 185 -4 6 -13 10 -20 10 -8 0
|
||||
-19 9 -25 20 -7 14 -21 20 -44 20 -20 0 -40 8 -51 20 -10 11 -26 20 -35 20 -9
|
||||
0 -25 -9 -35 -20z m60 -10 c4 -6 -7 -10 -25 -10 -18 0 -29 4 -25 10 3 6 15 10
|
||||
25 10 10 0 22 -4 25 -10z m90 -40 c4 -6 -37 -10 -115 -10 -78 0 -119 4 -115
|
||||
10 4 6 53 10 115 10 62 0 111 -4 115 -10z m-205 -110 c0 -78 -1 -80 -24 -80
|
||||
-22 0 -25 5 -31 48 -3 26 -8 62 -11 80 -5 31 -4 32 31 32 l35 0 0 -80z m80 0
|
||||
l0 -80 -30 0 -30 0 0 80 0 80 30 0 30 0 0 -80z m80 0 l0 -80 -30 0 -30 0 0 80
|
||||
0 80 30 0 30 0 0 -80z m86 48 c-3 -18 -8 -54 -11 -80 -6 -43 -9 -48 -31 -48
|
||||
-23 0 -24 2 -24 80 l0 80 35 0 c35 0 36 -1 31 -32z m-46 -158 c0 -6 -43 -10
|
||||
-110 -10 -67 0 -110 4 -110 10 0 6 43 10 110 10 67 0 110 -4 110 -10z m-50
|
||||
-40 c0 -6 -27 -10 -60 -10 -33 0 -60 4 -60 10 0 6 27 10 60 10 33 0 60 -4 60
|
||||
-10z m-20 -130 l0 -100 -40 0 -40 0 0 100 0 100 40 0 40 0 0 -100z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
22
client/public/images/buttons/question-mark-circle.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M386 870 c-63 -16 -153 -70 -197 -117 -22 -24 -55 -74 -72 -111 -29
|
||||
-61 -32 -76 -32 -163 0 -90 2 -99 37 -171 45 -91 103 -147 196 -191 61 -29 76
|
||||
-32 162 -32 86 0 101 3 162 32 93 44 151 100 196 191 35 72 37 81 37 172 0 91
|
||||
-2 100 -37 172 -68 136 -188 217 -336 224 -42 2 -94 -1 -116 -6z m229 -101
|
||||
c63 -30 124 -90 155 -154 34 -72 34 -198 0 -270 -31 -64 -91 -124 -155 -155
|
||||
-72 -34 -198 -34 -270 0 -260 126 -238 495 35 594 63 23 170 16 235 -15z"/>
|
||||
<path d="M415 706 c-37 -16 -70 -52 -84 -89 -19 -49 -15 -57 29 -57 29 0 40 4
|
||||
40 15 0 27 47 65 80 65 41 0 80 -39 80 -79 0 -23 -13 -44 -51 -83 -46 -48 -69
|
||||
-84 -69 -109 0 -5 18 -9 40 -9 22 0 40 4 40 10 0 5 24 37 54 70 30 34 58 75
|
||||
61 91 27 123 -104 226 -220 175z"/>
|
||||
<path d="M440 280 l0 -40 40 0 40 0 0 40 0 40 -40 0 -40 0 0 -40z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
18
client/public/images/buttons/question-mark.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M400 868 c-48 -16 -133 -104 -147 -151 -14 -45 -9 -64 18 -72 26 -8
|
||||
45 8 59 50 6 19 27 48 45 64 81 71 202 45 249 -53 34 -72 16 -128 -69 -216
|
||||
-29 -30 -65 -73 -79 -94 -32 -48 -35 -102 -6 -111 27 -8 37 -1 60 46 11 21 50
|
||||
70 85 108 100 105 125 183 91 279 -35 97 -107 152 -209 158 -34 2 -78 -2 -97
|
||||
-8z"/>
|
||||
<path d="M444 146 c-3 -8 -4 -25 -2 -38 2 -19 9 -23 38 -23 33 0 35 2 35 35 0
|
||||
32 -3 35 -33 38 -21 2 -34 -2 -38 -12z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 856 B |
28
client/public/images/buttons/radar.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="50.000000pt" height="50.000000pt" viewBox="0 0 50.000000 50.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,50.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M149 442 c-149 -78 -158 -288 -16 -376 198 -122 423 103 301 301 -21
|
||||
34 -44 55 -83 75 -71 37 -131 37 -202 0z m161 -2 c52 -15 76 -36 61 -53 -10
|
||||
-13 -17 -12 -48 4 -47 24 -96 24 -147 0 -23 -10 -48 -21 -57 -24 -25 -8 -36
|
||||
-132 -16 -179 14 -34 72 -88 94 -88 6 0 16 -7 23 -15 15 -18 46 -20 57 -2 4 7
|
||||
23 19 41 27 77 31 114 136 74 212 -17 32 -18 39 -5 49 18 15 38 -10 54 -66 24
|
||||
-84 -10 -174 -84 -223 -56 -37 -153 -39 -210 -3 -110 68 -128 220 -37 311 55
|
||||
55 124 72 200 50z m2 -66 c27 -14 29 -17 15 -31 -11 -12 -19 -12 -35 -4 -62
|
||||
33 -142 -18 -142 -90 0 -32 26 -74 55 -87 37 -17 65 -15 102 8 37 22 53 76 34
|
||||
117 -10 21 -9 29 3 41 14 14 17 12 31 -21 26 -63 15 -117 -35 -162 -27 -25
|
||||
-133 -30 -168 -8 -34 21 -62 73 -62 113 0 40 7 54 33 63 9 3 17 15 17 26 0 44
|
||||
93 65 152 35z m-172 -34 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10
|
||||
10 6 0 10 -4 10 -10z m148 -18 c15 -10 -12 -38 -43 -44 -14 -2 -20 -11 -20
|
||||
-28 0 -20 5 -25 25 -25 17 0 26 6 28 20 6 31 34 58 44 43 13 -21 9 -65 -7 -72
|
||||
-8 -3 -15 -12 -15 -20 0 -16 -21 -26 -56 -26 -35 0 -74 42 -74 80 0 57 72 101
|
||||
118 72z m-28 -72 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10 6 0
|
||||
10 -4 10 -10z m0 -150 c0 -5 -4 -10 -10 -10 -5 0 -10 5 -10 10 0 6 5 10 10 10
|
||||
6 0 10 -4 10 -10z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
21
client/public/images/buttons/reorder.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="96.000000pt" height="96.000000pt" viewBox="0 0 96.000000 96.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,96.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M411 846 c-77 -78 -70 -86 69 -86 139 0 146 8 69 86 -29 30 -60 54
|
||||
-69 54 -9 0 -40 -24 -69 -54z"/>
|
||||
<path d="M132 668 c-16 -16 -15 -43 2 -57 9 -8 110 -11 351 -9 309 3 339 4
|
||||
349 21 8 12 8 22 0 35 -10 16 -40 17 -350 20 -256 2 -343 -1 -352 -10z"/>
|
||||
<path d="M132 508 c-16 -16 -15 -43 2 -57 9 -8 110 -11 351 -9 309 3 339 4
|
||||
349 21 8 12 8 22 0 35 -10 16 -40 17 -350 20 -256 2 -343 -1 -352 -10z"/>
|
||||
<path d="M132 348 c-16 -16 -15 -43 2 -57 9 -8 110 -11 351 -9 309 3 339 4
|
||||
349 21 8 12 8 22 0 35 -10 16 -40 17 -350 20 -256 2 -343 -1 -352 -10z"/>
|
||||
<path d="M363 184 c-7 -18 92 -124 117 -124 25 0 124 106 117 124 -9 24 -225
|
||||
24 -234 0z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
client/public/images/formations/azimuth.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
client/public/images/formations/range.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
client/public/images/formations/single.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
194
client/public/stylesheets/aic.css
Normal file
@ -0,0 +1,194 @@
|
||||
/**************************************/
|
||||
|
||||
.olympus-dialog {
|
||||
align-self: center;
|
||||
background:white;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-self: center;
|
||||
padding:10px;
|
||||
position:absolute;
|
||||
width:fit-content;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.olympus-dialog-close {
|
||||
cursor:pointer;
|
||||
position:absolute;
|
||||
right:10px;
|
||||
top:5px;
|
||||
}
|
||||
|
||||
.olympus-dialog-header {
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
|
||||
/**************************************/
|
||||
|
||||
|
||||
/***** AIC *****/
|
||||
|
||||
.aic-panel {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
#aic-control-panel {
|
||||
bottom:30px;
|
||||
position: absolute;
|
||||
left:30px;
|
||||
}
|
||||
|
||||
|
||||
#aic-control-panel .olympus-button img {
|
||||
max-width: 32px;
|
||||
}
|
||||
|
||||
|
||||
#aic-toolbox, #aic-callsign-panel {
|
||||
align-items: flex-start;
|
||||
align-self: center;
|
||||
flex-direction: column;
|
||||
row-gap: 10px;
|
||||
display:none;
|
||||
position:absolute;
|
||||
}
|
||||
|
||||
.aic-panel {
|
||||
background:#eaeaea;
|
||||
border-bottom-right-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
justify-self: left;
|
||||
padding:5px 10px;
|
||||
}
|
||||
|
||||
.aic-enabled #aic-toolbox, .aic-enabled #aic-callsign-panel {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
.aic-enabled #aic-callsign-panel {
|
||||
align-self: auto;
|
||||
top: 100px;
|
||||
}
|
||||
|
||||
.aic-panel h2 {
|
||||
font-size:90%;
|
||||
margin:0;
|
||||
padding:0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#aic-callsign-display {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#aic-formation-list {
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#aic-formation-list > div {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-top:10px;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
#aic-formation-list .aic-formation-image img {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
max-width: 50px;
|
||||
}
|
||||
|
||||
#aic-formation-list .aic-formation-name {
|
||||
font-size:90%;
|
||||
}
|
||||
|
||||
#aic-formation-list .aic-formation-descriptor {
|
||||
background:white;
|
||||
border-radius: 10px;
|
||||
left:100px;
|
||||
padding:5px;
|
||||
position:absolute;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
#aic-teleprompt {
|
||||
background-color: white;
|
||||
border:2px solid black;
|
||||
border-radius: 10px;
|
||||
bottom: 50px;
|
||||
color: black;
|
||||
display: none;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
padding: 10px;
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.aic-enabled #aic-teleprompt {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
#aic-descriptor {
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-section {
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
margin:0 10px;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-section-label {
|
||||
background-color:#eaeaea;
|
||||
border-top-left-radius: 10px;
|
||||
border-top-right-radius: 10px;
|
||||
padding:.25em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-phrase {
|
||||
border-bottom: 1px solid #ccc;
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
margin-bottom:5px;
|
||||
padding-bottom:2px;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-phrase:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-components .aic-descriptor-component {
|
||||
margin:0 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-component-label {
|
||||
display:none;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-component-value:after {
|
||||
content:",";
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after {
|
||||
content:"; ";
|
||||
}
|
||||
|
||||
#aic-descriptor .aic-descriptor-section:last-of-type .aic-descriptor-component:last-of-type .aic-descriptor-component-value:after {
|
||||
content:".";
|
||||
}
|
||||
|
||||
|
||||
/**************************************/
|
||||
211
client/public/stylesheets/atc.css
Normal file
@ -0,0 +1,211 @@
|
||||
/*** 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;
|
||||
|
||||
}
|
||||
*/
|
||||
@ -1,8 +1,9 @@
|
||||
|
||||
/* Page style */
|
||||
body {
|
||||
padding: 0;
|
||||
display:grid;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
@ -108,4 +109,11 @@ body {
|
||||
#unit-control-panel {
|
||||
top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.hide {
|
||||
display:none !important;
|
||||
}
|
||||
494
client/public/stylesheets/olympus.css
Normal file
@ -0,0 +1,494 @@
|
||||
/* Variables definitions */
|
||||
:root {
|
||||
--accent-green : #8bff63;
|
||||
--accent-light-blue : #5ca7ff;
|
||||
--background-grey : #3d4651;
|
||||
--background-offwhite : #f2f2f3;
|
||||
--background-steel : #202831;
|
||||
--primary-blue : #247be2;
|
||||
--primary-grey : #CFD9E8;
|
||||
--primary-red : #ff5858;
|
||||
--secondary-blue-outline : #082e44;
|
||||
--secondary-dark-steel : #181e25;
|
||||
--secondary-gunmetal-grey : #2f2f2f;
|
||||
--secondary-light-grey : #797e83;
|
||||
--secondary-red-outline : #262222;
|
||||
--secondary-yellow : #ffd46893;
|
||||
|
||||
|
||||
--border-radius-xs : 2px;
|
||||
--border-radius-sm : 5px;
|
||||
--border-radius-md : 10px;
|
||||
--border-radius-lg : 15px;
|
||||
|
||||
--font-weight-bolder : 600;
|
||||
|
||||
}
|
||||
|
||||
:rootOLD {
|
||||
--active-coalition-color: var(--blue-coalition-color);
|
||||
--background-color-dark: #202831;
|
||||
--background-color-light: #AAA;
|
||||
--border-radius-sm:5px;
|
||||
--border-radius-md:10px;
|
||||
--border-radius-lg:15px;
|
||||
--blue-coalition-color: #247be2;
|
||||
--font-weight-bolder:600;
|
||||
--highlight-color: #FFF5;
|
||||
--neutral-coalition-color: whitesmoke;
|
||||
--neutral-coalition-text: #202831;
|
||||
--red-coalition-color: #f32121;
|
||||
--text-color: white;
|
||||
--title-color: #d3e9ff;
|
||||
}
|
||||
|
||||
|
||||
* {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
html {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
background-color:var(--background-steel);
|
||||
border:1px solid var( --background-steel );
|
||||
border-radius: var( --border-radius-sm );
|
||||
color:whitesmoke;
|
||||
cursor:pointer;
|
||||
font-weight: var( --font-weight-bolder );
|
||||
padding:8px;
|
||||
}
|
||||
|
||||
button[disabled="disabled"] {
|
||||
color: var( --highlight-color );
|
||||
cursor:not-allowed;
|
||||
}
|
||||
|
||||
|
||||
.pill {
|
||||
border-radius: var( --border-radius-sm );
|
||||
display:inline-block;
|
||||
padding:6px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.ol-panel {
|
||||
background-color: var(--background-steel);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0px 2px 5px #000A;
|
||||
color:white;
|
||||
font-size: 12px;
|
||||
height:fit-content;
|
||||
padding:10px;
|
||||
width:fit-content;
|
||||
}
|
||||
|
||||
|
||||
.ol-panel-list {
|
||||
border-radius: var( --border-radius-sm );
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: fit-content;
|
||||
row-gap: 5px;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.ol-panel-list .list-item {
|
||||
border-radius: var( --border-radius-md );
|
||||
display:flex;
|
||||
justify-content: space-between;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.ol-panel-list.sortable > .sortable-item {
|
||||
align-items: center;
|
||||
column-gap: 5px;
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.ol-panel-list.sortable > .sortable-item > .handle {
|
||||
cursor:grab;
|
||||
filter:invert(100);
|
||||
}
|
||||
|
||||
.ol-panel-list.sortable > .sortable-item > .handle img {
|
||||
max-width: 16px;
|
||||
}
|
||||
|
||||
|
||||
.ol-panel-board {
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.ol-panel-board > .panel-section {
|
||||
border-right: 1px solid #555;
|
||||
padding:10px;
|
||||
}
|
||||
|
||||
.ol-panel-board > .panel-section:last-of-type {
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.ol-panel-board h1, .ol-panel-board h2 {
|
||||
font-size:18px;
|
||||
font-weight: var( --font-weight-bolder );
|
||||
margin: 0;
|
||||
padding:0 0 5px 0;
|
||||
}
|
||||
|
||||
.ol-panel-board h2 {
|
||||
font-size:14px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.highlight-primary {
|
||||
background-color: var(--secondary-light-grey);
|
||||
}
|
||||
|
||||
.highlight-bluefor {
|
||||
background-color: var(--primary-blue);
|
||||
color: var(--background-steel )
|
||||
}
|
||||
|
||||
.highlight-redfor {
|
||||
background-color: var(--primary-red);
|
||||
}
|
||||
|
||||
.highlight-neutral {
|
||||
background-color: var(--primary-grey);
|
||||
color: var(--secondary-gunmetal-grey)
|
||||
}
|
||||
|
||||
|
||||
.unit {
|
||||
border-radius: var( --border-radius-xs );
|
||||
display:grid;
|
||||
height: fit-content;
|
||||
position:relative;
|
||||
width:fit-content;
|
||||
}
|
||||
|
||||
.unit .unit-id {
|
||||
align-items: center;
|
||||
background: var( --primary-grey );
|
||||
border:3px solid var(--background-steel );
|
||||
border-radius: var( --border-radius-xs );
|
||||
color: var(--background-steel);
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
padding:4px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width:32px;
|
||||
z-index:100;
|
||||
}
|
||||
|
||||
|
||||
.unit .unit-spotlight {
|
||||
border-radius: 50%;
|
||||
align-items: center;
|
||||
display:flex;
|
||||
height: fit-content;
|
||||
justify-items: center;
|
||||
padding:9px;
|
||||
width:fit-content;
|
||||
}
|
||||
|
||||
.unit .unit-hotgroup {
|
||||
align-self: flex-start;
|
||||
background: black;
|
||||
color:white;
|
||||
display:none;
|
||||
height:fit-content;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
line-height: 14px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top:-8px;
|
||||
transform:rotate(45deg);
|
||||
width:14px;
|
||||
}
|
||||
|
||||
.unit .unit-hotgroup-id {
|
||||
font-size:11px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
.unit .unit-vvi {
|
||||
display:flex;
|
||||
position:absolute;
|
||||
left:50%;
|
||||
transform:rotate(-90deg);
|
||||
transform-origin:0 50%;
|
||||
top:50%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.unit .unit-vvi-heading {
|
||||
border:1px solid var( --secondary-dark-steel );
|
||||
padding-left:14px;
|
||||
transform: rotate(90deg);
|
||||
transform-origin:0 50%;
|
||||
width:30px;
|
||||
}
|
||||
|
||||
|
||||
.unit .unit-selected-border {
|
||||
border: 2px solid transparent;
|
||||
border-radius: var( --border-radius-xs );
|
||||
position: relative;
|
||||
z-index:1;
|
||||
}
|
||||
|
||||
|
||||
.unit .unit-fuel {
|
||||
background:white;
|
||||
border:2px solid var( --secondary-dark-steel );
|
||||
border-radius: var( --border-radius-xs );
|
||||
display:none;
|
||||
margin:0 auto;
|
||||
position: relative;
|
||||
top:-6px;
|
||||
width: calc( 100% - 16px );
|
||||
}
|
||||
|
||||
.unit .unit-fuel-empty {
|
||||
align-self: center;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
color:red;
|
||||
display:none;
|
||||
font-weight: bold;
|
||||
justify-self: center;
|
||||
padding:1px;
|
||||
position:absolute;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="0"] .unit-fuel-empty {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="10"] .unit-fuel,
|
||||
.unit[data-fuel-level="20"] .unit-fuel,
|
||||
.unit[data-fuel-level="30"] .unit-fuel {
|
||||
animation: blinker 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.unit .unit-fuel-level {
|
||||
background-color: var( --secondary-light-grey );
|
||||
display:flex;
|
||||
height: 4px;
|
||||
visibility: hidden;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="10"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="20"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="30"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="40"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="50"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="60"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="70"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="80"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="90"] .unit-fuel-level,
|
||||
.unit[data-fuel-level="100"] .unit-fuel-level {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.unit[data-fuel-level="10"] .unit-fuel-level {
|
||||
width:10%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="20"] .unit-fuel-level {
|
||||
width:20%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="30"] .unit-fuel-level {
|
||||
width:30%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="40"] .unit-fuel-level {
|
||||
width:40%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="50"] .unit-fuel-level {
|
||||
width:50%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="60"] .unit-fuel-level {
|
||||
width:60%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="70"] .unit-fuel-level {
|
||||
width:70%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="80"] .unit-fuel-level {
|
||||
width:80%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="90"] .unit-fuel-level {
|
||||
width:90%;
|
||||
}
|
||||
|
||||
.unit[data-fuel-level="100"] .unit-fuel-level {
|
||||
width:100%;
|
||||
}
|
||||
|
||||
|
||||
.unit-ammo {
|
||||
column-gap: 2px;
|
||||
display:none;
|
||||
flex-direction: row;
|
||||
flex-wrap:nowrap;
|
||||
height:fit-content;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
top:-2px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.unit-ammo > [data-ammo-type] {
|
||||
background:white;
|
||||
border:2px solid var( --secondary-dark-steel );
|
||||
border-radius: 50%;
|
||||
padding:3px;
|
||||
}
|
||||
|
||||
.unit[data-has-fox-1="true"] .unit-ammo > [data-ammo-type="fox-1"],
|
||||
.unit[data-has-fox-2="true"] .unit-ammo > [data-ammo-type="fox-2"],
|
||||
.unit[data-has-fox-3="true"] .unit-ammo > [data-ammo-type="fox-3"],
|
||||
.unit[data-has-other-ammo="true"] .unit-ammo > [data-ammo-type="other"] {
|
||||
background-color: var( --secondary-light-grey );
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*** BLUEFOR ***/
|
||||
|
||||
.unit[data-coalition="blue"] .unit-hotgroup {
|
||||
background-color: var( --secondary-blue-outline );
|
||||
}
|
||||
|
||||
.unit[data-coalition="blue"] .unit-id,
|
||||
.unit[data-coalition="blue"][data-has-fox-1="true"] .unit-ammo > [data-ammo-type="fox-1"],
|
||||
.unit[data-coalition="blue"][data-has-fox-2="true"] .unit-ammo > [data-ammo-type="fox-2"],
|
||||
.unit[data-coalition="blue"][data-has-fox-3="true"] .unit-ammo > [data-ammo-type="fox-3"],
|
||||
.unit[data-coalition="blue"][data-has-other-ammo="true"] .unit-ammo > [data-ammo-type="other"],
|
||||
.unit[data-coalition="blue"] .unit-fuel-level,
|
||||
.unit[data-coalition="blue"] .unit-vvi-heading {
|
||||
background: var( --primary-blue );
|
||||
border-color: var( --secondary-blue-outline );
|
||||
color: var( --secondary-blue-outline );
|
||||
}
|
||||
|
||||
.unit[data-coalition="blue"][data-is-selected="true"] .unit-id {
|
||||
border-color: var( --secondary-blue-outline );
|
||||
color: var( --primary-blue );
|
||||
}
|
||||
|
||||
.unit[data-coalition="blue"] .unit-ammo {
|
||||
border-color: var( --secondary-blue-outline );
|
||||
}
|
||||
|
||||
/*** REDFOR ***/
|
||||
|
||||
.unit[data-coalition="red"] .unit-hotgroup {
|
||||
background-color: var( --secondary-red-outline );
|
||||
}
|
||||
|
||||
.unit[data-coalition="red"] .unit-id,
|
||||
.unit[data-coalition="red"][data-has-fox-1="true"] .unit-ammo > [data-ammo-type="fox-1"],
|
||||
.unit[data-coalition="red"][data-has-fox-2="true"] .unit-ammo > [data-ammo-type="fox-2"],
|
||||
.unit[data-coalition="red"][data-has-fox-3="true"] .unit-ammo > [data-ammo-type="fox-3"],
|
||||
.unit[data-coalition="red"][data-has-other-ammo="true"] .unit-ammo > [data-ammo-type="other"],
|
||||
.unit[data-coalition="red"] .unit-fuel-level,
|
||||
.unit[data-coalition="red"] .unit-vvi-heading {
|
||||
background: var( --primary-red );
|
||||
border-color: var( --secondary-red-outline );
|
||||
color: var( --secondary-red-outline );
|
||||
}
|
||||
|
||||
|
||||
.unit[data-coalition="red"][data-is-selected="true"] .unit-id {
|
||||
color: var( --primary-red );
|
||||
}
|
||||
|
||||
.unit[data-coalition="red"] .unit-ammo {
|
||||
border-color: var( --secondary-red-outline );
|
||||
}
|
||||
|
||||
/**************/
|
||||
|
||||
|
||||
.unit[data-is-selected="true"] .unit-spotlight {
|
||||
background: var(--secondary-yellow);
|
||||
}
|
||||
|
||||
.unit[data-is-in-hotgroup="true"] .unit-hotgroup {
|
||||
display:flex
|
||||
}
|
||||
|
||||
.unit[data-is-selected="true"] .unit-id {
|
||||
background:white;
|
||||
}
|
||||
|
||||
.unit[data-is-selected="true"] .unit-selected-border {
|
||||
border-color:white;
|
||||
}
|
||||
|
||||
.unit[data-is-selected="true"] .unit-fuel {
|
||||
display:grid;
|
||||
}
|
||||
|
||||
.unit[data-is-selected="true"] .unit-ammo {
|
||||
display:flex;
|
||||
}
|
||||
|
||||
|
||||
.unit[data-pilot="human"] .unit-hotgroup,
|
||||
.unit[data-pilot="human"] .unit-fuel,
|
||||
.unit[data-pilot="human"] .unit-ammo {
|
||||
display:none;
|
||||
}
|
||||
|
||||
|
||||
.unit .unit-summary {
|
||||
align-self: center;
|
||||
column-gap: 8px;
|
||||
color:white;
|
||||
display: flex;
|
||||
flex-flow: wrap;
|
||||
font-size: 12px;
|
||||
justify-content: flex-end;
|
||||
justify-self: flex-end;
|
||||
left: -62px;
|
||||
position: absolute;
|
||||
row-gap:2px;
|
||||
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
|
||||
white-space: nowrap;
|
||||
width: min-content;
|
||||
}
|
||||
@ -16,6 +16,9 @@
|
||||
@import url("mouseinfopanel.css");
|
||||
@import url("logpanel.css");
|
||||
|
||||
@import url( "aic.css" );
|
||||
@import url( "atc.css" );
|
||||
|
||||
@import url("layout.css");
|
||||
|
||||
|
||||
@ -35,7 +38,7 @@
|
||||
* {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
|
||||
61
client/public/stylesheets/uikit.css
Normal file
@ -0,0 +1,61 @@
|
||||
body {
|
||||
background-color:#eaeaea;
|
||||
}
|
||||
|
||||
#content-wrapper {
|
||||
row-gap: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
height:100%;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
section {
|
||||
column-gap: 20px;
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size:125%;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
.content {
|
||||
background:white;
|
||||
border-radius: 10px;
|
||||
height:fit-content;
|
||||
margin-bottom:4vh;
|
||||
padding:20px;
|
||||
width:fit-content;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
color:#666;
|
||||
letter-spacing:1px;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
.content-body {
|
||||
column-gap: 20px;
|
||||
display:flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.example {
|
||||
align-items: center;
|
||||
display:flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin:2vh 0 1vh 0;
|
||||
}
|
||||
|
||||
|
||||
#paragraph {
|
||||
max-width: 750px;
|
||||
}
|
||||
1451
client/public/uikit.html
Normal file
134
client/src/FeatureSwitches.ts
Normal file
@ -0,0 +1,134 @@
|
||||
export interface FeatureSwitchInterface {
|
||||
"defaultEnabled": boolean, // default on/off state (if allowed by masterSwitch)
|
||||
"label": string,
|
||||
"masterSwitch": boolean, // on/off regardless of user preference
|
||||
"name": string,
|
||||
"options"?: object,
|
||||
"removeArtifactsIfDisabled"?: boolean
|
||||
}
|
||||
|
||||
|
||||
class FeatureSwitch {
|
||||
|
||||
// From config param
|
||||
defaultEnabled;
|
||||
label;
|
||||
masterSwitch;
|
||||
name;
|
||||
removeArtifactsIfDisabled = true;
|
||||
|
||||
// Self-set
|
||||
userPreference;
|
||||
|
||||
|
||||
constructor( config:FeatureSwitchInterface ) {
|
||||
|
||||
this.defaultEnabled = config.defaultEnabled;
|
||||
this.label = config.label;
|
||||
this.masterSwitch = config.masterSwitch;
|
||||
this.name = config.name;
|
||||
|
||||
this.userPreference = this.getUserPreference();
|
||||
|
||||
}
|
||||
|
||||
|
||||
getUserPreference() {
|
||||
|
||||
let preferences = JSON.parse( localStorage.getItem( "featureSwitches" ) || "{}" );
|
||||
|
||||
return ( preferences.hasOwnProperty( this.name ) ) ? preferences[ this.name ] : this.defaultEnabled;
|
||||
|
||||
}
|
||||
|
||||
|
||||
isEnabled() {
|
||||
|
||||
if ( !this.masterSwitch ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.userPreference;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class FeatureSwitches {
|
||||
|
||||
#featureSwitches:FeatureSwitch[] = [
|
||||
|
||||
new FeatureSwitch({
|
||||
"defaultEnabled": false,
|
||||
"label": "AIC",
|
||||
"masterSwitch": true,
|
||||
"name": "aic"
|
||||
}),
|
||||
|
||||
new FeatureSwitch({
|
||||
"defaultEnabled": false,
|
||||
"label": "AI Formations",
|
||||
"masterSwitch": true,
|
||||
"name": "ai-formations",
|
||||
"removeArtifactsIfDisabled": false
|
||||
}),
|
||||
|
||||
new FeatureSwitch({
|
||||
"defaultEnabled": false,
|
||||
"label": "ATC",
|
||||
"masterSwitch": true,
|
||||
"name": "atc"
|
||||
})
|
||||
|
||||
];
|
||||
|
||||
|
||||
constructor() {
|
||||
|
||||
this.#removeArtifacts();
|
||||
|
||||
this.savePreferences();
|
||||
|
||||
}
|
||||
|
||||
|
||||
getSwitch( switchName:string ) {
|
||||
|
||||
return this.#featureSwitches.find( featureSwitch => featureSwitch.name === switchName );
|
||||
|
||||
}
|
||||
|
||||
|
||||
#removeArtifacts() {
|
||||
|
||||
for ( const featureSwitch of this.#featureSwitches ) {
|
||||
if ( !featureSwitch.isEnabled() ) {
|
||||
|
||||
document.querySelectorAll( "[data-feature-switch='" + featureSwitch.name + "']" ).forEach( el => {
|
||||
|
||||
if ( featureSwitch.removeArtifactsIfDisabled === false ) {
|
||||
el.remove();
|
||||
} else {
|
||||
el.classList.add( "hide" );
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
savePreferences() {
|
||||
|
||||
let preferences:any = {};
|
||||
|
||||
for ( const featureSwitch of this.#featureSwitches ) {
|
||||
preferences[ featureSwitch.name ] = featureSwitch.isEnabled();
|
||||
}
|
||||
|
||||
localStorage.setItem( "featureSwitches", JSON.stringify( preferences ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
client/src/ToggleableFeature.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export abstract class ToggleableFeature {
|
||||
|
||||
#status:boolean = false;
|
||||
|
||||
|
||||
constructor( defaultStatus:boolean ) {
|
||||
|
||||
this.#status = defaultStatus;
|
||||
|
||||
this.onStatusUpdate();
|
||||
|
||||
}
|
||||
|
||||
|
||||
getStatus() : boolean {
|
||||
return this.#status;
|
||||
}
|
||||
|
||||
|
||||
protected onStatusUpdate() {}
|
||||
|
||||
|
||||
toggleStatus( force?:boolean ) : void {
|
||||
|
||||
if ( force ) {
|
||||
this.#status = force;
|
||||
} else {
|
||||
this.#status = !this.#status;
|
||||
}
|
||||
|
||||
this.onStatusUpdate();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
54
client/src/aic/AICFormation.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { AICFormationContextDataInterface, AICFormationDescriptor } from "./AICFormationDescriptor";
|
||||
import { AICFormationDescriptorPhrase } from "./AICFormationDescriptorPhrase";
|
||||
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
|
||||
|
||||
export interface AICFormationInterface {
|
||||
"icon" : string,
|
||||
"label" : string,
|
||||
"name" : string,
|
||||
"numGroups" : number,
|
||||
"summary" : string,
|
||||
"unitBreakdown" : string[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export abstract class AICFormation {
|
||||
|
||||
"icon" = "";
|
||||
"label" = "";
|
||||
"name" = "";
|
||||
"numGroups" = 1;
|
||||
"summary" = "";
|
||||
"unitBreakdown":string[] = []
|
||||
|
||||
|
||||
constructor() {
|
||||
|
||||
this.unitBreakdown = [];
|
||||
}
|
||||
|
||||
|
||||
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
|
||||
return phrase;
|
||||
}
|
||||
|
||||
|
||||
getDescriptor( contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
return new AICFormationDescriptor().generate( this, contextData );
|
||||
|
||||
}
|
||||
|
||||
|
||||
hasUnitBreakdown() {
|
||||
return this.unitBreakdown.length > 0;
|
||||
}
|
||||
|
||||
|
||||
showFormationNameInDescriptor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
38
client/src/aic/AICFormation/Azimuth.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { AICFormation, AICFormationInterface } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
|
||||
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
|
||||
|
||||
export class AICFormation_Azimuth extends AICFormation implements AICFormationInterface {
|
||||
|
||||
"icon" = "azimuth.png";
|
||||
"label" = "Azimuth";
|
||||
"name" = "azimuth";
|
||||
"numGroups" = 2;
|
||||
"summary" = "Two contacts, side-by-side in a line perpedicular to the perspective.";
|
||||
"unitBreakdown" = [ "<compass> group", "<compass> group" ];
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
switch ( section.name ) {
|
||||
|
||||
case "formation":
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<distance>" ) );
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>" ) );
|
||||
|
||||
}
|
||||
|
||||
return phrase;
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
38
client/src/aic/AICFormation/Range.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { AICFormation, AICFormationInterface } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
|
||||
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
|
||||
|
||||
export class AICFormation_Range extends AICFormation implements AICFormationInterface {
|
||||
|
||||
"icon" = "range.png";
|
||||
"label" = "Range";
|
||||
"name" = "range";
|
||||
"numGroups" = 2;
|
||||
"summary" = "Two contacts, one behind the other";
|
||||
"unitBreakdown" = [ "Lead group", "Trail group" ];
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
addToDescriptorPhrase( section: AICFormationDescriptorSection, phrase: AICFormationDescriptorPhrase, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
switch ( section.name ) {
|
||||
|
||||
case "formation":
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<distance>" ) );
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>" ) );
|
||||
|
||||
|
||||
}
|
||||
|
||||
return phrase;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
24
client/src/aic/AICFormation/Single.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { AICFormation, AICFormationInterface } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface, AICFormationDescriptor } from "../AICFormationDescriptor";
|
||||
|
||||
export class AICFormation_Single extends AICFormation implements AICFormationInterface {
|
||||
|
||||
"icon" = "single.png";
|
||||
"label" = "Single";
|
||||
"name" = "single";
|
||||
"numGroups" = 1;
|
||||
"summary" = "One contact on its own";
|
||||
"unitBreakdown" = [];
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
|
||||
showFormationNameInDescriptor() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
55
client/src/aic/AICFormationDescriptor.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { AICFormation } from "./AICFormation";
|
||||
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorSection_Formation } from "./AICFormationDescriptorSection/Formation";
|
||||
import { AICFormationDescriptorSection_Unit } from "./AICFormationDescriptorSection/Unit";
|
||||
import { AICFormationDescriptorSection_NumGroups } from "./AICFormationDescriptorSection/NumGroups";
|
||||
import { AICFormationDescriptorSection_Who } from "./AICFormationDescriptorSection/Who";
|
||||
|
||||
|
||||
export interface AICFormationContextDataInterface {
|
||||
"aicCallsign" : string,
|
||||
"bullseyeName" : string,
|
||||
"control" : "broadcast" | "tactical",
|
||||
"numGroups" : number
|
||||
}
|
||||
|
||||
|
||||
export class AICFormationDescriptor {
|
||||
|
||||
#sections:AICFormationDescriptorSection[] = [
|
||||
new AICFormationDescriptorSection_Who(),
|
||||
new AICFormationDescriptorSection_NumGroups(),
|
||||
new AICFormationDescriptorSection_Formation(),
|
||||
new AICFormationDescriptorSection_Unit()
|
||||
]
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
||||
addSection( section:AICFormationDescriptorSection ) {
|
||||
this.#sections.push( section );
|
||||
}
|
||||
|
||||
|
||||
getSections() {
|
||||
return this.#sections;
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
let output:object[] = [];
|
||||
|
||||
for ( const section of this.#sections ) {
|
||||
output.push(
|
||||
section.generate( formation, contextData )
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
18
client/src/aic/AICFormationDescriptorComponent.ts
Normal file
@ -0,0 +1,18 @@
|
||||
interface ComponentInterface {
|
||||
"label" : string;
|
||||
"value" : string;
|
||||
}
|
||||
|
||||
export class AICFormationDescriptorComponent implements ComponentInterface {
|
||||
|
||||
label = "(not set)";
|
||||
value = "(not set)";
|
||||
|
||||
constructor( value:any, label?:string ) {
|
||||
|
||||
this.label = label || "(not set)";
|
||||
this.value = value;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
|
||||
export abstract class AICFormactionDescriptorComponent_Distance extends AICFormationDescriptorComponent {
|
||||
|
||||
constructor( value:string, label?:string ) {
|
||||
super( value, label );
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { AICFormactionDescriptorComponent_Distance } from "../Distance";
|
||||
|
||||
export class AICFormationDescriptorComponent_Distance_Range extends AICFormactionDescriptorComponent_Distance {
|
||||
|
||||
constructor( value:string, label?:string ) {
|
||||
super( value, label );
|
||||
}
|
||||
|
||||
}
|
||||
40
client/src/aic/AICFormationDescriptorPhrase.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { AICFormation } from "./AICFormation";
|
||||
import { AICFormationContextDataInterface } from "./AICFormationDescriptor";
|
||||
import { AICFormationDescriptorComponent } from "./AICFormationDescriptorComponent";
|
||||
|
||||
export interface AICFormationDescriptorPhraseInterface {
|
||||
"generate" : CallableFunction,
|
||||
"label" : string,
|
||||
"name" : string
|
||||
}
|
||||
|
||||
export class AICFormationDescriptorPhrase {
|
||||
|
||||
#components : AICFormationDescriptorComponent[] = [];
|
||||
label = "";
|
||||
name = "";
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
||||
addComponent( component:AICFormationDescriptorComponent ) {
|
||||
this.#components.push( component );
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
getComponents() {
|
||||
return this.#components;
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
40
client/src/aic/AICFormationDescriptorSection.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { AICFormation } from "./AICFormation";
|
||||
import { AICFormationContextDataInterface } from "./AICFormationDescriptor";
|
||||
import { AICFormationDescriptorPhrase } from "./AICFormationDescriptorPhrase";
|
||||
|
||||
export interface AICFormationDescriptorSectionInterface {
|
||||
"generate" : CallableFunction,
|
||||
"label" : string,
|
||||
"name" : string,
|
||||
"omitSection" : boolean
|
||||
}
|
||||
|
||||
export abstract class AICFormationDescriptorSection {
|
||||
|
||||
#phrases : AICFormationDescriptorPhrase[] = [];
|
||||
label = "";
|
||||
name = "";
|
||||
omitSection = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
||||
addPhrase( phrase:AICFormationDescriptorPhrase ) {
|
||||
this.#phrases.push( phrase );
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
|
||||
getPhrases() {
|
||||
return this.#phrases;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
39
client/src/aic/AICFormationDescriptorSection/Formation.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { AICFormation } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
|
||||
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
|
||||
|
||||
export class AICFormationDescriptorSection_Formation extends AICFormationDescriptorSection {
|
||||
|
||||
label = "Formation";
|
||||
name = "formation";
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
if ( !formation.showFormationNameInDescriptor() ) {
|
||||
this.omitSection = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
let phrase = new AICFormationDescriptorPhrase();
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( formation.label, "Formation" ) );
|
||||
|
||||
phrase = formation.addToDescriptorPhrase( this, phrase, contextData );
|
||||
|
||||
this.addPhrase( phrase );
|
||||
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
35
client/src/aic/AICFormationDescriptorSection/NumGroups.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { AICFormation } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
|
||||
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
|
||||
|
||||
export class AICFormationDescriptorSection_NumGroups extends AICFormationDescriptorSection {
|
||||
|
||||
label = "Groups";
|
||||
name = "numgroups";
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
let value = "Single group";
|
||||
|
||||
if ( contextData.numGroups > 1 ) {
|
||||
value = contextData.numGroups + " groups";
|
||||
}
|
||||
|
||||
let phrase = new AICFormationDescriptorPhrase();
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( value, "Number of groups" ) );
|
||||
this.addPhrase( phrase );
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
83
client/src/aic/AICFormationDescriptorSection/Unit.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { AICFormation } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
|
||||
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
|
||||
|
||||
interface addUnitInformationInterface {
|
||||
omitTrack?: boolean
|
||||
}
|
||||
|
||||
export class AICFormationDescriptorSection_Unit extends AICFormationDescriptorSection {
|
||||
|
||||
label = "Unit";
|
||||
name = "unit";
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
|
||||
addUnitInformation( formation:AICFormation, contextData: AICFormationContextDataInterface, phrase: AICFormationDescriptorPhrase, options?:addUnitInformationInterface ) {
|
||||
|
||||
options = options || {};
|
||||
|
||||
const originPoint = ( contextData.control === "broadcast" ) ? contextData.bullseyeName : "BRAA";
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( originPoint, "Bearing origin point" ) );
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<bearing>", "Bearing" ) );
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<range>", "Range" ) );
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<altitude>", "Altitude" ) );
|
||||
|
||||
if ( contextData.control === "broadcast" ) {
|
||||
if ( !options.hasOwnProperty( "omitTrack" ) || options.omitTrack !== true ) {
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "track <compass>", "Tracking" ) );
|
||||
}
|
||||
} else {
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "[hot|flanking [left|right]|beam <compass>|cold]", "Azimuth" ) );
|
||||
}
|
||||
|
||||
return phrase;
|
||||
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
if ( formation.hasUnitBreakdown() ) {
|
||||
|
||||
for ( const [ i, unitRef ] of formation.unitBreakdown.entries() ) {
|
||||
|
||||
let phrase = new AICFormationDescriptorPhrase();
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( unitRef, "Unit reference" ) );
|
||||
|
||||
if ( i === 0 ) {
|
||||
this.addUnitInformation( formation, contextData, phrase, { "omitTrack": true } );
|
||||
} else {
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<altitude>" ) );
|
||||
}
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "hostile" ) );
|
||||
|
||||
this.addPhrase( phrase );
|
||||
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
this.addPhrase(
|
||||
this.addUnitInformation( formation, contextData, new AICFormationDescriptorPhrase() )
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
35
client/src/aic/AICFormationDescriptorSection/Who.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { AICFormation } from "../AICFormation";
|
||||
import { AICFormationContextDataInterface } from "../AICFormationDescriptor";
|
||||
import { AICFormationDescriptorSection } from "../AICFormationDescriptorSection";
|
||||
import { AICFormationDescriptorComponent } from "../AICFormationDescriptorComponent";
|
||||
import { AICFormationDescriptorPhrase } from "../AICFormationDescriptorPhrase";
|
||||
|
||||
export class AICFormationDescriptorSection_Who extends AICFormationDescriptorSection {
|
||||
|
||||
label = "Who";
|
||||
name = "who";
|
||||
|
||||
constructor() {
|
||||
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
|
||||
generate( formation:AICFormation, contextData: AICFormationContextDataInterface ) {
|
||||
|
||||
let phrase = new AICFormationDescriptorPhrase();
|
||||
|
||||
if ( contextData.control === "tactical" ) {
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( "<their callsign>", "Their callsign" ) );
|
||||
}
|
||||
|
||||
phrase.addComponent( new AICFormationDescriptorComponent( contextData.aicCallsign, "Your callsign" ) );
|
||||
|
||||
this.addPhrase( phrase );
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
172
client/src/aic/aic.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import { ToggleableFeature } from "../ToggleableFeature";
|
||||
import { AICFormation_Azimuth } from "./AICFormation/Azimuth";
|
||||
import { AICFormation_Range } from "./AICFormation/Range";
|
||||
import { AICFormation_Single } from "./AICFormation/Single";
|
||||
import { AICFormationDescriptorSection } from "./AICFormationDescriptorSection";
|
||||
|
||||
|
||||
export class AIC extends ToggleableFeature {
|
||||
|
||||
#formations = [
|
||||
|
||||
new AICFormation_Single(),
|
||||
new AICFormation_Range(),
|
||||
new AICFormation_Azimuth()
|
||||
|
||||
];
|
||||
|
||||
|
||||
constructor() {
|
||||
|
||||
super( false );
|
||||
|
||||
this.onStatusUpdate();
|
||||
|
||||
// This feels kind of dirty
|
||||
let $aicFormationList = document.getElementById( "aic-formation-list" );
|
||||
|
||||
if ( $aicFormationList ) {
|
||||
|
||||
this.getFormations().forEach( formation => {
|
||||
|
||||
// Image
|
||||
let $imageDiv = document.createElement( "div" );
|
||||
$imageDiv.classList.add( "aic-formation-image" );
|
||||
|
||||
let $img = document.createElement( "img" );
|
||||
$img.src = "images/formations/" + formation.icon;
|
||||
|
||||
$imageDiv.appendChild( $img );
|
||||
|
||||
// Name
|
||||
let $nameDiv = document.createElement( "div" );
|
||||
$nameDiv.classList.add( "aic-formation-name" );
|
||||
$nameDiv.innerText = formation.label;
|
||||
|
||||
// Wrapper
|
||||
let $wrapperDiv = document.createElement( "div" );
|
||||
$wrapperDiv.dataset.formationName = formation.name;
|
||||
$wrapperDiv.appendChild( $imageDiv )
|
||||
$wrapperDiv.appendChild( $nameDiv );
|
||||
$wrapperDiv.addEventListener( "click", ( ev ) => {
|
||||
|
||||
const controlTypeInput = document.querySelector( "input[type='radio'][name='control-type']:checked" );
|
||||
|
||||
let controlTypeValue:any = ( controlTypeInput instanceof HTMLInputElement && [ "broadcast", "tactical" ].indexOf( controlTypeInput.value ) > -1 ) ? controlTypeInput.value : "broadcast";
|
||||
|
||||
// TODO: make this not an "any"
|
||||
const output:any = formation.getDescriptor({
|
||||
"aicCallsign" : "Magic",
|
||||
"bullseyeName" : "Bullseye",
|
||||
"control" : controlTypeValue,
|
||||
"numGroups" : formation.numGroups
|
||||
});
|
||||
|
||||
this.updateTeleprompt( output );
|
||||
|
||||
});
|
||||
|
||||
// Add to DOM
|
||||
$aicFormationList?.appendChild( $wrapperDiv );
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
getFormations() {
|
||||
return this.#formations;
|
||||
}
|
||||
|
||||
|
||||
onStatusUpdate() {
|
||||
|
||||
// Update the DOM
|
||||
document.body.classList.toggle( "aic-enabled", this.getStatus() );
|
||||
|
||||
}
|
||||
|
||||
|
||||
toggleHelp() {
|
||||
document.getElementById( "aic-help" )?.classList.toggle( "hide" );
|
||||
}
|
||||
|
||||
//*
|
||||
updateTeleprompt<T extends AICFormationDescriptorSection>( descriptor:T[] ) {
|
||||
|
||||
let $teleprompt = document.getElementById( "aic-teleprompt" );
|
||||
|
||||
if ( $teleprompt instanceof HTMLElement ) {
|
||||
|
||||
// Clean slate
|
||||
while ( $teleprompt.childNodes.length > 0 ) {
|
||||
$teleprompt.childNodes[0].remove();
|
||||
}
|
||||
|
||||
function newDiv() {
|
||||
return document.createElement( "div" );
|
||||
}
|
||||
|
||||
// Wrapper
|
||||
let $descriptor = newDiv();
|
||||
$descriptor.id = "aic-descriptor";
|
||||
|
||||
for ( const section of descriptor ) {
|
||||
|
||||
if ( section.omitSection ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let $section = newDiv();
|
||||
$section.classList.add( "aic-descriptor-section" );
|
||||
|
||||
let $sectionLabel = newDiv();
|
||||
$sectionLabel.classList.add( "aic-descriptor-section-label" );
|
||||
$sectionLabel.innerText = section.label;
|
||||
$section.appendChild( $sectionLabel );
|
||||
|
||||
|
||||
for ( const phrase of section.getPhrases() ) {
|
||||
|
||||
let $phrase = newDiv();
|
||||
$phrase.classList.add( "aic-descriptor-phrase" );
|
||||
|
||||
for ( const component of phrase.getComponents() ) {
|
||||
|
||||
let $component = newDiv();
|
||||
$component.classList.add( "aic-descriptor-component" );
|
||||
|
||||
let $componentLabel = newDiv();
|
||||
$componentLabel.classList.add( "aic-descriptor-component-label" );
|
||||
$componentLabel.innerText = component.label;
|
||||
|
||||
let $componentValue = newDiv();
|
||||
$componentValue.classList.add( "aic-descriptor-component-value" );
|
||||
$componentValue.innerText = component.value;
|
||||
|
||||
$component.appendChild( $componentLabel );
|
||||
$component.appendChild( $componentValue );
|
||||
|
||||
$phrase.appendChild( $component );
|
||||
|
||||
}
|
||||
|
||||
$section.appendChild( $phrase );
|
||||
|
||||
}
|
||||
|
||||
$descriptor.appendChild( $section );
|
||||
|
||||
}
|
||||
|
||||
$teleprompt.appendChild( $descriptor );
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
//*/
|
||||
|
||||
}
|
||||
87
client/src/atc/ATC.ts
Normal file
@ -0,0 +1,87 @@
|
||||
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() );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
7
client/src/atc/ATCMockAPI.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export abstract class ATCMockAPI {
|
||||
|
||||
constructor() {}
|
||||
|
||||
generateMockData() {}
|
||||
|
||||
}
|
||||
40
client/src/atc/ATCMockAPI/Flights.ts
Normal file
@ -0,0 +1,40 @@
|
||||
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 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
18
client/src/atc/FlightList.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ATCMockAPI_Flights } from "./ATCMockAPI/Flights";
|
||||
|
||||
export class ATCFLightList {
|
||||
|
||||
|
||||
constructor() {
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
getFlights( generateMockDataIfEmpty?:boolean ) {
|
||||
let api = new ATCMockAPI_Flights();
|
||||
return api.get( generateMockDataIfEmpty );
|
||||
}
|
||||
|
||||
}
|
||||
@ -9,8 +9,14 @@ import { ConnectionStatusPanel } from "./panels/connectionstatuspanel";
|
||||
import { MissionData } from "./missiondata/missiondata";
|
||||
import { UnitControlPanel } from "./panels/unitcontrolpanel";
|
||||
import { MouseInfoPanel } from "./panels/mouseInfoPanel";
|
||||
import { Slider } from "./controls/slider";
|
||||
import { AIC } from "./aic/AIC";
|
||||
|
||||
import { VisibilityControlPanel } from "./panels/visibilitycontrolpanel";
|
||||
import { ATC } from "./atc/ATC";
|
||||
import { FeatureSwitches } from "./FeatureSwitches";
|
||||
import { LogPanel } from "./panels/logpanel";
|
||||
import { Button } from "./controls/button";
|
||||
|
||||
/* TODO: should this be a class? */
|
||||
var map: Map;
|
||||
@ -29,10 +35,31 @@ var logPanel: LogPanel;
|
||||
|
||||
var mapSourceDropdown: Dropdown;
|
||||
|
||||
var slowButton: Button;
|
||||
var fastButton: Button;
|
||||
var climbButton: Button;
|
||||
var descendButton: Button;
|
||||
|
||||
var aic: AIC;
|
||||
var aicToggleButton: Button;
|
||||
var aicHelpButton: Button;
|
||||
|
||||
|
||||
var atc: ATC;
|
||||
var atcToggleButton: Button;
|
||||
|
||||
var altitudeSlider: Slider;
|
||||
var airspeedSlider: Slider;
|
||||
|
||||
var connected: boolean;
|
||||
var activeCoalition: string;
|
||||
|
||||
var featureSwitches;
|
||||
|
||||
function setup() {
|
||||
|
||||
featureSwitches = new FeatureSwitches();
|
||||
|
||||
/* Initialize */
|
||||
map = new Map('map-container');
|
||||
unitsManager = new UnitsManager();
|
||||
@ -43,11 +70,71 @@ function setup() {
|
||||
|
||||
unitInfoPanel = new UnitInfoPanel("unit-info-panel");
|
||||
unitControlPanel = new UnitControlPanel("unit-control-panel");
|
||||
//scenarioDropdown = new Dropdown("scenario-dropdown", ["Caucasus", "Marianas", "Nevada", "South Atlantic", "Syria", "The Channel"], () => { });
|
||||
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
|
||||
connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel");
|
||||
mouseInfoPanel = new MouseInfoPanel("mouse-info-panel");
|
||||
visibilityControlPanel = new VisibilityControlPanel("visibility-control-panel");
|
||||
logPanel = new LogPanel("log-panel");
|
||||
|
||||
missionData = new MissionData();
|
||||
|
||||
/* Unit control buttons */
|
||||
slowButton = new Button("slow-button", ["images/buttons/slow.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("slow"); });
|
||||
fastButton = new Button("fast-button", ["images/buttons/fast.svg"], () => { getUnitsManager().selectedUnitsChangeSpeed("fast"); });
|
||||
climbButton = new Button("climb-button", ["images/buttons/climb.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("climb"); });
|
||||
descendButton = new Button("descend-button", ["images/buttons/descend.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("descend"); });
|
||||
|
||||
/* Unit control sliders */
|
||||
altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => getUnitsManager().selectedUnitsSetAltitude(value * 0.3048));
|
||||
airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => getUnitsManager().selectedUnitsSetSpeed(value / 1.94384));
|
||||
|
||||
/* AIC */
|
||||
|
||||
let aicFeatureSwitch = featureSwitches.getSwitch( "aic" );
|
||||
|
||||
if ( aicFeatureSwitch?.isEnabled() ) {
|
||||
aic = new AIC();
|
||||
|
||||
aicToggleButton = new Button( "toggle-aic-button", ["images/buttons/radar.svg"], () => {
|
||||
aic.toggleStatus();
|
||||
});
|
||||
|
||||
aicHelpButton = new Button( "aic-help-button", [ "images/buttons/question-mark.svg" ], () => {
|
||||
aic.toggleHelp();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/* Generic clicks */
|
||||
|
||||
document.addEventListener( "click", ( ev ) => {
|
||||
|
||||
if ( ev instanceof PointerEvent && ev.target instanceof HTMLElement ) {
|
||||
|
||||
if ( ev.target.classList.contains( "olympus-dialog-close" ) ) {
|
||||
ev.target.closest( "div.olympus-dialog" )?.classList.add( "hide" );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/*** ATC ***/
|
||||
|
||||
let atcFeatureSwitch = featureSwitches.getSwitch( "atc" );
|
||||
|
||||
if ( atcFeatureSwitch?.isEnabled() ) {
|
||||
|
||||
atc = new ATC();
|
||||
|
||||
atcToggleButton = new Button( "atc-toggle-button", [ "images/buttons/atc.svg" ], () => {
|
||||
atc.toggleStatus();
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
|
||||
|
||||
/* Default values */
|
||||
@ -59,12 +146,15 @@ function setup() {
|
||||
|
||||
function requestUpdate() {
|
||||
getDataFromDCS(update);
|
||||
|
||||
/* Main update rate = 250ms is minimum time, equal to server update time. */
|
||||
setTimeout(() => requestUpdate(), getConnected() ? 250 : 1000);
|
||||
|
||||
connectionStatusPanel.update(getConnected());
|
||||
}
|
||||
|
||||
export function update(data: JSON) {
|
||||
console.log( data );
|
||||
unitsManager.update(data);
|
||||
missionData.update(data);
|
||||
logPanel.update(data);
|
||||
@ -118,4 +208,9 @@ export function getConnected() {
|
||||
return connected;
|
||||
}
|
||||
|
||||
export function getUnitControlSliders() {
|
||||
return {altitude: altitudeSlider, airspeed: airspeedSlider}
|
||||
}
|
||||
|
||||
|
||||
window.onload = setup;
|
||||
@ -1,3 +1,37 @@
|
||||
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||
const φ1 = deg2rad(lat1); // φ, λ in radians
|
||||
const φ2 = deg2rad(lat2);
|
||||
const λ1 = deg2rad(lon1); // φ, λ in radians
|
||||
const λ2 = deg2rad(lon2);
|
||||
const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
|
||||
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
|
||||
const θ = Math.atan2(y, x);
|
||||
const brng = (rad2deg(θ) + 360) % 360; // in degrees
|
||||
|
||||
return brng;
|
||||
}
|
||||
|
||||
|
||||
export function ConvertDDToDMS(D: number, lng: boolean) {
|
||||
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
|
||||
var deg = 0 | (D < 0 ? (D = -D) : D);
|
||||
var min = 0 | (((D += 1e-9) % 1) * 60);
|
||||
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
|
||||
var dec = Math.round((sec - Math.floor(sec)) * 100);
|
||||
var sec = Math.floor(sec);
|
||||
if (lng)
|
||||
return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
|
||||
else
|
||||
return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
|
||||
}
|
||||
|
||||
|
||||
export function deg2rad(deg: number) {
|
||||
var pi = Math.PI;
|
||||
return deg * (pi / 180);
|
||||
}
|
||||
|
||||
|
||||
export function distance(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||
const R = 6371e3; // metres
|
||||
const φ1 = deg2rad(lat1); // φ, λ in radians
|
||||
@ -13,27 +47,24 @@ export function distance(lat1: number, lon1: number, lat2: number, lon2: number)
|
||||
return d;
|
||||
}
|
||||
|
||||
export function bearing(lat1: number, lon1: number, lat2: number, lon2: number) {
|
||||
const φ1 = deg2rad(lat1); // φ, λ in radians
|
||||
const φ2 = deg2rad(lat2);
|
||||
const λ1 = deg2rad(lon1); // φ, λ in radians
|
||||
const λ2 = deg2rad(lon2);
|
||||
const y = Math.sin(λ2 - λ1) * Math.cos(φ2);
|
||||
const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(λ2 - λ1);
|
||||
const θ = Math.atan2(y, x);
|
||||
const brng = (rad2deg(θ) + 360) % 360; // in degrees
|
||||
|
||||
return brng;
|
||||
export function rad2deg(rad: number) {
|
||||
var pi = Math.PI;
|
||||
return rad / (pi / 180);
|
||||
}
|
||||
|
||||
export const zeroPad = function (num: number, places: number) {
|
||||
var string = String(num);
|
||||
while (string.length < places) {
|
||||
string += "0";
|
||||
|
||||
export function reciprocalHeading( heading:number ): number {
|
||||
|
||||
if ( heading > 180 ) {
|
||||
return heading - 180;
|
||||
}
|
||||
return string;
|
||||
|
||||
return heading + 180;
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const zeroAppend = function (num: number, places: number) {
|
||||
var string = String(num);
|
||||
while (string.length < places) {
|
||||
@ -42,25 +73,11 @@ export const zeroAppend = function (num: number, places: number) {
|
||||
return string;
|
||||
}
|
||||
|
||||
export function ConvertDDToDMS(D: number, lng: boolean) {
|
||||
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
|
||||
var deg = 0 | (D < 0 ? (D = -D) : D);
|
||||
var min = 0 | (((D += 1e-9) % 1) * 60);
|
||||
var sec = (0 | (((D * 60) % 1) * 6000)) / 100;
|
||||
var dec = Math.round((sec - Math.floor(sec)) * 100);
|
||||
var sec = Math.floor(sec);
|
||||
if (lng)
|
||||
return dir + zeroPad(deg, 3) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
|
||||
else
|
||||
return dir + zeroPad(deg, 2) + "°" + zeroPad(min, 2) + "'" + zeroPad(sec, 2) + "." + zeroPad(dec, 2) + "\"";
|
||||
}
|
||||
|
||||
export function deg2rad(deg: number) {
|
||||
var pi = Math.PI;
|
||||
return deg * (pi / 180);
|
||||
}
|
||||
|
||||
export function rad2deg(rad: number) {
|
||||
var pi = Math.PI;
|
||||
return rad / (pi / 180);
|
||||
export const zeroPad = function (num: number, places: number) {
|
||||
var string = String(num);
|
||||
while (string.length < places) {
|
||||
string += "0";
|
||||
}
|
||||
return string;
|
||||
}
|
||||
17
client/views/aiccontrolpanel.ejs
Normal file
@ -0,0 +1,17 @@
|
||||
<div class="ol-panel aic-panel" id="aic-control-panel" data-feature-switch="aic">
|
||||
<div class="olympus-button" id="toggle-aic-button"></div>
|
||||
<div class="olympus-button" id="aic-help-button"></div>
|
||||
</div>
|
||||
|
||||
<div id="aic-help" class="olympus-dialog hide" data-feature-switch="aic">
|
||||
<div class="olympus-dialog-close">×</div>
|
||||
<div class="olympus-dialog-header">AIC Help</div>
|
||||
<div class="olympus-dialog-content">
|
||||
<p>How to be a good AIC and get people to do stuff good, too.</p>
|
||||
<div style="align-items: center; background:black; color:white; display:flex; height:250px; justify-content: center; justify-self: center; width:450px;">
|
||||
<div>[DCS with Volvo video]</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aic-teleprompt"></div>
|
||||
33
client/views/aicformationpanel.ejs
Normal file
@ -0,0 +1,33 @@
|
||||
<div id="aic-callsign-panel" class="aic-panel" data-feature-switch="aic">
|
||||
|
||||
<div class="aic-panel">
|
||||
<h2>My callsign</h2>
|
||||
<div>Magic</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div id="aic-toolbox" class="aic-panel" data-feature-switch="aic">
|
||||
|
||||
<div id="aic-control-type" class="aic-toolbox-panel">
|
||||
<h2>Control</h2>
|
||||
<div>
|
||||
<input type="radio" name="control-type" id="control-type-broadcast" value="broadcast" checked="checked" />
|
||||
<label for="control-type-broadcast">Broadcast</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" name="control-type" id="control-type-tactical" value="tactical" />
|
||||
<label for="control-type-tactical">Tactical</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aic-formation-panel" class="aic-toolbox-panel">
|
||||
|
||||
<h2>Formations</h2>
|
||||
|
||||
<div id="aic-formation-list"></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
105
client/views/atc.ejs
Normal file
@ -0,0 +1,105 @@
|
||||
<div id="atc-control-panel" data-feature-switch="atc">
|
||||
<div class="ol-button" id="atc-toggle-button"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="atc-flight-list" class="atc-tool hide" data-feature-switch="atc">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Flight</th>
|
||||
<th>T/O</th>
|
||||
<th>TTG</th>
|
||||
<th>Status</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="atc-flight-list-table-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="atc-strip-board" data-feature-switch="atc">
|
||||
|
||||
<div class="atc-strip-board-header">
|
||||
<div class="name">Name</div>
|
||||
<div class="bearing-range">BR</div>
|
||||
<div class="target-altitude">t. Alt</div>
|
||||
<div class="current-altitude">Alt</div>
|
||||
<div class="target-speed">t. Spd</div>
|
||||
<div class="current-speed">Speed</div>
|
||||
<div class="runway">RWY</div>
|
||||
<div class="line">Line</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="atc-strip-board-arrivals" class="atc-strip-board-strips ol-panel">
|
||||
<div class="atc-strip-board-strip">
|
||||
<div class="handle"></div>
|
||||
<div class="rectangular-container">
|
||||
<div class="name">Shark 3</div>
|
||||
<div class="bearing-range">250 / 28</div>
|
||||
<div class="target-altitude">-</div>
|
||||
<div class="current-altitude">10000</div>
|
||||
<div class="target-speed">-</div>
|
||||
<div class="current-speed">421</div>
|
||||
<div class="runway">-</div>
|
||||
<div class="line">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atc-strip-board-strip">
|
||||
<div class="handle"></div>
|
||||
<div class="rectangular-container">
|
||||
<div class="name">Shark 2</div>
|
||||
<div class="bearing-range">250 / 24</div>
|
||||
<div class="target-altitude">6000</div>
|
||||
<div class="current-altitude">6000</div>
|
||||
<div class="target-speed">-</div>
|
||||
<div class="current-speed">400</div>
|
||||
<div class="runway">-</div>
|
||||
<div class="line">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atc-strip-board-strip">
|
||||
<div class="handle"></div>
|
||||
<div class="rectangular-container">
|
||||
<div class="name">Shark 1</div>
|
||||
<div class="bearing-range link-warning">262 / 12</div>
|
||||
<div class="target-altitude">5000</div>
|
||||
<div class="current-altitude">5100</div>
|
||||
<div class="target-speed">-</div>
|
||||
<div class="current-speed">367</div>
|
||||
<div class="runway warning">-</div>
|
||||
<div class="line">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atc-strip-board-strip">
|
||||
<div class="handle"></div>
|
||||
<div class="rectangular-container">
|
||||
<div class="name">Dolphin 1</div>
|
||||
<div class="bearing-range">250 / 4</div>
|
||||
<div class="target-altitude link-warning">3000</div>
|
||||
<div class="current-altitude warning">4100</div>
|
||||
<div class="target-speed">-</div>
|
||||
<div class="current-speed">511</div>
|
||||
<div class="runway">25L</div>
|
||||
<div class="line">2nd</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="atc-strip-board-strip">
|
||||
<div class="handle"></div>
|
||||
<div class="rectangular-container">
|
||||
<div class="name">Whale 1</div>
|
||||
<div class="bearing-range">070 / 2</div>
|
||||
<div class="target-altitude">1500</div>
|
||||
<div class="current-altitude">1650</div>
|
||||
<div class="target-speed link-warning">350</div>
|
||||
<div class="current-speed warning">312</div>
|
||||
<div class="runway">25L</div>
|
||||
<div class="line">1st</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
@ -7,6 +7,17 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;600&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Google tag (gtag.js) -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-Z4L2TC3YX0"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-Z4L2TC3YX0');
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@ -20,6 +31,12 @@
|
||||
<%- include('visibilitycontrolpanel.ejs') %>
|
||||
<%- include('connectionstatuspanel.ejs') %>
|
||||
<%- include('mouseinfopanel.ejs') %>
|
||||
|
||||
<%- include('aiccontrolpanel.ejs') %>
|
||||
<%- include('aicformationpanel.ejs') %>
|
||||
|
||||
<%- include( 'atc.ejs' ) %>
|
||||
|
||||
<%- include('logpanel.ejs') %>
|
||||
<script src="javascripts/bundle.js"></script>
|
||||
</body>
|
||||
|
||||
@ -3,14 +3,15 @@
|
||||
<div id="ol-title-label">Selected units</div>
|
||||
|
||||
<div id="selected-units-container" class="ol-scrollable">
|
||||
-->
|
||||
<!-- This is where all the unit selection buttons will be shown-->
|
||||
<!--
|
||||
</div>
|
||||
|
||||
|
||||
<div id="formation-creation-container">
|
||||
<div class="ol-rectangular-button white" id="create-formation"><img src="images\buttons\create.svg">Create formation</div>
|
||||
<div class="ol-rectangular-button white" id="undo-formation"><img src="images\buttons\erase.svg">Undo formation</div>
|
||||
<div id="formation-creation-container" data-feature-switch="ai-formations">
|
||||
<div class="rectangular-button white" id="create-formation"><img src="images\buttons\create.svg">Create formation</div>
|
||||
<div class="rectangular-button white" id="undo-formation"><img src="images\buttons\erase.svg">Undo formation</div>
|
||||
</div>
|
||||
<div class="ol-hl"></div>
|
||||
-->
|
||||
|
||||
1004
package-lock.json
generated
Normal file
7
package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.7",
|
||||
"mocha": "^10.2.0",
|
||||
"sortablejs": "^1.15.0"
|
||||
}
|
||||
}
|
||||
@ -77,9 +77,11 @@ function Olympus.setMissionData(arg, time)
|
||||
basesData[i] = info
|
||||
end
|
||||
|
||||
|
||||
|
||||
-- Assemble missionData table
|
||||
missionData["bullseye"] = bullseye
|
||||
missionData["unitsData"] = unitsData
|
||||
missionData["unitsData"] = unitsData
|
||||
missionData["airbases"] = basesData
|
||||
|
||||
local command = "Olympus.missionData = " .. Olympus.serializeTable(missionData) .. "\n" .. "Olympus.OlympusDLL.setMissionData()"
|
||||
|
||||