From 05e0cc393a826f0d0f823e6532333d05525afd2a Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Tue, 27 Feb 2024 16:46:11 +0100 Subject: [PATCH] Complete camera control UI --- .../public/stylesheets/other/toolbar.css | 2 +- .../public/stylesheets/panels/unitcontrol.css | 2 +- .../server/public/stylesheets/style/style.css | 189 ++++++++++-------- .../olympus/images/buttons/camera/linked.svg | 36 ++++ .../images/buttons/visibility/airbase.svg | 11 +- .../images/buttons/visibility/aircraft.svg | 2 +- .../buttons/visibility/groundunit-sam.svg | 6 +- .../images/buttons/visibility/groundunit.svg | 4 +- .../visibility/head-side-virus-solid.svg | 1 - .../images/buttons/visibility/helicopter.svg | 3 +- .../images/buttons/visibility/navyunit.svg | 2 +- .../images/buttons/visibility/olympus.svg | 1 + .../themes/olympus/images/icons/camera.svg | 34 ++++ .../server/views/toolbars/commandmode.ejs | 3 +- frontend/server/views/toolbars/primary.ejs | 49 ++++- frontend/website/src/constants/constants.ts | 3 +- frontend/website/src/controls/dropdown.ts | 7 + frontend/website/src/map/map.ts | 109 ++++++++-- .../website/src/mission/missionmanager.ts | 4 +- frontend/website/src/other/utils.ts | 46 ++++- .../website/src/toolbars/primarytoolbar.ts | 7 + frontend/website/src/unit/unit.ts | 2 +- scripts/python/http_example.py | 23 +++ 23 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 frontend/server/public/themes/olympus/images/buttons/camera/linked.svg delete mode 100644 frontend/server/public/themes/olympus/images/buttons/visibility/head-side-virus-solid.svg create mode 100644 frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg create mode 100644 frontend/server/public/themes/olympus/images/icons/camera.svg create mode 100644 scripts/python/http_example.py diff --git a/frontend/server/public/stylesheets/other/toolbar.css b/frontend/server/public/stylesheets/other/toolbar.css index 8c3ec441..5fdf1490 100644 --- a/frontend/server/public/stylesheets/other/toolbar.css +++ b/frontend/server/public/stylesheets/other/toolbar.css @@ -64,7 +64,7 @@ width: 20px; } -#toolbar-container>*:nth-child(3)>svg { +#command-mode-toolbar>svg { display: none; } diff --git a/frontend/server/public/stylesheets/panels/unitcontrol.css b/frontend/server/public/stylesheets/panels/unitcontrol.css index 1040ac3b..a5f41c02 100644 --- a/frontend/server/public/stylesheets/panels/unitcontrol.css +++ b/frontend/server/public/stylesheets/panels/unitcontrol.css @@ -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; diff --git a/frontend/server/public/stylesheets/style/style.css b/frontend/server/public/stylesheets/style/style.css index b1bb100b..3ce629ca 100644 --- a/frontend/server/public/stylesheets/style/style.css +++ b/frontend/server/public/stylesheets/style/style.css @@ -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.off.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); +} diff --git a/frontend/server/public/themes/olympus/images/buttons/camera/linked.svg b/frontend/server/public/themes/olympus/images/buttons/camera/linked.svg new file mode 100644 index 00000000..86e90e32 --- /dev/null +++ b/frontend/server/public/themes/olympus/images/buttons/camera/linked.svg @@ -0,0 +1,36 @@ + + diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg index 28ad42a4..af684050 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg @@ -50,27 +50,26 @@ diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg index 09a9e322..593e7643 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg @@ -36,6 +36,6 @@ diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg index 2a568fbb..3198968b 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg @@ -38,12 +38,12 @@ \ No newline at end of file diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg index d6eebd8a..c333457b 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg @@ -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"/> diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg index d4ff6fec..87f20c12 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg @@ -36,6 +36,6 @@ diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg new file mode 100644 index 00000000..c13a36a3 --- /dev/null +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/server/public/themes/olympus/images/icons/camera.svg b/frontend/server/public/themes/olympus/images/icons/camera.svg new file mode 100644 index 00000000..71f65572 --- /dev/null +++ b/frontend/server/public/themes/olympus/images/icons/camera.svg @@ -0,0 +1,34 @@ + + diff --git a/frontend/server/views/toolbars/commandmode.ejs b/frontend/server/views/toolbars/commandmode.ejs index 1954bc66..69000053 100644 --- a/frontend/server/views/toolbars/commandmode.ejs +++ b/frontend/server/views/toolbars/commandmode.ejs @@ -3,5 +3,6 @@
Spawn points
- + \ No newline at end of file diff --git a/frontend/server/views/toolbars/primary.ejs b/frontend/server/views/toolbars/primary.ejs index 70e130b8..7b1fbe3f 100644 --- a/frontend/server/views/toolbars/primary.ejs +++ b/frontend/server/views/toolbars/primary.ejs @@ -6,7 +6,8 @@

DCS Olympus

-
version {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}
+
version {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}} +
Discord @@ -31,14 +32,16 @@
-
ArcGIS Satellite
+
ArcGIS Satellite
-
Options
+
Options +
@@ -57,17 +60,51 @@
+ data-on-click-params='{ "coalition": "blue" }' title="Toggle Blue coalition visibility">
+ data-on-click-params='{ "coalition": "red" }' title="Toggle Red coalition visibility">
+ data-on-click-params='{ "coalition": "neutral" }' title="Toggle Neutral coalition visibility">
+ + \ No newline at end of file diff --git a/frontend/website/src/constants/constants.ts b/frontend/website/src/constants/constants.ts index fc96f47a..a0fa35cf 100644 --- a/frontend/website/src/constants/constants.ts +++ b/frontend/website/src/constants/constants.ts @@ -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, diff --git a/frontend/website/src/controls/dropdown.ts b/frontend/website/src/controls/dropdown.ts index 38014dc9..99694154 100644 --- a/frontend/website/src/controls/dropdown.ts +++ b/frontend/website/src/controls/dropdown.ts @@ -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 diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index 4c82614c..6629c59c 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -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, deg2rad, getGroundElevation, 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 { 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 } 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; @@ -94,7 +99,7 @@ export class Map extends L.Map { #mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS; #mapVisibilityOptionsDropdown: Dropdown; #optionButtons: { [key: string]: HTMLButtonElement[] } = {} - #visibilityOptions: { [key: string]: boolean } = {} + #visibilityOptions: { [key: string]: boolean | string | number } = {} #hiddenTypes: string[] = []; /** @@ -158,7 +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) => this.#broadcastPosition(e)); + this.on('move', (e: any) => { if (this.#slaveDCSCamera) this.#broadcastPosition() }); /* Event listeners */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { @@ -202,6 +207,7 @@ 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", () => { @@ -216,6 +222,18 @@ export class Map extends L.Map { } }) + document.addEventListener("toggleCameraLinkStatus", () => { + if (this.#slaveDCSCameraAvailable) { + this.setSlaveDCSCamera(!this.#slaveDCSCamera); + } + }) + + document.addEventListener("slewCameraToPosition", () => { + if (this.#slaveDCSCameraAvailable) { + this.#broadcastPosition(); + } + }) + /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) @@ -223,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); @@ -238,9 +265,14 @@ 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) { @@ -538,6 +570,21 @@ export class Map extends L.Map { return this.#mapMarkerVisibilityControls; } + setSlaveDCSCamera(newSlaveDCSCamera: boolean) { + if (this.#slaveDCSCameraAvailable || !newSlaveDCSCamera) { + 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; + this.#broadcastPosition(); + } + /* Event handlers */ #onClick(e: any) { if (!this.#preventLeftClick) { @@ -720,27 +767,26 @@ export class Map extends L.Map { this.#isZooming = false; } - #broadcastPosition(e: any) { + #broadcastPosition() { getGroundElevation(this.getCenter(), (response: string) => { var groundElevation: number | null = null; try { groundElevation = parseFloat(response); var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("PUT", "http://localhost:8080"); + xmlHttp.open("PUT", `http://localhost:${this.#cameraControlPort}`); xmlHttp.setRequestHeader("Content-Type", "application/json"); - const C = 40075016.686; + 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})); + 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") } }); - } /* */ @@ -909,8 +955,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); + 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://localhost:${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(""); + } } diff --git a/frontend/website/src/mission/missionmanager.ts b/frontend/website/src/mission/missionmanager.ts index fb557e33..0cbe8232 100644 --- a/frontend/website/src/mission/missionmanager.ts +++ b/frontend/website/src/mission/missionmanager.ts @@ -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); } } } diff --git a/frontend/website/src/other/utils.ts b/frontend/website/src/other/utils.ts index 0476a610..f8e6d5ff 100644 --- a/frontend/website/src/other/utils.ts +++ b/frontend/website/src/other/utils.ts @@ -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(); diff --git a/frontend/website/src/toolbars/primarytoolbar.ts b/frontend/website/src/toolbars/primarytoolbar.ts index ad0aa625..a9d477c1 100644 --- a/frontend/website/src/toolbars/primarytoolbar.ts +++ b/frontend/website/src/toolbars/primarytoolbar.ts @@ -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() { diff --git a/frontend/website/src/unit/unit.ts b/frontend/website/src/unit/unit.ts index 6166ce41..ab8f8b9a 100644 --- a/frontend/website/src/unit/unit.ts +++ b/frontend/website/src/unit/unit.ts @@ -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)) } diff --git a/scripts/python/http_example.py b/scripts/python/http_example.py new file mode 100644 index 00000000..5b857886 --- /dev/null +++ b/scripts/python/http_example.py @@ -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 \ No newline at end of file