Merge pull request #851 from Pax1601/camera-control

Implemented camera control from Olympus to DCS
This commit is contained in:
Pax1601 2024-02-29 12:22:45 +01:00 committed by GitHub
commit a84e190548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
56 changed files with 1655 additions and 183 deletions

3
.gitignore vendored
View File

@ -36,3 +36,6 @@ package-lock.json
/frontend/setup
frontend/server/public/plugins/controltipsplugin/index.js
frontend/website/plugins/controltips/index.js
/frontend/server/public/maps
*.pyc
/scripts/**/*.jpg

View File

@ -15,7 +15,7 @@ module.exports = function (configLocation) {
var indexRouter = require('./routes/index');
var uikitRouter = require('./routes/uikit');
var usersRouter = require('./routes/users');
var resourcesRouter = require('./routes/resources');
var resourcesRouter = require('./routes/resources')(configLocation);
var pluginsRouter = require('./routes/plugins');
/* Load the config and create the express app */

View File

@ -23,9 +23,10 @@
"regedit": "^5.1.2",
"save": "^2.9.0",
"sha256": "^0.2.0",
"sharp": "^0.33.2",
"srtm-elevation": "^2.1.2",
"tcp-ping-port": "^1.0.1",
"uuid": "^9.0.1",
"yargs": "^17.7.2"
}
}
}

View File

@ -64,7 +64,7 @@
width: 20px;
}
#toolbar-container>*:nth-child(3)>svg {
#command-mode-toolbar>svg {
display: none;
}

View File

@ -330,7 +330,7 @@ body.feature-forceShowUnitControlPanel #unit-control-panel {
#advanced-settings-div>button {
background-color: var(--background-grey);
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.25);
/*box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.25);*/
font-size: 13px;
height: 40px;
padding: 0 20px;

View File

@ -177,7 +177,7 @@ button svg.fill-coalition[data-coalition="red"] * {
.ol-select>.ol-select-value {
align-content: center;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
/*box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);*/
cursor: pointer;
display: flex;
justify-content: left;
@ -279,7 +279,7 @@ button svg.fill-coalition[data-coalition="red"] * {
}
.ol-select>.ol-select-options div hr {
background-color: white;
background-color: var(--background-hover);
height: 1px;
width: 100%;
}
@ -408,10 +408,8 @@ nav.ol-panel {
display: flex;
flex-direction: row;
height: 58px;
}
nav.ol-panel> :last-child {
margin-right: 5px;
padding-left: 15px;
padding-right: 15px;
}
.ol-panel .ol-group {
@ -654,9 +652,9 @@ nav.ol-panel> :last-child {
align-items: center;
}
.ol-navbar-buttons-group > div {
.ol-navbar-buttons-group>div {
align-items: center;
display:flex;
display: flex;
flex-direction: row;
}
@ -683,9 +681,16 @@ nav.ol-panel> :last-child {
border: 1px solid white;
}
.ol-navbar-buttons-group button.off svg * {
fill: white !important; /* Higher price than the Soul Stone but inline styling is causing issues. */
stroke: white !important; /* I'm sorry, daughter. */
.ol-navbar-buttons-group button.off svg *[fill="black"] {
fill: white !important;
}
.ol-navbar-buttons-group button.off svg *[stroke="black"] {
stroke: white !important;
}
.ol-navbar-buttons-group button.red svg *[fill="black"] {
fill: red !important;
}
.ol-navbar-buttons-group button svg * {
@ -696,53 +701,54 @@ nav.ol-panel> :last-child {
.ol-navbar-buttons-group .protectable button:first-of-type {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
width:28px;
width: 28px;
}
.ol-navbar-buttons-group > .protectable > button.lock {
.ol-navbar-buttons-group>.protectable>button.lock {
align-items: center;
background-color: var(--primary-red);
border-bottom-left-radius: 0;
border-top-left-radius: 0;
display:flex;
display: flex;
justify-content: center;
width:18px;
width: 18px;
}
.ol-navbar-buttons-group > .protectable > button[data-protected].lock {
.ol-navbar-buttons-group>.protectable>button[data-protected].lock {
background-color: var(--background-grey);
}
.ol-navbar-buttons-group > .protectable > button.lock svg {
height:10px;
width:10px;
.ol-navbar-buttons-group>.protectable>button.lock svg {
height: 10px;
width: 10px;
}
@keyframes lock-prompt {
100% {
opacity: 1;
}
0% {
opacity: 0;
}
}
.ol-navbar-buttons-group > .protectable > button[data-protected].lock.prompt svg {
.ol-navbar-buttons-group>.protectable>button[data-protected].lock.prompt svg {
animation: lock-prompt .25s alternate infinite;
}
.ol-navbar-buttons-group > .protectable > button.lock svg.locked * {
fill:white !important;
.ol-navbar-buttons-group>.protectable>button.lock svg.locked * {
fill: white !important;
}
.ol-navbar-buttons-group > .protectable > button:not([data-protected]).lock svg.unlocked,
.ol-navbar-buttons-group > .protectable > button[data-protected].lock svg.locked {
display:flex;
.ol-navbar-buttons-group>.protectable>button:not([data-protected]).lock svg.unlocked,
.ol-navbar-buttons-group>.protectable>button[data-protected].lock svg.locked {
display: flex;
}
.ol-navbar-buttons-group > .protectable > button[data-protected].lock svg.unlocked,
.ol-navbar-buttons-group > .protectable > button:not([data-protected]).lock svg.locked {
display:none;
.ol-navbar-buttons-group>.protectable>button[data-protected].lock svg.unlocked,
.ol-navbar-buttons-group>.protectable>button:not([data-protected]).lock svg.locked {
display: none;
}
@ -750,8 +756,7 @@ nav.ol-panel> :last-child {
#roe-buttons-container button,
#reaction-to-threat-buttons-container button,
#emissions-countermeasures-buttons-container button,
#shots-scatter-buttons-container button
#shots-intensity-buttons-container button {
#shots-scatter-buttons-container button #shots-intensity-buttons-container button {
align-items: center;
background-color: transparent;
border: 1px solid var(--accent-light-blue);
@ -834,7 +839,7 @@ nav.ol-panel> :last-child {
width: auto;
}
}
#splash-content::after {
background-color: var(--background-steel);
content: "";
@ -1030,7 +1035,7 @@ nav.ol-panel> :last-child {
font-size: 14px;
font-weight: bolder;
padding-left: 10px;
margin-left: -11px;
margin-left: -16px;
margin-top: -0px;
margin-bottom: -0px;
height: 58px;
@ -1068,15 +1073,8 @@ nav.ol-panel> :last-child {
#spawn-points-container {
height: 100%;
border-right: 1px solid gray;
display: flex;
align-items: center;
padding-right: 20px;
}
#command-mode-phase::before {
content: "Time to start";
font-size: 14px;
}
#command-mode-phase.setup-phase::after {
@ -1085,7 +1083,6 @@ nav.ol-panel> :last-child {
border-radius: 999px;
padding: 5px 10px;
background-color: var(--background-grey);
margin-left: 15px;
content: attr(data-remaining-time);
font-size: 14px;
}
@ -1099,20 +1096,17 @@ nav.ol-panel> :last-child {
display: flex;
flex-direction: column;
align-items: center;
}
#command-mode-phase.game-commenced::before {
content: "Game commenced";
font-weight: bold;
justify-content: center;
height: 100%;
}
#command-mode-phase.game-commenced::after {
content: "Spawn restrictions are being enforced";
font-size: 10px;
content: "Spawn restrictions on";
font-size: 12px;
}
#command-mode-phase.no-restrictions::after {
content: "No spawn restrictions";
content: "Spawn restrictions on";
font-size: 10px;
}
@ -1327,8 +1321,8 @@ dl.ol-data-grid dd {
.ol-dialog-content table th {
background-color: var(--background-grey);
color:white;
font-size:14px;
color: white;
font-size: 14px;
font-weight: normal;
}
@ -1349,7 +1343,8 @@ dl.ol-data-grid dd {
overflow-y: auto;
}
.ol-checkbox label {
.ol-checkbox label,
.ol-text-input label {
align-items: center;
cursor: pointer;
display: flex;
@ -1357,6 +1352,11 @@ dl.ol-data-grid dd {
white-space: nowrap;
}
.ol-text-input label {
justify-content: space-between;
width: 100%;
}
.ol-checkbox input[type="checkbox"] {
appearance: none;
background-color: transparent;
@ -1388,16 +1388,19 @@ dl.ol-data-grid dd {
.ol-text-input input {
background-color: var(--background-grey);
border: 1px solid var(--background-grey);
border-radius: 5px;
border-radius: var(--border-radius-sm);
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
/*box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);*/
color: var(--background-offwhite);
height: 32px;
text-align: center;
}
.ol-text-input.border input {
border: 1px solid var(--background-offwhite);
}
.ol-text-input input[disabled] {
color:var(--ol-dialog-disabled-text-color);
color: var(--ol-dialog-disabled-text-color);
}
input[type=number] {
@ -1441,7 +1444,7 @@ input[type=number]::-webkit-outer-spin-button {
.ol-button-apply[disabled] {
border-color: var(--ol-dialog-disabled-text-color);
color:var(--ol-dialog-disabled-text-color);
color: var(--ol-dialog-disabled-text-color);
}
.ol-button-apply::before {
@ -1525,49 +1528,49 @@ input[type=number]::-webkit-outer-spin-button {
}
.switch-control.yes-no .ol-switch[data-value="false"] .ol-switch-fill {
background-color: var(--ol-switch-off);
background-color: var(--ol-switch-off);
}
.switch-control.yes-no .ol-switch[data-value="undefined"] .ol-switch-fill {
background-color: var(--ol-switch-undefined);
background-color: var(--ol-switch-undefined);
}
.switch-control.coalition .ol-switch>.ol-switch-fill::before,
.switch-control.yes-no .ol-switch>.ol-switch-fill::before {
translate:-100% 0;
translate: -100% 0;
transform: none;
}
.switch-control.yes-no .ol-switch[data-value="true"]>.ol-switch-fill::before {
content: "YES";
content: "YES";
}
.switch-control.yes-no .ol-switch[data-value="false"]>.ol-switch-fill::before {
content: "NO";
content: "NO";
}
.switch-control.coalition [data-value="true"] .ol-switch-fill {
background-color: var(--primary-blue);
background-color: var(--primary-blue);
}
.switch-control.coalition [data-value="false"] .ol-switch-fill {
background-color: var(--primary-red);
background-color: var(--primary-red);
}
.switch-control.coalition [data-value="undefined"] .ol-switch-fill {
background-color: var(--primary-neutral);
background-color: var(--primary-neutral);
}
.switch-control.coalition [data-value="true"] .ol-switch-fill::before {
content: "BLUE";
content: "BLUE";
}
.switch-control.coalition [data-value="false"] .ol-switch-fill::before {
content: "RED";
content: "RED";
}
.switch-control.no-label [data-value] .ol-switch-fill::before {
content:"";
content: "";
}
.ol-context-menu>ul {
@ -1628,17 +1631,17 @@ input[type=number]::-webkit-outer-spin-button {
}
#map-visibility-options .ol-select-options .ol-checkbox {
font-size:13px;
font-weight:400;
padding:6px 15px;
font-size: 13px;
font-weight: 400;
padding: 6px 15px;
}
#map-visibility-options .ol-select-options .ol-checkbox:first-of-type {
padding-top:12px;
padding-top: 12px;
}
#map-visibility-options .ol-select-options .ol-checkbox:last-of-type {
padding-bottom:12px;
padding-bottom: 18px;
}
#map-visibility-options .ol-select-options .ol-checkbox label:hover span {
@ -1658,19 +1661,19 @@ input[type=number]::-webkit-outer-spin-button {
}
.file-import-export .ol-dialog-content {
display:flex;
display: flex;
flex-direction: column;
justify-content: center;
}
.file-import-export p {
background-color: var(--background-grey);
border-left:6px solid var(--secondary-blue-text);
padding:12px;
border-left: 6px solid var(--secondary-blue-text);
padding: 12px;
}
.file-import-export th {
padding:4px 6px;
padding: 4px 6px;
}
.file-import-export tr td:first-child {
@ -1678,12 +1681,12 @@ input[type=number]::-webkit-outer-spin-button {
}
.file-import-export td {
color:white;
color: white;
text-align: center;
}
.file-import-export .ol-checkbox {
display:flex;
display: flex;
justify-content: center;
}
@ -1692,7 +1695,7 @@ input[type=number]::-webkit-outer-spin-button {
}
.file-import-export .ol-checkbox span {
display:none;
display: none;
}
.file-import-export button.start-transfer {
@ -1732,7 +1735,7 @@ input[type=number]::-webkit-outer-spin-button {
pointer-events: none;
}
.file-import-export .ol-dialog-footer button:first-of-type{
.file-import-export .ol-dialog-footer button:first-of-type {
margin-left: auto;
}
@ -1742,3 +1745,31 @@ input[type=number]::-webkit-outer-spin-button {
}
}
#camera-link-type-switch {
width: 60px;
height: 25px;
}
#camera-link-type-switch[data-value="true"]>.ol-switch-fill::before {
content: "MAP";
}
#camera-link-type-switch[data-value="false"]>.ol-switch-fill::before {
content: "LIVE";
}
#camera-link-type-switch[data-value="true"]>.ol-switch-fill {
background-color: var(--background-grey);
}
#camera-link-type-switch[data-value="false"]>.ol-switch-fill {
background-color: var(--background-offwhite);
}
#camera-link-type-switch[data-value="false"]>.ol-switch-fill::before {
color: var(--background-steel);
}
#camera-link-type-switch[data-value="false"]>.ol-switch-fill::after {
background-color: var(--background-steel);
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 16 16"
version="1.1"
id="svg1"
sodipodi:docname="linked.svg"
xml:space="preserve"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
width="16"
height="16"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="12.703125"
inkscape:cx="15.940959"
inkscape:cy="19.01107"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path
d="m 14.718784,8.2555996 c 1.471866,-1.4718688 1.471866,-3.8555134 0,-5.3273791 C 13.416246,1.6256822 11.363445,1.4563517 9.8655269,2.5270381 l -0.041681,0.028645 C 9.4487143,2.8240079 9.3627479,3.3450208 9.63107,3.7175489 c 0.2683236,0.372525 0.789337,0.4610992 1.161864,0.1927748 l 0.04169,-0.028645 c 0.83623,-0.5965628 1.97986,-0.5027818 2.70407,0.2240338 0.820598,0.8205996 0.820598,2.1491875 0,2.9697871 l -2.922893,2.9281044 c -0.8206004,0.8206 -2.1491882,0.8206 -2.969788,0 C 6.9191889,9.2767887 6.8254045,8.1331608 7.4219671,7.2995366 l 0.028647,-0.041682 C 7.7189395,6.8827242 7.630369,6.3617082 7.2578404,6.0959918 6.8853158,5.8302724 6.3616949,5.9162417 6.0959777,6.2887668 l -0.028647,0.041682 C 4.9940392,7.8257624 5.1633693,9.8785611 6.4659076,11.1811 c 1.4718694,1.471869 3.8555134,1.471869 5.3273794,0 z M 1.1828078,7.6460126 c -1.47186608,1.4718659 -1.47186608,3.8555104 0,5.3273784 1.3025385,1.302539 3.3553399,1.471869 4.8532579,0.401182 l 0.041681,-0.02865 C 6.4528799,13.077605 6.5388455,12.556589 6.2705243,12.184063 6.0021998,11.811539 5.481187,11.722964 5.1086592,11.991289 l -0.041681,0.02865 c -0.8362262,0.596562 -1.9798574,0.502778 -2.7040679,-0.224038 -0.8205997,-0.823205 -0.8205997,-2.1517921 0,-2.9723919 L 5.2858059,5.8980077 c 0.8205997,-0.8205966 2.1491875,-0.8205966 2.9697873,0 0.7268153,0.7268156 0.8205997,1.8704464 0.2240372,2.7066757 L 8.450982,8.6463657 C 8.1826613,9.0214958 8.2712326,9.5425118 8.643758,9.8082277 9.0162857,10.073945 9.5399067,9.9879784 9.805623,9.6154532 L 9.83427,9.5737712 c 1.073291,-1.4979187 0.90396,-3.5507171 -0.3985777,-4.8532555 -1.4718686,-1.4718689 -3.8555132,-1.4718689 -5.3273817,0 z"
id="path1-3"
style="fill-opacity:1;stroke:none;stroke-width:0.0320135;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
fill="black" /></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -50,27 +50,26 @@
<path
d="m 10.664,18.4362 c 4.2922,0 7.7717,-3.4796 7.7717,-7.7717 0,-4.2921 -3.4796,-7.7717 -7.7717,-7.7717 -4.2921,0 -7.7717,3.4796 -7.7717,7.7717 0,4.2921 3.4796,7.7717 7.7717,7.7717 z m 0,2.8918 c 5.8893,0 10.664,-4.7742 10.664,-10.664 C 21.328,4.7742 16.5538,0 10.664,0 4.7742,0 0,4.7742 0,10.664 0,16.5538 4.7742,21.328 10.664,21.328 Z"
clip-rule="evenodd"
fill="#ffffff"
fill-rule="evenodd"
stroke-width="0"
style="fill:#000000"
fill="black"
id="path7220" />
<line
x1="7.796"
x2="13.187"
y1="6.8541994"
y2="14.231199"
stroke="#247BE2"
stroke="black"
stroke-linecap="square"
style="fill:none;stroke:#000000"
style="fill:none;"
id="line7222" />
<line
x1="14.397999"
x2="7.8680005"
y1="11.885201"
y2="11.450199"
stroke="#247BE2"
stroke="black"
stroke-linecap="square"
style="fill:none;stroke:#000000"
style="fill:none;"
id="line7224" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -36,6 +36,6 @@
<path
stroke-width="0"
d="m 13.1797,5.25 c 0.9297,0 2.5703,0.793 2.5703,1.75 0,0.9844 -1.6406,1.75 -2.5703,1.75 H 9.9805 L 7.2461,13.5625 C 7.082,13.8359 6.7812,14 6.4805,14 H 4.9492 C 4.6484,14 4.4297,13.7266 4.5117,13.4531 L 5.8516,8.75 H 3.0625 L 1.85938,10.3359 C 1.77734,10.4453 1.66797,10.5 1.53125,10.5 H 0.38281 C 0.16406,10.5 0,10.3359 0,10.1172 0,10.0898 0,10.0625 0,10.0352 L 0.875,7 0,3.9922 C 0,3.9648 0,3.9375 0,3.8828 0,3.6914 0.16406,3.5 0.38281,3.5 h 1.14844 c 0.13672,0 0.24609,0.082 0.32813,0.1914 L 3.0625,5.25 H 5.8516 L 4.5117,0.57422 C 4.4297,0.30078 4.6484,0 4.9492,0 H 6.4805 C 6.7812,0 7.082,0.19141 7.2461,0.46484 L 9.9805,5.25 Z"
fill="#202831"
fill="black"
id="path7232" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M0 224.2C0 100.6 100.2 0 224 0h24c95.2 0 181.2 69.3 197.3 160.2c2.3 13 6.8 25.7 15.1 36l42 52.6c6.2 7.8 9.6 17.4 9.6 27.4c0 24.2-19.6 43.8-43.8 43.8H448v64c0 35.3-28.7 64-64 64H320v32c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V407.3c0-16.7-6.9-32.5-17.1-45.8C16.6 322.4 0 274.1 0 224.2zM224 64c-8.8 0-16 7.2-16 16c0 33-39.9 49.5-63.2 26.2c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6C145.5 152.1 129 192 96 192c-8.8 0-16 7.2-16 16s7.2 16 16 16c33 0 49.5 39.9 26.2 63.2c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0C168.1 286.5 208 303 208 336c0 8.8 7.2 16 16 16s16-7.2 16-16c0-33 39.9-49.5 63.2-26.2c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6C302.5 263.9 319 224 352 224c8.8 0 16-7.2 16-16s-7.2-16-16-16c-33 0-49.5-39.9-26.2-63.2c6.2-6.2 6.2-16.4 0-22.6s-16.4-6.2-22.6 0C279.9 129.5 240 113 240 80c0-8.8-7.2-16-16-16zm-24 96a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm40 80a16 16 0 1 1 32 0 16 16 0 1 1 -32 0z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -52,5 +52,6 @@
inkscape:connector-curvature="0"
d="m 3.173155,1.0409694 c 0,-0.46651814 0.3476271,-0.84342412 0.7779063,-0.84342412 h 9.3348367 c 0.430278,0 0.777906,0.37690598 0.777906,0.84342412 0,0.4665187 -0.347628,0.8434202 -0.777906,0.8434202 H 9.3963807 V 3.571233 h 0.7779053 c 2.148956,0 3.889518,1.8871557 3.889518,4.2171087 v 1.686841 c 0,0.4665172 -0.347628,0.8434233 -0.777906,0.8434233 H 9.3963807 7.8405774 c -0.4886243,0 -0.9505011,-0.250391 -1.2446479,-0.6747365 L 4.8602324,7.1346873 C 4.7751488,7.0108098 4.6584662,6.9159274 4.5271922,6.8579429 L 1.2089503,5.4188529 C 0.97800953,5.318696 0.80298167,5.1025686 0.7422081,4.8389987 L 0.18309237,2.4088928 C 0.11988251,2.1426888 0.30706869,1.8843896 0.55988607,1.8843896 H 1.2283992 c 0.245524,0 0.4764649,0.1238773 0.6223221,0.3373706 L 2.7842066,3.571233 H 7.8405774 V 1.8843896 H 3.9510613 c -0.4302792,0 -0.7779063,-0.3769015 -0.7779063,-0.8434202 z m 6.2232257,7.5907923 h 3.1116153 v -0.84342 c 0,-1.3969171 -1.045307,-2.5302639 -2.33371,-2.5302639 H 9.3963807 Z m 5.9947173,2.7780193 c 0.303871,0.329462 0.303871,0.864511 0,1.193968 l -0.09481,0.102794 c -0.583429,0.632565 -1.375917,0.988385 -2.200006,0.988385 H 6.2847694 c -0.4302775,0 -0.7779054,-0.376906 -0.7779054,-0.843421 0,-0.466518 0.3476279,-0.843424 0.7779054,-0.843424 h 6.8115166 c 0.413259,0 0.809506,-0.176591 1.101221,-0.492874 l 0.09481,-0.102793 c 0.30387,-0.329461 0.79735,-0.329461 1.101219,0 z"
id="path1174-3"
style="fill:#202831;fill-opacity:1;stroke-width:0.02531246" />
style="fill-opacity:1;stroke-width:0.02531246"
fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -36,6 +36,6 @@
<path
stroke-width="0"
d="m 5.2724978,0.875 c 0,-0.46484 0.3828,-0.875 0.875,-0.875 h 3.5 C 10.112298,0 10.522498,0.41016 10.522498,0.875 V 1.75 h 1.3125 c 0.7109,0 1.3125,0.6016 1.3125,1.3125 v 3.5 l 1.2031,0.4102 c 0.6289,0.2187 0.793,1.039 0.3008,1.4765 l -2.7617,2.543 c -0.4375,0.2461 -0.9297,0.4101 -1.3672,0.4101 -0.5469002,0 -1.1211002,-0.2187 -1.6406002,-0.5468 -0.6016,-0.4375 -1.3946,-0.4375 -1.9961,0 -0.4649,0.3007 -1.0391,0.5468 -1.6406,0.5468 -0.4375,0 -0.9297,-0.164 -1.3672,-0.4101 l -2.76175,-2.543 C 0.62405778,8.0117 0.78811778,7.1914 1.4170278,6.9727 l 1.23047,-0.4102 v -3.5 c 0,-0.7109 0.5742,-1.3125 1.3125,-1.3125 h 1.3125 z m -0.875,5.1133 2.9258,-0.9844 c 0.3554,-0.1094 0.7656,-0.1094 1.1211,0 L 11.397498,5.9883 V 3.5 H 4.3974978 Z m 3.9922,5.5508 c 0.6289,0.4375 1.3672,0.7109 2.1328002,0.7109 0.7109,0 1.5039,-0.2734 2.1055,-0.7109 0.3281,-0.2188 0.7656,-0.1914 1.0664,0.0547 0.4101,0.3281 0.9023,0.5742 1.3945,0.6835 0.4648,0.1094 0.7656,0.5743 0.6563,1.0665 -0.1094,0.4648 -0.6016,0.7656 -1.0665,0.6562 -0.6562,-0.1641 -1.2304,-0.4648 -1.5859,-0.6836 -0.793,0.4102 -1.668,0.6836 -2.5703,0.6836 -0.8750002,0 -1.6680002,-0.2461 -2.2148002,-0.4922 -0.1641,-0.082 -0.3008,-0.164 -0.4102,-0.2187 -0.1367,0.0547 -0.2734,0.1367 -0.4375,0.2187 -0.5469,0.2461 -1.3398,0.4922 -2.1875,0.4922 -0.9023,0 -1.8047,-0.2734 -2.5977,-0.6836 -0.3554,0.2188 -0.92964,0.5195 -1.58589,0.6836 -0.46485002,0.1094 -0.95703002,-0.1914 -1.06641002,-0.6562 -0.10938,-0.4649 0.19141,-0.9571 0.65625,-1.0665 0.49219002,-0.1093 1.01172002,-0.3554 1.39455002,-0.6835 0.3008,-0.2461 0.7383,-0.2735 1.0664,-0.0547 0.6016,0.4375 1.3945,0.7109 2.1328,0.7109 0.7383,0 1.5039,-0.2734 2.1055,-0.7109 0.3007,-0.2188 0.7109,-0.2188 1.0117,0 z"
fill="#181e25"
fill="black"
id="path7277" />
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path fill="black" d="M0 224.2C0 100.6 100.2 0 224 0h24c95.2 0 181.2 69.3 197.3 160.2c2.3 13 6.8 25.7 15.1 36l42 52.6c6.2 7.8 9.6 17.4 9.6 27.4c0 24.2-19.6 43.8-43.8 43.8H448v64c0 35.3-28.7 64-64 64H320v32c0 17.7-14.3 32-32 32H96c-17.7 0-32-14.3-32-32V407.3c0-16.7-6.9-32.5-17.1-45.8C16.6 322.4 0 274.1 0 224.2zM224 64c-8.8 0-16 7.2-16 16c0 33-39.9 49.5-63.2 26.2c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6C145.5 152.1 129 192 96 192c-8.8 0-16 7.2-16 16s7.2 16 16 16c33 0 49.5 39.9 26.2 63.2c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0C168.1 286.5 208 303 208 336c0 8.8 7.2 16 16 16s16-7.2 16-16c0-33 39.9-49.5 63.2-26.2c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6C302.5 263.9 319 224 352 224c8.8 0 16-7.2 16-16s-7.2-16-16-16c-33 0-49.5-39.9-26.2-63.2c6.2-6.2 6.2-16.4 0-22.6s-16.4-6.2-22.6 0C279.9 129.5 240 113 240 80c0-8.8-7.2-16-16-16zm-24 96a24 24 0 1 1 0 48 24 24 0 1 1 0-48zm40 80a16 16 0 1 1 32 0 16 16 0 1 1 -32 0z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 576 512"
version="1.1"
id="svg1"
sodipodi:docname="slew.svg"
xml:space="preserve"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:zoom="0.79394531"
inkscape:cx="352.03936"
inkscape:cy="210.97171"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path
d="m 18.892988,123.94151 c 0,-33.293066 27.068299,-60.361356 60.361351,-60.361356 H 320.69975 c 33.29306,0 60.36135,27.06829 60.36135,60.361356 V 365.3869 c 0,33.29306 -27.06829,60.36135 -60.36135,60.36135 H 79.254339 c -33.293052,0 -60.361351,-27.06829 -60.361351,-60.36135 z M 546.20598,97.344781 c 9.80872,5.281629 15.93917,15.467609 15.93917,26.596729 V 365.3869 c 0,11.12912 -6.13045,21.3151 -15.93917,26.59673 -9.80872,5.28161 -21.69237,4.71572 -31.02951,-1.50904 l -90.54203,-60.36135 -13.39267,-8.9599 V 305.02555 184.30286 168.17507 l 13.39267,-8.9599 90.54203,-60.361354 c 9.24284,-6.130451 21.12646,-6.790646 31.02951,-1.509035 z"
id="path1"
style="stroke-width:0.943146"
fill="black" /></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,18 +1,34 @@
const express = require('express');
const sharp = require('sharp')
const fs = require('fs');
const pfs = require('fs/promises')
const router = express.Router();
router.get('/theme/*', function (req, res, next) {
var reqTheme = "olympus";
module.exports = function (configLocation) {
router.get('/theme/*', function (req, res, next) {
var reqTheme = "olympus";
/* Yes, this in an easter egg! :D Feel free to ignore it, or activate the parrot theme to check what it does. Why parrots? The story is a bit long, come to the Discord and ask :D */
if (reqTheme === "parrot" && !req.url.includes(".css"))
res.redirect('/themes/parrot/images/parrot.svg');
else
res.redirect(req.url.replace("theme", "themes/" + reqTheme));
});
router.put('/theme/:newTheme', function (req, res, next) {
res.end("Ok");
});
router.get('/config', function (req, res, next) {
if (fs.existsSync(configLocation)) {
let rawdata = fs.readFileSync(configLocation);
config = JSON.parse(rawdata);
res.send(JSON.stringify(config.frontend));
res.end()
} else {
res.sendStatus(404);
}
});
/* Yes, this in an easter egg! :D Feel free to ignore it, or activate the parrot theme to check what it does. Why parrots? The story is a bit long, come to the Discord and ask :D */
if (reqTheme === "parrot" && !req.url.includes(".css"))
res.redirect('/themes/parrot/images/parrot.svg');
else
res.redirect(req.url.replace("theme", "themes/" + reqTheme));
});
router.put('/theme/:newTheme', function (req, res, next) {
res.end("Ok");
});
module.exports = router;
return router;
}

View File

@ -3,5 +3,6 @@
<span id="command-mode"></span>
<div id="spawn-points-container">Spawn points<span id="spawn-points"></span></div>
<span id="command-mode-phase"></span>
<button id="command-mode-settings-button" class="ol-button" data-on-click="showCommandModeDialog"><img src="/resources/theme/images/icons/gears-solid.svg" inject-svg>Settings</button>
<button id="command-mode-settings-button" class="ol-button" data-on-click="showCommandModeDialog"><img
src="/resources/theme/images/icons/gears-solid.svg" inject-svg>Settings</button>
</nav>

View File

@ -6,7 +6,8 @@
<div class="ol-select-options">
<div id="toolbar-summary">
<h3>DCS Olympus</h3>
<div class="accent-green app-version-number">version {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}</div>
<div class="accent-green app-version-number">version {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}
</div>
</div>
<div>
<a href="https://discord.gg/wWXyVVBZT7" target="_blank">Discord</a>
@ -31,14 +32,16 @@
<div class="ol-group">
<div id="map-type" class="ol-select">
<div class="ol-select-value"><img src="resources/theme/images/icons/map-source.svg" inject-svg /><span class="ol-select-value-text">ArcGIS Satellite</span></div>
<div class="ol-select-value"><img src="resources/theme/images/icons/map-source.svg" inject-svg /><span
class="ol-select-value-text">ArcGIS Satellite</span></div>
<div class="ol-select-options">
<!-- Here the available map sources will be listed-->
</div>
</div>
<div id="map-visibility-options" class="ol-select">
<div class="ol-select-value"><img src="/resources/theme/images/icons/gears-solid.svg" inject-svg />Options</div>
<div class="ol-select-value"><img src="/resources/theme/images/icons/gears-solid.svg" inject-svg />Options
</div>
<div class="ol-select-options">
<!-- This is where the advanced visibility options will be listed -->
</div>
@ -57,17 +60,51 @@
<div id="coalition-visibility-control" class="ol-group ol-navbar-buttons-group">
<div>
<button id="coalition-visibility-control-blue" data-on-click="toggleCoalitionVisibility"
data-on-click-params='{ "coalition": "blue" }' title="Toggle Blue coalition visibility"><img src="/resources/theme/images/buttons/visibility/shield.svg" class="fill-coalition" data-coalition="blue" inject-svg /></button>
data-on-click-params='{ "coalition": "blue" }' title="Toggle Blue coalition visibility"><img
src="/resources/theme/images/buttons/visibility/shield.svg" class="fill-coalition"
data-coalition="blue" inject-svg /></button>
</div>
<div>
<button id="coalition-visibility-control-red" data-on-click="toggleCoalitionVisibility"
data-on-click-params='{ "coalition": "red" }' title="Toggle Red coalition visibility"><img src="/resources/theme/images/buttons/visibility/shield.svg" class="fill-coalition" data-coalition="red" inject-svg /></button>
data-on-click-params='{ "coalition": "red" }' title="Toggle Red coalition visibility"><img
src="/resources/theme/images/buttons/visibility/shield.svg" class="fill-coalition"
data-coalition="red" inject-svg /></button>
</div>
<div>
<button id="coalition-visibility-control-neutral" data-on-click="toggleCoalitionVisibility"
data-on-click-params='{ "coalition": "neutral" }' title="Toggle Neutral coalition visibility"><img src="/resources/theme/images/buttons/visibility/shield.svg" class="fill-coalition" data-coalition="neutral" inject-svg /></button>
data-on-click-params='{ "coalition": "neutral" }' title="Toggle Neutral coalition visibility"><img
src="/resources/theme/images/buttons/visibility/shield.svg" class="fill-coalition"
data-coalition="neutral" inject-svg /></button>
</div>
</div>
</nav>
<nav class="ol-panel" oncontextmenu="return false;">
<div id="camera-label" class="ol-panel-tab">
<img src="resources/theme/images/icons/camera.svg" inject-svg />
<span>Camera</span>
</div>
<div id="camera-control" class="ol-group ol-navbar-buttons-group">
<div>
<button class="off red" id="camera-link-control" data-on-click="toggleCameraLinkStatus"
title="Camera link to DCS is not available"><img src="/resources/theme/images/buttons/camera/linked.svg"
inject-svg /></button>
</div>
<div id="camera-link-type-switch" class="ol-switch"></div>
<!--
<div>
<button class="off" id="camera-slew-to-position" data-on-click="slewCameraToPosition" title="Camera link to DCS is not available"><img src="/resources/theme/images/buttons/camera/slew.svg" inject-svg /></button>
</div>
<div>
<button style="color: black" id="camera-link-control-map" data-on-click="toggleCameraLinkMapType"
title="Camera link to DCS is not available"><img src="/resources/theme/images/buttons/camera/F10.svg"
inject-svg /></button>
</div>
-->
</div>
</nav>

View File

@ -176,7 +176,7 @@ declare module "constants/constants" {
zoom: number;
};
};
export const mapLayers: {
export const defaultMapLayers: {
"ArcGIS Satellite": {
urlTemplate: string;
minZoom: number;
@ -235,6 +235,7 @@ declare module "constants/constants" {
export const SHOW_UNIT_CONTACTS = "Show selected units contact lines";
export const SHOW_UNIT_PATHS = "Show selected unit paths";
export const SHOW_UNIT_TARGETS = "Show selected unit targets";
export const DCS_LINK_PORT = "DCS Camera link port";
export enum DataIndexes {
startOfData = 0,
category = 1,
@ -362,6 +363,7 @@ declare module "controls/dropdown" {
setOptionsElements(optionsElements: HTMLElement[]): void;
getOptionElements(): HTMLCollection;
addOptionElement(optionElement: HTMLElement): void;
addHorizontalDivider(): void;
/** Select the active value of the dropdown
*
* @param idx The index of the element to select
@ -851,10 +853,11 @@ declare module "other/utils" {
export function enumToCoalition(coalitionID: number): "" | "blue" | "red" | "neutral";
export function coalitionToEnum(coalition: string): 0 | 1 | 2;
export function convertDateAndTimeToDate(dateAndTime: DateAndTime): Date;
export function createCheckboxOption(value: string, text: string, checked?: boolean, callback?: CallableFunction, options?: any): HTMLElement;
export function createCheckboxOption(text: string, description: string, checked?: boolean, callback?: CallableFunction, options?: any): HTMLElement;
export function getCheckboxOptions(dropdown: Dropdown): {
[key: string]: boolean;
};
export function createTextInputOption(text: string, description: string, initialValue: string, type: string, callback?: CallableFunction, options?: any): HTMLElement;
export function getGroundElevation(latlng: LatLng, callback: CallableFunction): void;
}
declare module "controls/slider" {
@ -1624,7 +1627,9 @@ declare module "map/map" {
* @param ID - the ID of the HTML element which will contain the context menu
*/
constructor(ID: string);
addVisibilityOption(option: string, defaultValue: boolean): void;
addVisibilityOption(option: string, defaultValue: boolean | number | string, options?: {
[key: string]: any;
}): void;
setLayer(layerName: string): void;
getLayers(): string[];
setState(state: string): void;
@ -1660,12 +1665,14 @@ declare module "map/map" {
getSelectedCoalitionArea(): CoalitionArea | undefined;
bringCoalitionAreaToBack(coalitionArea: CoalitionArea): void;
getVisibilityOptions(): {
[key: string]: boolean;
[key: string]: string | number | boolean;
};
isZooming(): boolean;
getPreviousZoom(): number;
getIsUnitProtected(unit: Unit): boolean;
getMapMarkerVisibilityControls(): MapMarkerVisibilityControl[];
setSlaveDCSCamera(newSlaveDCSCamera: boolean): void;
setCameraControlMode(newCameraControlMode: string): void;
}
}
declare module "mission/bullseye" {
@ -2612,6 +2619,7 @@ declare module "olympusapp" {
*/
setLoginStatus(status: string): void;
start(): void;
getConfig(): any;
}
}
declare module "index" {

View File

@ -154,7 +154,7 @@ export const mapBounds = {
"SinaiMap": { bounds: new LatLngBounds([34.312222, 28.523333], [25.946944, 36.897778]), zoom: 4 },
}
export const mapLayers = {
export const defaultMapLayers = {
"ArcGIS Satellite": {
urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
minZoom: 1,
@ -207,7 +207,7 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{
"toggles": ["human"],
"tooltip": "Toggle human players' visibility"
}, {
"image": "visibility/head-side-virus-solid.svg",
"image": "visibility/olympus.svg",
"isProtected": false,
"name": "Olympus",
"protectable": false,
@ -268,6 +268,7 @@ export const FILL_SELECTED_RING = "Fill the threat range rings of selected units
export const SHOW_UNIT_CONTACTS = "Show selected units contact lines";
export const SHOW_UNIT_PATHS = "Show selected unit paths";
export const SHOW_UNIT_TARGETS = "Show selected unit targets";
export const DCS_LINK_PORT = "DCS Camera link port";
export enum DataIndexes {
startOfData = 0,

View File

@ -117,6 +117,13 @@ export class Dropdown {
this.#options.appendChild(optionElement);
}
addHorizontalDivider() {
let div = document.createElement("div");
let hr = document.createElement('hr');
div.appendChild(hr);
this.#options.appendChild(div);
}
/** Select the active value of the dropdown
*
* @param idx The index of the element to select

View File

@ -7,12 +7,12 @@ import { AirbaseContextMenu } from "../contextmenus/airbasecontextmenu";
import { Dropdown } from "../controls/dropdown";
import { Airbase } from "../mission/airbase";
import { Unit } from "../unit/unit";
import { bearing, createCheckboxOption, polyContains } from "../other/utils";
import { bearing, createCheckboxOption, createTextInputOption, deg2rad, getGroundElevation, polyContains } from "../other/utils";
import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker";
import { TemporaryUnitMarker } from "./markers/temporaryunitmarker";
import { ClickableMiniMap } from "./clickableminimap";
import { SVGInjector } from '@tanem/svg-injector'
import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants";
import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS, DCS_LINK_PORT } from "../constants/constants";
import { CoalitionArea } from "./coalitionarea/coalitionarea";
import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu";
import { DrawingCursor } from "./coalitionarea/drawingcursor";
@ -70,6 +70,11 @@ export class Map extends L.Map {
#selecting: boolean = false;
#isZooming: boolean = false;
#previousZoom: number = 0;
#slaveDCSCamera: boolean = false;
#slaveDCSCameraAvailable: boolean = false;
#cameraControlTimer: number = 0;
#cameraControlPort: number = 3003;
#cameraControlMode: string = 'map';
#destinationGroupRotation: number = 0;
#computeDestinationRotation: boolean = false;
@ -90,10 +95,11 @@ export class Map extends L.Map {
#coalitionAreaContextMenu: CoalitionAreaContextMenu = new CoalitionAreaContextMenu("coalition-area-contextmenu");
#mapSourceDropdown: Dropdown;
#mapLayers: any = defaultMapLayers;
#mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS;
#mapVisibilityOptionsDropdown: Dropdown;
#optionButtons: { [key: string]: HTMLButtonElement[] } = {}
#visibilityOptions: { [key: string]: boolean } = {}
#visibilityOptions: { [key: string]: boolean | string | number } = {}
#hiddenTypes: string[] = [];
/**
@ -120,10 +126,10 @@ export class Map extends L.Map {
this.#ID = ID;
this.setLayer(Object.keys(mapLayers)[0]);
this.setLayer(Object.keys(this.#mapLayers)[0]);
/* Minimap */
var minimapLayer = new L.TileLayer(mapLayers[Object.keys(mapLayers)[0] as keyof typeof mapLayers].urlTemplate, { minZoom: 0, maxZoom: 13 });
var minimapLayer = new L.TileLayer(this.#mapLayers[Object.keys(this.#mapLayers)[0]].urlTemplate, { minZoom: 0, maxZoom: 13 });
this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]);
this.#miniMapPolyline = new L.Polyline([], { color: '#202831' });
this.#miniMapPolyline.addTo(this.#miniMapLayerGroup);
@ -157,6 +163,7 @@ export class Map extends L.Map {
this.on('drag', (e: any) => this.#onMouseMove(e));
this.on('keydown', (e: any) => this.#onKeyDown(e));
this.on('keyup', (e: any) => this.#onKeyUp(e));
this.on('move', (e: any) => { if (this.#slaveDCSCamera) this.#broadcastPosition() });
/* Event listeners */
document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => {
@ -200,8 +207,33 @@ export class Map extends L.Map {
document.addEventListener("mapOptionsChanged", () => {
this.getContainer().toggleAttribute("data-hide-labels", !this.getVisibilityOptions()[SHOW_UNIT_LABELS]);
this.#cameraControlPort = this.getVisibilityOptions()[DCS_LINK_PORT] as number;
});
document.addEventListener("configLoaded", () => {
let config = getApp().getConfig();
if (config.additionalMaps) {
let additionalMaps = config.additionalMaps;
this.#mapLayers = {
...this.#mapLayers,
...additionalMaps
}
this.#mapSourceDropdown.setOptions(this.getLayers());
}
})
document.addEventListener("toggleCameraLinkStatus", () => {
// if (this.#slaveDCSCameraAvailable) { // Commented to experiment with usability
this.setSlaveDCSCamera(!this.#slaveDCSCamera);
// }
})
document.addEventListener("slewCameraToPosition", () => {
// if (this.#slaveDCSCameraAvailable) { // Commented to experiment with usability
this.#broadcastPosition();
// }
})
/* Pan interval */
this.#panInterval = window.setInterval(() => {
if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft)
@ -209,10 +241,19 @@ export class Map extends L.Map {
((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta * (this.#shiftKey ? 3 : 1)));
}, 20);
/* Periodically check if the camera control endpoint is available */
this.#cameraControlTimer = window.setInterval(() => {
this.#checkCameraPort();
}, 1000)
/* Option buttons */
this.#createUnitMarkerControlButtons();
/* Create the checkboxes to select the advanced visibility options */
this.addVisibilityOption(DCS_LINK_PORT, 3003, { min: 1024, max: 65535 });
this.#mapVisibilityOptionsDropdown.addHorizontalDivider();
this.addVisibilityOption(SHOW_UNIT_CONTACTS, false);
this.addVisibilityOption(HIDE_GROUP_MEMBERS, true);
this.addVisibilityOption(SHOW_UNIT_PATHS, true);
@ -224,21 +265,28 @@ export class Map extends L.Map {
/* this.addVisibilityOption(FILL_SELECTED_RING, false); Removed since currently broken: TODO fix!*/
}
addVisibilityOption(option: string, defaultValue: boolean) {
addVisibilityOption(option: string, defaultValue: boolean | number | string, options?: { [key: string]: any }) {
this.#visibilityOptions[option] = defaultValue;
this.#mapVisibilityOptionsDropdown.addOptionElement(createCheckboxOption(option, option, defaultValue, (ev: any) => { this.#setVisibilityOption(option, ev); }));
if (typeof defaultValue === 'boolean')
this.#mapVisibilityOptionsDropdown.addOptionElement(createCheckboxOption(option, option, defaultValue as boolean, (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
else if (typeof defaultValue === 'number')
this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue.toString(), 'number', (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
else
this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue, 'text', (ev: any) => { this.#setVisibilityOption(option, ev); }, options));
}
setLayer(layerName: string) {
if (this.#layer != null)
this.removeLayer(this.#layer)
if (layerName in mapLayers) {
const layerData = mapLayers[layerName as keyof typeof mapLayers];
if (layerName in this.#mapLayers) {
const layerData = this.#mapLayers[layerName];
var options: L.TileLayerOptions = {
attribution: layerData.attribution,
minZoom: layerData.minZoom,
maxZoom: layerData.maxZoom
maxZoom: layerData.maxZoom,
minNativeZoom: layerData.minNativeZoom,
maxNativeZoom: layerData.maxNativeZoom
};
this.#layer = new L.TileLayer(layerData.urlTemplate, options);
}
@ -247,7 +295,7 @@ export class Map extends L.Map {
}
getLayers() {
return Object.keys(mapLayers);
return Object.keys(this.#mapLayers);
}
/* State machine */
@ -522,6 +570,22 @@ export class Map extends L.Map {
return this.#mapMarkerVisibilityControls;
}
setSlaveDCSCamera(newSlaveDCSCamera: boolean) {
// if (this.#slaveDCSCameraAvailable || !newSlaveDCSCamera) { // Commented to experiment with usability
this.#slaveDCSCamera = newSlaveDCSCamera;
let button = document.getElementById("camera-link-control");
button?.classList.toggle("off", !newSlaveDCSCamera);
if (newSlaveDCSCamera)
this.#broadcastPosition();
// }
}
setCameraControlMode(newCameraControlMode: string) {
this.#cameraControlMode = newCameraControlMode;
if (this.#slaveDCSCamera)
this.#broadcastPosition();
}
/* Event handlers */
#onClick(e: any) {
if (!this.#preventLeftClick) {
@ -704,6 +768,28 @@ export class Map extends L.Map {
this.#isZooming = false;
}
#broadcastPosition() {
getGroundElevation(this.getCenter(), (response: string) => {
var groundElevation: number | null = null;
try {
groundElevation = parseFloat(response);
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("PUT", `http://127.0.0.1:${this.#cameraControlPort}`);
xmlHttp.setRequestHeader("Content-Type", "application/json");
const C = 40075016.686;
let mpp = C * Math.cos(deg2rad(this.getCenter().lat)) / Math.pow(2, this.getZoom() + 8);
let d = mpp * 1920;
let alt = d / 2 * 1 / Math.tan(deg2rad(40));
if (alt > 100000)
alt = 100000;
xmlHttp.send(JSON.stringify({ lat: this.getCenter().lat, lng: this.getCenter().lng, alt: alt + groundElevation, mode: this.#cameraControlMode }));
} catch {
console.warn("broadcastPosition: could not retrieve ground elevation")
}
});
}
/* */
#panToUnit(unit: Unit) {
var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng);
@ -870,8 +956,47 @@ export class Map extends L.Map {
}
#setVisibilityOption(option: string, ev: any) {
this.#visibilityOptions[option] = ev.currentTarget.checked;
if (typeof this.#visibilityOptions[option] === 'boolean')
this.#visibilityOptions[option] = ev.currentTarget.checked;
else if (typeof this.#visibilityOptions[option] === 'number')
this.#visibilityOptions[option] = Number(ev.currentTarget.value);
else
this.#visibilityOptions[option] = ev.currentTarget.value;
document.dispatchEvent(new CustomEvent("mapOptionsChanged"));
}
#setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) {
this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable;
let linkButton = document.getElementById("camera-link-control");
if (linkButton) {
if (!newSlaveDCSCameraAvailable) {
//this.setSlaveDCSCamera(false); // Commented to experiment with usability
linkButton.classList.add("red");
linkButton.title = "Camera link to DCS is not available";
} else {
linkButton.classList.remove("red");
linkButton.title = "Link/Unlink DCS camera with Olympus position";
}
}
}
#checkCameraPort(){
var xmlHttp = new XMLHttpRequest();
xmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`);
xmlHttp.onload = (res: any) => {
if (xmlHttp.status == 200)
this.#setSlaveDCSCameraAvailable(true);
else
this.#setSlaveDCSCameraAvailable(false);
};
xmlHttp.onerror = (res: any) => {
this.#setSlaveDCSCameraAvailable(false);
}
xmlHttp.ontimeout = (res: any) => {
this.#setSlaveDCSCameraAvailable(false);
}
xmlHttp.timeout = 500;
xmlHttp.send("");
}
}

View File

@ -118,8 +118,8 @@ export class MissionManager {
}
commandModePhaseEl.classList.toggle("setup-phase", this.#remainingSetupTime > 0 && this.getCommandModeOptions().restrictSpawns);
commandModePhaseEl.classList.toggle("game-commenced", this.#remainingSetupTime <= 0 || !this.getCommandModeOptions().restrictSpawns);
commandModePhaseEl.classList.toggle("no-restrictions", !this.getCommandModeOptions().restrictSpawns);
//commandModePhaseEl.classList.toggle("game-commenced", this.#remainingSetupTime <= 0 || !this.getCommandModeOptions().restrictSpawns);
//commandModePhaseEl.classList.toggle("no-restrictions", !this.getCommandModeOptions().restrictSpawns);
}
}
}

View File

@ -33,6 +33,7 @@ export class OlympusApp {
/* Global data */
#activeCoalition: string = "blue";
#latestVersion: string|undefined = undefined;
#config: any = {};
/* Main leaflet map, extended by custom methods */
#map: Map | null = null;
@ -251,6 +252,19 @@ export class OlympusApp {
latestVersionSpan.classList.toggle("new-version", this.#latestVersion !== VERSION);
}
})
/* Load the config file from the server */
const configRequest = new Request(location.href + "resources/config");
fetch(configRequest).then((response) => {
if (response.status === 200) {
return response.json();
} else {
throw new Error("Error retrieving config file");
}
}).then((res) => {
this.#config = res;
document.dispatchEvent(new CustomEvent("configLoaded"));
})
}
#setupEvents() {
@ -446,4 +460,8 @@ export class OlympusApp {
img.addEventListener("load", () => { SVGInjector(img); });
})
}
getConfig() {
return this.#config;
}
}

View File

@ -463,7 +463,7 @@ export function convertDateAndTimeToDate(dateAndTime: DateAndTime) {
return new Date(year, month, date.Day, time.h, time.m, time.s);
}
export function createCheckboxOption(value: string, text: string, checked: boolean = true, callback: CallableFunction = (ev: any) => {}, options?:any) {
export function createCheckboxOption(text: string, description: string, checked: boolean = true, callback: CallableFunction = (ev: any) => {}, options?:any) {
options = {
"disabled": false,
"name": "",
@ -473,16 +473,15 @@ export function createCheckboxOption(value: string, text: string, checked: boole
var div = document.createElement("div");
div.classList.add("ol-checkbox");
var label = document.createElement("label");
label.title = text;
label.title = description;
var input = document.createElement("input");
input.type = "checkbox";
input.checked = checked;
input.name = options.name;
input.disabled = options.disabled;
input.readOnly = options.readOnly;
input.value = value;
var span = document.createElement("span");
span.innerText = value;
span.innerText = text;
label.appendChild(input);
label.appendChild(span);
div.appendChild(label);
@ -503,6 +502,45 @@ export function getCheckboxOptions(dropdown: Dropdown) {
return values;
}
export function createTextInputOption(text: string, description: string, initialValue: string, type: string, callback: CallableFunction = (ev: any) => {}, options?:any) {
options = {
"disabled": false,
"name": "",
"readOnly": false,
...options
};
var div = document.createElement("div");
div.classList.add("ol-text-input", "border");
var label = document.createElement("label");
label.title = description;
var input = document.createElement("input");
input.type = type;
input.name = options.name;
input.disabled = options.disabled;
input.readOnly = options.readOnly;
if (options.min)
input.min = options.min;
if (options.max)
input.max = options.max;
input.value = initialValue;
input.style.width = "80px";
var span = document.createElement("span");
span.innerText = text;
label.appendChild(span);
label.appendChild(input);
div.appendChild(label);
input.onchange = (ev: any) => {
if (type === 'number') {
if (Number(input.max) && Number(ev.srcElement.value) > Number(input.max))
input.value = input.max;
else if (Number(input.min) && Number(ev.srcElement.value) < Number(input.min))
input.value = input.min;
}
callback(ev);
}
return div as HTMLElement;
}
export function getGroundElevation(latlng: LatLng, callback: CallableFunction) {
/* Get the ground elevation from the server endpoint */
const xhr = new XMLHttpRequest();

View File

@ -1,14 +1,21 @@
import { getApp } from "..";
import { Dropdown } from "../controls/dropdown";
import { Switch } from "../controls/switch";
import { Toolbar } from "./toolbar";
export class PrimaryToolbar extends Toolbar {
#mainDropdown: Dropdown;
#cameraLinkTypeSwitch: Switch;
constructor(ID: string) {
super(ID);
/* The content of the dropdown is entirely defined in the .ejs file */
this.#mainDropdown = new Dropdown("app-icon", () => { });
this.#cameraLinkTypeSwitch = new Switch("camera-link-type-switch", (value: boolean) => {
getApp().getMap().setCameraControlMode(value? 'map': 'live');
})
}
getMainDropdown() {

View File

@ -1654,7 +1654,7 @@ export class GroundUnit extends Unit {
/* When we zoom past the grouping limit, grouping is enabled and the unit is a leader, we redraw the unit to apply any possible grouped marker */
checkZoomRedraw(): boolean {
return (this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] &&
return (this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] as boolean &&
(getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION ||
getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION))
}

25
manager/ejs/camera.ejs Normal file
View File

@ -0,0 +1,25 @@
<style>
</style>
<div>
<div class="instructions">
<div class="step">
Step <%= instances.length === 1? "5": "6" %> of <%= instances.length === 1? "5": "6" %>
</div>
<div class="title">
Do you want to install the camera control plugin?
</div>
<div class="description">
The camera control plugin allows you to control the camera position in DCS from Olympus. <br>
It is necessary to install it in DCS even if you plan to use Olympus on a remote machine via your browser.
</div>
</div>
<div class="wizard-inputs">
<div class="button radio install selected" onclick="signal('onInstallCameraControlClicked', 'install')">
Install
</div>
<div class="button radio no-install" onclick="signal('onInstallCameraControlClicked', 'no-install')">
Do not install
</div>
</div>
</div>

View File

@ -4,7 +4,7 @@
<div id="connections-page">
<div class="instructions">
<div class="step">
Step <%= instances.length === 1? "3": "4" %> of <%= instances.length === 1? "4": "5" %>
Step <%= instances.length === 1? "3": "4" %> of <%= instances.length === 1? "5": "6" %>
</div>
<div class="title">
Manually set Olympus port and address settings

View File

@ -4,7 +4,7 @@
<div>
<div class="instructions">
<div class="step">
Step <%= instances.length === 1? "2": "3" %> of <%= instances.length === 1? "4": "5" %>
Step <%= instances.length === 1? "2": "3" %> of <%= instances.length === 1? "5": "6" %>
</div>
<div class="title">
Do you want to set port and address settings?

View File

@ -73,6 +73,13 @@
title="Allows services to connect to Olympus directly. This is NOT NEEDED for normal Olympus operation, even for dedicated servers. Leave it unchecked if in doubt.">
</span>
</div>
<div class="input-group camera-plugin">
<span onclick="signal('onEnableCameraPluginClicked')">
<div class="checkbox checked"></div> Enable camera control plugin
<img src="./icons/circle-info-solid.svg"
title="Install the camera control plugin, which allows direct control of the DCS camera from Olympus. It is necessary even to control the camera even if Olympus is being used remotely using a browser.">
</span>
</div>
</div>
</div>
</div>

View File

@ -5,7 +5,7 @@
<div class="instructions">
<% if (instances.length > 0) { %>
<div class="step">
Step 1 of <%= instances.length === 1? "4": "5" %>
Step 1 of <%= instances.length === 1? "5": "6" %>
</div>
<div class="title">
Which DCS instance you want to add Olympus to?

View File

@ -4,7 +4,7 @@
<div id="passwords-page">
<div class="instructions">
<div class="step">
Step <%= instances.length === 1? "4": "5" %> of <%= instances.length === 1? "4": "5" %>
Step <%= instances.length === 1? "4": "5" %> of <%= instances.length === 1? "5": "6" %>
</div>
<div class="title">
Enter your passwords for Olympus

View File

@ -4,7 +4,7 @@
<div>
<div class="instructions">
<div class="step">
Step <%= instances.length === 1? "1": "2" %> of <%= instances.length === 1? "4": "5" %>
Step <%= instances.length === 1? "1": "2" %> of <%= instances.length === 1? "5": "6" %>
</div>
<div class="title">
Do you want to add Olympus for singleplayer or multiplayer?

View File

@ -6,7 +6,7 @@ const { checkPort, fetchWithTimeout, getFreePort } = require('./net')
const dircompare = require('dir-compare');
const { spawn } = require('child_process');
const find = require('find-process');
const { installHooks, installMod, installJSON, applyConfiguration, installShortCuts, deleteMod, deleteHooks, deleteJSON, deleteShortCuts } = require('./filesystem')
const { installHooks, installMod, installJSON, applyConfiguration, installShortCuts, deleteMod, deleteHooks, deleteJSON, deleteShortCuts, installCameraPlugin, deleteCameraPlugin } = require('./filesystem')
const { showErrorPopup, showConfirmPopup, showWaitLoadingPopup, setPopupLoadingProgress } = require('./popup')
const { logger } = require("./filesystem")
const { hidePopup } = require('./popup');
@ -129,6 +129,7 @@ class DCSInstance {
fps = 0;
installationType = 'singleplayer';
connectionsType = 'auto';
installCameraPlugin = 'install';
gameMasterPasswordEdited = false;
blueCommanderPasswordEdited = false;
redCommanderPasswordEdited = false;
@ -154,6 +155,7 @@ class DCSInstance {
this.error = false;
this.installationType = 'singleplayer';
this.connectionsType = 'auto';
this.installCameraPlugin = 'install';
/* Check if the olympus.json file is detected. If true, Olympus is considered to be installed */
if (fs.existsSync(path.join(this.folder, "Config", "olympus.json"))) {
@ -518,22 +520,28 @@ class DCSInstance {
await sleep(100);
await installHooks(getManager().getActiveInstance().folder);
setPopupLoadingProgress("Installing mod folder...", 20);
setPopupLoadingProgress("Installing mod folder...", 16);
await sleep(100);
await installMod(getManager().getActiveInstance().folder, getManager().getActiveInstance().name);
setPopupLoadingProgress("Installing JSON file...", 40);
setPopupLoadingProgress("Installing JSON file...", 33);
await sleep(100);
await installJSON(getManager().getActiveInstance().folder);
setPopupLoadingProgress("Applying configuration...", 60);
setPopupLoadingProgress("Applying configuration...", 50);
await sleep(100);
await applyConfiguration(getManager().getActiveInstance().folder, getManager().getActiveInstance());
setPopupLoadingProgress("Creating shortcuts...", 80);
setPopupLoadingProgress("Creating shortcuts...", 67);
await sleep(100);
await installShortCuts(getManager().getActiveInstance().folder, getManager().getActiveInstance().name);
if (getManager().getActiveInstance().installCameraPlugin === 'install') {
setPopupLoadingProgress("Installing camera plugin...", 83);
await sleep(100);
await installCameraPlugin(getManager().getActiveInstance().folder);
}
setPopupLoadingProgress("Installation completed!", 100);
await sleep(500);
logger.log(`Installation completed successfully`);
@ -575,18 +583,22 @@ class DCSInstance {
await sleep(100);
await deleteMod(this.folder, this.name);
setPopupLoadingProgress("Deleting hook scripts...", 25);
setPopupLoadingProgress("Deleting hook scripts...", 20);
await sleep(100);
await deleteHooks(this.folder);
setPopupLoadingProgress("Deleting JSON...", 50);
setPopupLoadingProgress("Deleting JSON...", 40);
await sleep(100);
await deleteJSON(this.folder);
setPopupLoadingProgress("Deleting shortcuts...", 75);
setPopupLoadingProgress("Deleting shortcuts...", 60);
await sleep(100);
await deleteShortCuts(this.folder, this.name);
setPopupLoadingProgress("Deleting camera plugin...", 80);
await sleep(100);
await deleteCameraPlugin(this.folder);
await sleep(500);
setPopupLoadingProgress("Instance removed!", 100);
logger.log(`Olympus removed from ${this.folder}`)

View File

@ -11,6 +11,8 @@ var logger = new Console(output, output);
const date = new Date();
output.write(` ======================= New log starting at ${date.toString()} =======================\n`);
var EXPORT_STRING = "pcall(function() local olympusLFS=require('lfs');dofile(olympusLFS.writedir()..[[Mods\\Services\\Olympus\\Scripts\\OlympusCameraControl.lua]]); end,nil) ";
/** Conveniency function to asynchronously delete a single file, with error catching
*
* @param {String} filePath The path to the file to delete
@ -172,6 +174,29 @@ async function applyConfiguration(folder, instance) {
}
}
/** Asynchronously install the camera control plugin
*
* @param {String} folder The base Saved Games folder where Olympus is installed
*/
async function installCameraPlugin(folder) {
logger.log(`Installing camera support plugin to DCS in ${folder}`);
/* If the export file doesn't exist, create it */
if (!(await exists(path.join(folder, "Scripts", "export.lua")))) {
await fsp.writeFile(path.join(folder, "Scripts", "export.lua"), EXPORT_STRING);
} else {
let content = await fsp.readFile(path.join(folder, "Scripts", "export.lua"), { encoding: 'utf8' });
if (content.indexOf(EXPORT_STRING) != -1) {
/* Looks like the export string is already installed, nothing to do */
}
else {
/* Append the export string at the end of the file */
content += ("\n" + EXPORT_STRING);
}
/* Write the content of the file */
await fsp.writeFile(path.join(folder, "Scripts", "export.lua"), content)
}
}
/** Asynchronously deletes the Hooks script
*
* @param {String} folder The base Saved Games folder where Olympus is installed
@ -231,15 +256,40 @@ async function deleteShortCuts(folder, name) {
logger.log(`ShortCuts deleted from ${folder} and desktop`);
}
/** Asynchronously removes the camera plugin string from the export lua file
*
* @param {String} folder The base Saved Games folder where Olympus is installed
*/
async function deleteCameraPlugin(folder) {
logger.log(`Deleting camera support plugin to DCS in ${folder}`);
if (!(await exists(path.join(folder, "Scripts", "export.lua")))) {
/* If the export file doesn't exist, nothing to do */
} else {
let content = await fsp.readFile(path.join(folder, "Scripts", "export.lua"), { encoding: 'utf8' });
if (content.indexOf(EXPORT_STRING) ==+ -1) {
/* Looks like the export string is not installed, nothing to do */
}
else {
/* Remove the export string from the file */
content = content.replace(EXPORT_STRING, "")
/* Write the content of the file */
await fsp.writeFile(path.join(folder, "Scripts", "export.lua"), content)
}
}
}
module.exports = {
applyConfiguration: applyConfiguration,
installJSON: installJSON,
installHooks: installHooks,
installMod: installMod,
installShortCuts, installShortCuts,
installShortCuts: installShortCuts,
installCameraPlugin: installCameraPlugin,
deleteHooks: deleteHooks,
deleteJSON: deleteJSON,
deleteMod: deleteMod,
deleteShortCuts: deleteShortCuts,
deleteCameraPlugin: deleteCameraPlugin,
logger: logger
}

View File

@ -32,6 +32,7 @@ class Manager {
connectionsTypePage = null;
connectionsPage = null;
passwordsPage = null;
cameraPage = null;
resultPage = null;
instancesPage = null;
expertSettingsPage = null;
@ -103,9 +104,9 @@ class Manager {
/* Get my public IP */
this.getPublicIP().then(
(IP) => { this.setIP(IP); },
(err) => {
(err) => {
logger.log(err)
this.setIP(undefined);
this.setIP(undefined);
}
)
@ -142,6 +143,7 @@ class Manager {
this.connectionsTypePage = new WizardPage(this, "./ejs/connectionsType.ejs");
this.connectionsPage = new WizardPage(this, "./ejs/connections.ejs");
this.passwordsPage = new WizardPage(this, "./ejs/passwords.ejs");
this.cameraPage = new WizardPage(this, "./ejs/camera.ejs");
this.resultPage = new ManagerPage(this, "./ejs/result.ejs");
this.instancesPage = new ManagerPage(this, "./ejs/instances.ejs");
this.expertSettingsPage = new WizardPage(this, "./ejs/expertsettings.ejs");
@ -159,7 +161,7 @@ class Manager {
this.setPort('backend', this.getActiveInstance().backendPort);
}
}
/* Always force the IDLE state when reaching the menu page */
this.menuPage.options.onShow = async () => {
await this.setState('IDLE');
@ -337,6 +339,17 @@ class Manager {
}
}
/* When the camera control installation is selected */
async onInstallCameraControlClicked(type) {
this.connectionsTypePage.getElement().querySelector(`.install`).classList.toggle("selected", type === 'install');
this.connectionsTypePage.getElement().querySelector(`.no-install`).classList.toggle("selected", type === 'no-install');
if (this.getActiveInstance())
this.getActiveInstance().installCameraPlugin = type;
else {
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
}
}
/* When the next button of a wizard page is clicked */
async onNextClicked() {
/* Choose which page to show depending on the active page */
@ -360,11 +373,11 @@ class Manager {
this.activePage.hide();
this.typePage.show();
}
/* Installation type page */
/* Installation type page */
} else if (this.activePage == this.typePage) {
this.activePage.hide();
this.connectionsTypePage.show();
/* Connection type page */
/* Connection type page */
} else if (this.activePage == this.connectionsTypePage) {
if (this.getActiveInstance()) {
if (this.getActiveInstance().connectionsType === 'auto') {
@ -374,24 +387,28 @@ class Manager {
else {
this.activePage.hide();
this.connectionsPage.show();
(this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*')
(this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*')
}
} else {
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
}
/* Connection page */
/* Connection page */
} else if (this.activePage == this.connectionsPage) {
if (await this.checkPorts()) {
this.activePage.hide();
this.passwordsPage.show();
}
/* Passwords page */
}
/* Passwords page */
} else if (this.activePage == this.passwordsPage) {
if (await this.checkPasswords()) {
this.activePage.hide();
this.getState() === 'INSTALL' ? this.getActiveInstance().install() : this.getActiveInstance().edit();
this.cameraPage.show()
}
/* Expert settings page */
/* Installation type page */
} else if (this.activePage == this.cameraPage) {
this.activePage.hide();
this.getState() === 'INSTALL' ? this.getActiveInstance().install() : this.getActiveInstance().edit();
/* Expert settings page */
} else if (this.activePage == this.expertSettingsPage) {
if (await this.checkPorts() && await this.checkPasswords()) {
this.activePage.hide();
@ -416,7 +433,7 @@ class Manager {
async onCancelClicked() {
this.activePage.hide();
await this.setState('IDLE');
if (this.getMode() === "basic")
if (this.getMode() === "basic")
this.menuPage.show(true);
else
this.instancesPage.show(true);
@ -441,7 +458,7 @@ class Manager {
if (this.getActiveInstance())
this.getActiveInstance().setBlueCommanderPassword(value);
else
else
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
}
@ -450,9 +467,9 @@ class Manager {
input.placeholder = "";
}
if (this.getActiveInstance())
if (this.getActiveInstance())
this.getActiveInstance().setRedCommanderPassword(value);
else
else
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`);
}
@ -485,6 +502,20 @@ class Manager {
}
}
/* When the "Enable camera control plugin" checkbox is clicked */
async onEnableCameraPluginClicked() {
if (this.getActiveInstance()) {
if (this.getActiveInstance().installCameraPlugin === 'install') {
this.getActiveInstance().installCameraPlugin = 'no-install';
} else {
this.getActiveInstance().installCameraPlugin = 'install';
}
this.expertSettingsPage.getElement().querySelector(".camera-plugin .checkbox").classList.toggle("checked", this.getActiveInstance().installCameraPlugin === 'install')
} else {
showErrorPopup(`<div class='main-message'>A critical error occurred! </div><div class='sub-message'> Check ${this.getLogLocation()} for more info. </div>`)
}
}
/* When the "Return to manager" button is pressed */
async onReturnClicked() {
await this.reload();
@ -562,7 +593,7 @@ class Manager {
this.setActiveInstance(instance);
await this.setState('EDIT');
this.activePage.hide();
(this.getMode() === 'basic'? this.typePage: this.expertSettingsPage).show();
(this.getMode() === 'basic' ? this.typePage : this.expertSettingsPage).show();
}
}
@ -571,7 +602,7 @@ class Manager {
this.setActiveInstance(instance);
await this.setState('INSTALL');
this.activePage.hide();
(this.getMode() === 'basic'? this.typePage: this.expertSettingsPage).show();
(this.getMode() === 'basic' ? this.typePage : this.expertSettingsPage).show();
}
async onUninstallClicked(name) {
@ -579,7 +610,7 @@ class Manager {
this.setActiveInstance(instance);
await this.setState('UNINSTALL');
if (instance.webserverOnline || instance.backendOnline)
showErrorPopup("<div class='main-message'>The selected Olympus instance is currently active </div><div class='sub-message'> Please stop DCS and Olympus Server/Client before removing it! </div>")
showErrorPopup("<div class='main-message'>The selected Olympus instance is currently active </div><div class='sub-message'> Please stop DCS and Olympus Server/Client before removing it! </div>")
else
await instance.uninstall();
}
@ -620,11 +651,11 @@ class Manager {
this.getActiveInstance().setBackendPort(value);
}
var successEls = (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".success");
var successEls = (this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".success");
for (let i = 0; i < successEls.length; i++) {
successEls[i].classList.toggle("hide", !success);
}
var errorEls = (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".error");
var errorEls = (this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".error");
for (let i = 0; i < errorEls.length; i++) {
errorEls[i].classList.toggle("hide", success);
}
@ -693,7 +724,7 @@ class Manager {
document.getElementById("loader").style.opacity = "0%";
window.setTimeout(() => {
document.getElementById("loader").classList.add("hide");
}, 250);
}, 250);
}
async setActiveInstance(newActiveInstance) {
@ -718,12 +749,12 @@ class Manager {
async setLogLocation(newLogLocation) {
this.options.logLocation = newLogLocation;
}
}
async setState(newState) {
this.options.state = newState;
await DCSInstance.reloadInstances();
if (newState === 'IDLE')
if (newState === 'IDLE')
this.setActiveInstance(undefined);
}

View File

@ -14,6 +14,8 @@
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
"username": null,
"password": null
}
},
"additionalMaps": {
}
}
}

View File

@ -0,0 +1,214 @@
local _prevLuaExportStart = LuaExportStart
local _prevLuaExportBeforeNextFrame = LuaExportBeforeNextFrame
local _prevLuaExportStop = LuaExportStop
local server = nil
local port = 3003
local headers = "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n\r\n"
function startTCPServer()
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'Starting TCP Server')
package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua"
package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll"
socket = require("socket")
server = assert(socket.bind("127.0.0.1", port))
if server then
server:setoption("tcp-nodelay", true)
server:settimeout(0)
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'TCP Server listening on port ' .. port)
else
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'TCP Server did not start successfully')
end
end
function receiveTCP()
if server then
-- Accept a new connection without blocking
local client = server:accept()
if client then
-- Set the timeout of the connection to 5ms
client:settimeout(0)
client:setoption("tcp-nodelay", true)
local acc = ""
local data = ""
-- Start receiving data, accumulate it in acc
while data ~= nil do
-- Receive a new line
data, err, partial = client:receive('*l')
if data then
-- If we receive an empty string it means the header section of the message is over
if data == "" then
-- Is this an OPTIONS request?
if string.find(acc, "OPTIONS") ~= nil then
client:send("HTTP/1.1 200 OK\r\n" .. headers)
client:close()
-- Is this a PUT request?
elseif string.find(acc, "PUT") ~= nil then
-- Extract the length of the body
local contentLength = string.match(acc, "Content%-Length: (%d+)")
if contentLength ~= nil then
-- Receive the body
body, err, partial = client:receive(tonumber(contentLength))
if body ~= nil then
local lat = string.match(body, '"lat":%s*([%+%-]?[%d%.]+)%s*[},]')
local lng = string.match(body, '"lng":%s*([%+%-]?[%d%.]+)%s*[},]')
local alt = string.match(body, '"alt":%s*([%+%-]?[%d%.]+)%s*[},]')
local mode = string.match(body, '"mode":%s*"(%a+)"%s*[},]')
if lat ~= nil and lng ~= nil then
client:send("HTTP/1.1 200 OK\r\n" .. headers)
local position = {}
position["lat"] = tonumber(lat)
position["lng"] = tonumber(lng)
if alt ~= nil then
position["alt"] = tonumber(alt)
end
-- F11 view
if mode == "live" or mode == nil then
LoSetCommand(158)
-- F10 view
elseif mode == "map" then
LoSetCommand(15)
end
client:send(setCameraPosition(position))
client:close()
else
client:send("HTTP/1.1 500 ERROR\r\n" .. headers)
client:close()
end
else
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, err)
end
end
client:close()
break
end
else
-- Keep accumulating the incoming data
acc = acc .. " " .. data
end
end
end
end
end
end
function stopTCPServer()
if server then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'Stopping TCP Server')
server:close()
end
server = nil
end
function setCameraPosition(position)
-- Get the old camera position
local oldPos = LoGetCameraPosition()
-- Extract the commanded position
local point = LoGeoCoordinatesToLoCoordinates(position.lng, position.lat)
local pointNorth = LoGeoCoordinatesToLoCoordinates(position.lng, position.lat + 0.1)
-- Compute the local map rotation and scale and send it back to the server
local rotation = math.atan2(pointNorth.z - point.z, pointNorth.x - point.x)
-- If no altitude is provided, preserve the current camera altitude
local altitude = nil
if position.alt == nil then
altitude = oldPos.p.y
else
altitude = position.alt
end
-- Set the camera position
local pos =
{
x = {x = 0, y = -1, z = 0},
y = {x = 1, y = 0, z = 0},
z = {x = 0, y = 0, z = 1},
p = {x = point.x, y = altitude, z = point.z}
}
LoSetCameraPosition(pos)
return '{"northRotation": ' .. rotation .. '}'
end
LuaExportStart = function()
package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua"
package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll"
startTCPServer()
-- call original
if _prevLuaExportStart then
_status, _result = pcall(_prevLuaExportStart)
if not _status then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportStart from another script', _result)
end
end
end
LuaExportBeforeNextFrame = function()
receiveTCP()
-- call original
if _prevLuaExportBeforeNextFrame then
_status, _result = pcall(_prevLuaExportBeforeNextFrame)
if not _status then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportBeforeNextFrame from another script', _result)
end
end
end
LuaExportStop = function()
stopTCPServer()
-- call original
if _prevLuaExportStop then
_status, _result = pcall(_prevLuaExportStop)
if not _status then
log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportStop from another script', _result)
end
end
end
function serializeTable(val, name, skipnewlines, depth)
skipnewlines = skipnewlines or false
depth = depth or 0
local tmp = string.rep(" ", depth)
if name then
if type(name) == "number" then
tmp = tmp .. "[" .. name .. "]" .. " = "
else
tmp = tmp .. name .. " = "
end
end
if type(val) == "table" then
tmp = tmp .. "{" .. (not skipnewlines and "\n" or "")
for k, v in pairs(val) do
tmp = tmp .. serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "")
end
tmp = tmp .. string.rep(" ", depth) .. "}"
elseif type(val) == "number" then
tmp = tmp .. tostring(val)
elseif type(val) == "string" then
tmp = tmp .. string.format("%q", val)
elseif type(val) == "boolean" then
tmp = tmp .. (val and "true" or "false")
else
tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\""
end
return tmp
end

View File

@ -0,0 +1,23 @@
import socket
from email.utils import formatdate
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('127.0.0.1', 3003))
sock.listen(5)
count = 0
while True:
connection, address = sock.accept()
buf = connection.recv(1024)
print(buf.decode("utf-8"))
if "OPTIONS" in buf.decode("utf-8"):
resp = (f"""HTTP/1.1 200 OK\r\nDate: {formatdate(timeval=None, localtime=False, usegmt=True)}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n""".encode("utf-8"))
connection.send(resp)
if not "PUT" in buf.decode("utf-8"):
connection.close()
else:
resp = (f"""HTTP/1.1 200 OK\r\nDate: {formatdate(timeval=None, localtime=False, usegmt=True)}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n\r\n{{"Hi": "Wirts!"}}\r\n""".encode("utf-8"))
connection.send(resp)
connection.close()
count += 1

View File

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "python",
"request": "launch",
"program": "main.py",
"console": "integratedTerminal",
"args": ["./configs/Caucasus/HighResolution.yml"]
}
]
}

View File

@ -0,0 +1,37 @@
import sys
from fastkml import kml
from pygeoif.geometry import Polygon
import json
import math
# constants
C = 40075016.686 # meters, Earth equatorial circumference
R = C / (2 * math.pi) # meters, Earth equatorial radius
W = 10000 # meters, size of the square around the airbase
if len(sys.argv) == 1:
print("Please provide a json file as first argument. You can also drop the json file on this script to run it.")
else:
input_file = sys.argv[1]
k = kml.KML()
ns = '{http://www.opengis.net/kml/2.2}'
d = kml.Document(ns, 'docid', 'doc name', 'doc description')
k.append(d)
with open(input_file) as jp:
j = json.load(jp)
for point in j['airbases'].values():
p = kml.Placemark(ns, 'id', 'name', 'description')
lat = point['latitude']
lng = point['longitude']
latDelta = math.degrees(W / R)
lngDelta = math.degrees(W / (R * math.cos(math.radians(lat))))
p.geometry = Polygon([(lng - lngDelta, lat - latDelta), (lng - lngDelta, lat + latDelta), (lng + lngDelta, lat + latDelta), (lng + lngDelta, lat - latDelta)])
d.append(p)
with open(input_file.removesuffix('.json')+'.kml', 'w') as kp:
kp.writelines(k.to_string(prettyprint=True))

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Caucasus', # Where to save the output files
'boundary_file': '.\configs\Caucasus\airbases.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.1 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>Senza titolo</name>
<gx:CascadingStyle kml:id="__managed_style_280E5494AE2F24E92C22">
<Style>
<IconStyle>
<scale>1.2</scale>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
</IconStyle>
<LabelStyle>
</LabelStyle>
<LineStyle>
<color>ff2dc0fb</color>
<width>6</width>
</LineStyle>
<PolyStyle>
<color>40ffffff</color>
</PolyStyle>
<BalloonStyle>
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<gx:CascadingStyle kml:id="__managed_style_1EB9027B622F24E92C22">
<Style>
<IconStyle>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
</IconStyle>
<LabelStyle>
</LabelStyle>
<LineStyle>
<color>ff2dc0fb</color>
<width>4</width>
</LineStyle>
<PolyStyle>
<color>40ffffff</color>
</PolyStyle>
<BalloonStyle>
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<StyleMap id="__managed_style_0F57E9B9782F24E92C22">
<Pair>
<key>normal</key>
<styleUrl>#__managed_style_1EB9027B622F24E92C22</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#__managed_style_280E5494AE2F24E92C22</styleUrl>
</Pair>
</StyleMap>
<Placemark id="0975D432582F24E92C1E">
<name>Poligono senza titolo</name>
<LookAt>
<longitude>37.25019544589698</longitude>
<latitude>44.41771380726969</latitude>
<altitude>-138.6844933247498</altitude>
<heading>0</heading>
<tilt>0</tilt>
<gx:fovy>35</gx:fovy>
<range>3831683.119853139</range>
<altitudeMode>absolute</altitudeMode>
</LookAt>
<styleUrl>#__managed_style_0F57E9B9782F24E92C22</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
32.46459319237173,45.67416695848307,0 32.2740650283415,45.2221541106433,0 33.22174616520244,44.4837859435444,0 34.05427109764131,44.2149221586376,0 34.96485577272431,44.60230684639296,0 35.50552864748745,44.8069362633187,0 36.446105774871,44.84425518198143,0 36.76914203317659,44.70347050722764,0 38.22313992004164,44.3163345847565,0 39.43106567523965,43.72064977016311,0 40.23832274382622,43.06831352526857,0 41.01327578994438,42.67925159935859,0 41.34464189582403,42.34329512558789,0 41.16749495371268,41.74956946999534,0 40.80780496107725,41.39360013128164,0 39.98364177441992,41.27272565351572,0 39.42209428526464,41.27830763089842,0 38.82136897872954,41.2291415593637,0 38.78900701766597,39.59331113999448,0 46.4826445997655,39.11657164682355,0 46.83937081793388,45.04996086829865,0 46.88987497227086,47.59122144470205,0 32.29992865035658,47.73230965442627,0 32.46459319237173,45.67416695848307,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Caucasus', # Where to save the output files
'boundary_file': '.\configs\Caucasus\LowResolution.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.5 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Caucasus', # Where to save the output files
'boundary_file': '.\configs\Caucasus\MediumResolution.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.25 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

View File

@ -0,0 +1 @@
{"airbases":{"1":{"callsign":"Anapa-Vityazevo","coalition":"neutral","latitude":45.013174733771677,"longitude":37.359783477555922},"10":{"callsign":"Gudauta","coalition":"neutral","latitude":43.124233340197144,"longitude":40.564175768400638},"11":{"callsign":"Batumi","coalition":"neutral","latitude":41.603279859649049,"longitude":41.609275483509791},"12":{"callsign":"Senaki-Kolkhi","coalition":"neutral","latitude":42.238728081573278,"longitude":42.061021312855914},"13":{"callsign":"Kobuleti","coalition":"neutral","latitude":41.93210535345338,"longitude":41.876483823101026},"14":{"callsign":"Kutaisi","coalition":"neutral","latitude":42.179153937689627,"longitude":42.495684077400142},"15":{"callsign":"Mineralnye Vody","coalition":"neutral","latitude":44.218646823806807,"longitude":43.100679733081456},"16":{"callsign":"Nalchik","coalition":"neutral","latitude":43.510071438529849,"longitude":43.625108736097914},"17":{"callsign":"Mozdok","coalition":"neutral","latitude":43.791303250938249,"longitude":44.620327262102009},"18":{"callsign":"Tbilisi-Lochini","coalition":"neutral","latitude":41.674720064437075,"longitude":44.946875226153338},"19":{"callsign":"Soganlug","coalition":"neutral","latitude":41.641163266786613,"longitude":44.947183065316693},"2":{"callsign":"Krasnodar-Center","coalition":"neutral","latitude":45.087429883845076,"longitude":38.925202300775062},"20":{"callsign":"Vaziani","coalition":"neutral","latitude":41.637735936261556,"longitude":45.019090938460067},"21":{"callsign":"Beslan","coalition":"neutral","latitude":43.208500987380937,"longitude":44.588922553542936},"3":{"callsign":"Novorossiysk","coalition":"neutral","latitude":44.673329604126899,"longitude":37.786226060479564},"4":{"callsign":"Krymsk","coalition":"neutral","latitude":44.961383022734175,"longitude":37.985886938697085},"5":{"callsign":"Maykop-Khanskaya","coalition":"neutral","latitude":44.67144025735508,"longitude":40.021427482235985},"6":{"callsign":"Gelendzhik","coalition":"neutral","latitude":44.56767458600406,"longitude":38.004146350528103},"7":{"callsign":"Sochi-Adler","coalition":"neutral","latitude":43.439378434050852,"longitude":39.924231880466095},"8":{"callsign":"Krasnodar-Pashkovsky","coalition":"neutral","latitude":45.0460996415433,"longitude":39.203066906324537},"9":{"callsign":"Sukhumi-Babushara","coalition":"neutral","latitude":42.852741071634995,"longitude":41.142447588488196}},"frameRate":60,"load":0,"sessionHash":"K2n7kpGE9yOaYE4G","time":"1709136685634"}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>Senza titolo</name>
<gx:CascadingStyle kml:id="__managed_style_1847AF2A832F1651A60F">
<Style>
<IconStyle>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
</IconStyle>
<LabelStyle>
</LabelStyle>
<LineStyle>
<color>ff2dc0fb</color>
<width>4</width>
</LineStyle>
<PolyStyle>
<color>40ffffff</color>
</PolyStyle>
<BalloonStyle>
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<gx:CascadingStyle kml:id="__managed_style_2C7F63B5A12F1651A60F">
<Style>
<IconStyle>
<scale>1.2</scale>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
</IconStyle>
<LabelStyle>
</LabelStyle>
<LineStyle>
<color>ff2dc0fb</color>
<width>6</width>
</LineStyle>
<PolyStyle>
<color>40ffffff</color>
</PolyStyle>
<BalloonStyle>
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<StyleMap id="__managed_style_043F3D3A202F1651A60F">
<Pair>
<key>normal</key>
<styleUrl>#__managed_style_1847AF2A832F1651A60F</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#__managed_style_2C7F63B5A12F1651A60F</styleUrl>
</Pair>
</StyleMap>
<Placemark id="0F15269F3D2F1651A60F">
<name>NTTR</name>
<LookAt>
<longitude>-117.2703145690532</longitude>
<latitude>37.39557832822189</latitude>
<altitude>1754.517427470683</altitude>
<heading>359.4706465490362</heading>
<tilt>0</tilt>
<gx:fovy>35</gx:fovy>
<range>1393300.815671235</range>
<altitudeMode>absolute</altitudeMode>
</LookAt>
<styleUrl>#__managed_style_043F3D3A202F1651A60F</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
-119.7864240113604,34.44074394422174,0 -112.42342379541,34.34217218687283,0 -112.1179107081757,39.75928290264283,0 -120.0041004413372,39.79698539473655,0 -119.7864240113604,34.44074394422174,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\NTTR', # Where to save the output files
'boundary_file': '.\configs\NTTR\boundary.kml', # Input kml file setting the boundary of the map to create
'zoom_factor': 0.5 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
}

View File

@ -0,0 +1,4 @@
{
'width': 1920, # The width of your screen, in pixels
'height': 1080 # The height of your screen, in pixels
}

View File

@ -0,0 +1,84 @@
import sys
import yaml
import json
import requests
from pyproj import Geod
from fastkml import kml
from shapely import wkt
from datetime import timedelta
import map_generator
# Port on which the camera control module is listening
port = 3003
if len(sys.argv) == 1:
print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.")
else:
config_file = sys.argv[1]
print(f"Using config file: {config_file}")
with open('configs/screen_properties.yml', 'r') as sp:
with open(config_file, 'r') as cp:
screen_config = yaml.safe_load(sp)
map_config = yaml.safe_load(cp)
print("Screen parameters:")
print(f"-> Screen width: {screen_config['width']}px")
print(f"-> Screen height: {screen_config['height']}px")
print("Map parameters:")
print(f"-> Output directory: {map_config['output_directory']}")
print(f"-> Boundary file: {map_config['boundary_file']}")
print(f"-> Zoom factor: {map_config['zoom_factor']}")
if 'geo_width' in map_config:
print(f"-> Geo width: {map_config['geo_width']}NM")
with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
# Read the config file and compute the total area of the covered map
doc = bp.read()
k = kml.KML()
k.from_string(doc)
geod = Geod(ellps="WGS84")
features = []
area = 0
for feature in k.features():
for sub_feature in list(feature.features()):
geo = sub_feature.geometry
area += abs(geod.geometry_area_perimeter(wkt.loads(geo.wkt))[0])
features.append(sub_feature)
print(f"Found {len(features)} features in the provided kml file")
if 'geo_width' not in map_config:
# Let the user input the size of the screen to compute resolution
data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'})
try:
r = requests.put(f'http://127.0.0.1:{port}', data = data)
print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles")
except:
print("No running DCS instance detected. You can still run the algorithm if you already took the screenshots, otherwise you will not be able to produce a map.")
map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ")
map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width']
tile_size = 256 * map_config['mpps'] # meters
tiles_per_screenshot = int(screen_config['width'] / 256) * int(screen_config['height'] / 256)
tiles_num = int(area / (tile_size * tile_size))
screenshots_num = int(tiles_num / tiles_per_screenshot)
total_time = int(screenshots_num / 1.0)
print(f"Total area: {int(area / 1e6)} square kilometers")
print(f"Estimated number of tiles: {tiles_num}")
print(f"Estimated number of screenshots: {screenshots_num}")
print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)")
input("Press enter to continue...")
map_generator.run(map_config, port)

View File

@ -0,0 +1,320 @@
import math
import requests
import pyautogui
import time
import os
import yaml
import json
import numpy
from fastkml import kml
from shapely import wkt, Point
from PIL import Image
from concurrent import futures
from os import listdir
from os.path import isfile, isdir, join
# global counters
fut_counter = 0
tot_futs = 0
# constants
C = 40075016.686 # meters, Earth equatorial circumference
R = C / (2 * math.pi) # meters, Earth equatorial radius
def deg_to_num(lat_deg, lon_deg, zoom):
lat_rad = math.radians(lat_deg)
n = 1 << zoom
xtile = int((lon_deg + 180.0) / 360.0 * n)
ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
return xtile, ytile
def num_to_deg(xtile, ytile, zoom):
n = 1 << zoom
lon_deg = xtile / n * 360.0 - 180.0
lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
lat_deg = math.degrees(lat_rad)
return lat_deg, lon_deg
def compute_mpps(lat, z):
return C * math.cos(math.radians(lat)) / math.pow(2, z + 8)
def printProgressBar(iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '', printEnd = "\r"):
percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
filledLength = int(length * iteration // total)
bar = fill * filledLength + '-' * (length - filledLength)
print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd)
# Print New Line on Complete
if iteration == total:
print()
def done_callback(fut):
global fut_counter, tot_futs
fut_counter += 1
printProgressBar(fut_counter, tot_futs)
def extract_tiles(n, screenshots_XY, params):
f = params['f']
zoom = params['zoom']
output_directory = params['output_directory']
n_width = params['n_width']
n_height = params['n_height']
XY = screenshots_XY[n]
if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))):
# Open the source screenshot
img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))
# Compute the Web Mercator Projection position of the top left corner of the most centered tile
X_center, Y_center = XY[0], XY[1]
# Compute the position of the top left corner of the top left tile
start_x = img.width / 2 - n_width / 2 * 256
start_y = img.height / 2 - n_height / 2 * 256
# Iterate on the grid
for column in range(0, n_width):
for row in range(0, n_height):
# Crop the tile and compute its Web Mercator Projection position
box = (start_x + column * 256, start_y + row * 256, start_x + (column + 1) * 256, start_y + (row + 1) * 256)
X = X_center - math.floor(n_width / 2) + column
Y = Y_center - math.floor(n_height / 2) + row
# Save the tile
if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom), str(X))):
try:
os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X)))
except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
continue
except Exception as e:
raise e
img.crop(box).save(os.path.join(output_directory, "tiles", str(zoom), str(X), f"{Y}.jpg"))
n += 1
else:
raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}_{zoom}.jpg')} missing")
def merge_tiles(base_path, zoom, tile):
X = tile[0]
Y = tile[1]
# If the image already exists, open it so we can paste the higher quality data in it
if os.path.exists(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg")):
dst = Image.open(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"))
dst = make_background_transparent(dst)
else:
dst = Image.new('RGB', (256, 256), (221, 221, 221))
# Loop on all the 4 subtiles in the tile
positions = [(0, 0), (0, 1), (1, 0), (1, 1)]
for i in range(0, 4):
# Open the subtile, if it exists, and resize it down to 128x128
if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")):
im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128))
im = make_background_transparent(im)
dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128))
# Create the output folder if it exists
if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))):
try:
os.mkdir(os.path.join(base_path, str(zoom - 1), str(X)))
except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
pass
except Exception as e:
raise e
# Save the image
dst.convert('RGB').save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95)
def make_background_transparent(im):
im.putalpha(255)
data = numpy.array(im)
red, green, blue, alpha = data.T
# If present, remove any "background" areas
background_areas = (red == 221) & (blue == 221) & (green == 221)
data[..., :][background_areas.T] = (0, 0, 0, 0) # make transparent
return Image.fromarray(data)
def run(map_config, port):
global tot_futs, fut_counter
with open('configs/screen_properties.yml', 'r') as sp:
screen_config = yaml.safe_load(sp)
# Create output folders
output_directory = map_config['output_directory']
if not os.path.exists(output_directory):
os.mkdir(output_directory)
skip_screenshots = False
if not os.path.exists(os.path.join(output_directory, "screenshots")):
os.mkdir(os.path.join(output_directory, "screenshots"))
else:
skip_screenshots = (input("Raw screenshots already found for this config, do you want to skip directly to tiles extraction? Enter y to skip: ") == "y")
if not os.path.exists(os.path.join(output_directory, "tiles")):
os.mkdir(os.path.join(output_directory, "tiles"))
# Compute the optimal zoom level
usable_width = screen_config['width'] - 400 # Keep a margin around the center
usable_height = screen_config['height'] - 400 # Keep a margin around the center
with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
# Read the config file
doc = bp.read()
k = kml.KML()
k.from_string(doc)
# Extract the features
features = []
for feature in k.features():
for sub_feature in list(feature.features()):
features.append(sub_feature)
# Iterate over all the closed features in the kml file
f = 1
for feature in features:
########### Take screenshots
geo = feature.geometry
# Define the boundary rect around the area
start_lat = geo.bounds[3]
start_lng = geo.bounds[0]
end_lat = geo.bounds[1]
end_lng = geo.bounds[2]
# Find the zoom level that better approximates the provided resolution
mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - map_config['mpps']) for z in range(0, 21)]
zoom = mpps_delta.index(min(mpps_delta))
print(f"Feature {f} of {len(features)}, using zoom level {zoom}")
# Find the maximum dimension of the tiles at the given resolution
mpps = compute_mpps(end_lat, zoom)
d = 256 * mpps / map_config['mpps']
n_height = math.floor(usable_height / d)
n_width = math.floor(usable_width / d)
print(f"Feature {f} of {len(features)}, each screenshot will provide {n_height} tiles in height and {n_width} tiles in width")
# Find the starting and ending points
start_X, start_Y = deg_to_num(start_lat, start_lng, zoom)
end_X, end_Y = deg_to_num(end_lat, end_lng, zoom)
# Find all the X, Y coordinates inside of the provided area
screenshots_XY = []
for X in range(start_X, end_X, n_width):
for Y in range(start_Y, end_Y, n_height):
lat, lng = num_to_deg(X, Y, zoom)
p = Point(lng, lat)
if p.within(wkt.loads(geo.wkt)):
screenshots_XY.append((X, Y))
print(f"Feature {f} of {len(features)}, {len(screenshots_XY)} screenshots will be taken")
# Start looping
if not skip_screenshots:
print(f"Feature {f} of {len(features)}, taking screenshots...")
n = 0
for XY in screenshots_XY:
# Making PUT request
# If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile!
lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom)
data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'})
r = requests.put(f'http://127.0.0.1:{port}', data = data)
geo_data = json.loads(r.text)
time.sleep(0.1)
# Take and save screenshot. The response to the put request contains data, among which there is the north rotation at that point.
screenshot = pyautogui.screenshot()
# Scale the screenshot to account for Mercator Map Deformation
lat1, lng1 = num_to_deg(XY[0], XY[1], zoom)
lat2, lng2 = num_to_deg(XY[0] + 1, XY[1] + 1, zoom)
deltaLat = abs(lat2 - lat1)
deltaLng = abs(lng2 - lng1)
# Compute the height and width the screenshot should have
m_height = math.radians(deltaLat) * R * n_height
m_width = math.radians(deltaLng) * R * math.cos(math.radians(lat1)) * n_width
# Compute the height and width the screenshot has
s_height = map_config['mpps'] * 256 * n_height
s_width = map_config['mpps'] * 256 * n_width
# Compute the scaling required to achieve that
sx = s_width / m_width
sy = s_height / m_height
# Resize, rotate and save the screenshot
screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"), quality=95)
printProgressBar(n + 1, len(screenshots_XY))
n += 1
########### Extract the tiles
if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom))):
os.mkdir(os.path.join(output_directory, "tiles", str(zoom)))
params = {
"f": f,
"zoom": zoom,
"output_directory": output_directory,
"n_width": n_width,
"n_height": n_height,
}
# Extract the tiles with parallel thread execution
with futures.ThreadPoolExecutor() as executor:
print(f"Feature {f} of {len(features)}, extracting tiles...")
futs = [executor.submit(extract_tiles, n, screenshots_XY, params) for n in range(0, len(screenshots_XY))]
tot_futs = len(futs)
fut_counter = 0
[fut.add_done_callback(done_callback) for fut in futs]
[fut.result() for fut in futures.as_completed(futs)]
# Increase the feature counter
print(f"Feature {f} of {len(features)} completed!")
f += 1
########### Assemble tiles to get lower zoom levels
for current_zoom in range(zoom, 8, -1):
Xs = [int(d) for d in listdir(os.path.join(output_directory, "tiles", str(current_zoom))) if isdir(join(output_directory, "tiles", str(current_zoom), d))]
existing_tiles = []
for X in Xs:
Ys = [int(f.removesuffix(".jpg")) for f in listdir(os.path.join(output_directory, "tiles", str(current_zoom), str(X))) if isfile(join(output_directory, "tiles", str(current_zoom), str(X), f))]
for Y in Ys:
existing_tiles.append((X, Y))
tiles_to_produce = []
for tile in existing_tiles:
if (int(tile[0] / 2), int(tile[1] / 2)) not in tiles_to_produce:
tiles_to_produce.append((int(tile[0] / 2), int(tile[1] / 2)))
# Merge the tiles with parallel thread execution
with futures.ThreadPoolExecutor() as executor:
print(f"Merging tiles for zoom level {current_zoom - 1}...")
if not os.path.exists(os.path.join(output_directory, "tiles", str(current_zoom - 1))):
os.mkdir(os.path.join(output_directory, "tiles", str(current_zoom - 1)))
futs = [executor.submit(merge_tiles, os.path.join(output_directory, "tiles"), current_zoom, tile) for tile in tiles_to_produce]
tot_futs = len(futs)
fut_counter = 0
[fut.add_done_callback(done_callback) for fut in futs]
[fut.result() for fut in futures.as_completed(futs)]

View File

@ -0,0 +1,23 @@
certifi==2024.2.2
charset-normalizer==3.3.2
fastkml==0.12
idna==3.6
MouseInfo==0.1.3
numpy==1.26.4
pillow==10.2.0
PyAutoGUI==0.9.54
pygeoif==0.7
PyGetWindow==0.0.9
PyMsgBox==1.0.9
pyperclip==1.8.2
pyproj==3.6.1
PyRect==0.2.0
PyScreeze==0.1.30
python-dateutil==2.8.2
pytweening==1.2.0
PyYAML==6.0.1
requests==2.31.0
setuptools==69.1.0
shapely==2.0.3
six==1.16.0
urllib3==2.2.1