Merge pull request #851 from Pax1601/camera-control
Implemented camera control from Olympus to DCS
3
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
#toolbar-container>*:nth-child(3)>svg {
|
||||
#command-mode-toolbar>svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 |
@ -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 |
@ -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 |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.8 KiB |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
16
frontend/website/@types/olympus/index.d.ts
vendored
@ -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" {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
@ -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>
|
||||
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
|
||||
"username": null,
|
||||
"password": null
|
||||
}
|
||||
},
|
||||
"additionalMaps": {
|
||||
}
|
||||
}
|
||||
}
|
||||
214
scripts/lua/backend/OlympusCameraControl.lua
Normal 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
|
||||
23
scripts/python/http_example.py
Normal 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
|
||||
16
scripts/python/map_generator/.vscode/launch.json
vendored
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
37
scripts/python/map_generator/airbases_to_kml.py
Normal 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))
|
||||
@ -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)]
|
||||
}
|
||||
@ -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&id=2000&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&id=2000&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>
|
||||
@ -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)]
|
||||
}
|
||||
@ -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)]
|
||||
}
|
||||
@ -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"}
|
||||
84
scripts/python/map_generator/configs/NTTR/boundary.kml
Normal 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&id=2000&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&id=2000&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>
|
||||
5
scripts/python/map_generator/configs/NTTR/config.yml
Normal 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)]
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
'width': 1920, # The width of your screen, in pixels
|
||||
'height': 1080 # The height of your screen, in pixels
|
||||
}
|
||||
84
scripts/python/map_generator/main.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
320
scripts/python/map_generator/map_generator.py
Normal 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)]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
23
scripts/python/map_generator/requirements.txt
Normal 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
|
||||