diff --git a/.gitignore b/.gitignore
index 047617ad..5ded2391 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ server
node_modules
/client/TODO.txt
/client/public/javascripts/bundle.js
+!client/bin
\ No newline at end of file
diff --git a/client/TODO.txt b/client/TODO.txt
index 343d85e2..43ba41c2 100644
--- a/client/TODO.txt
+++ b/client/TODO.txt
@@ -1,11 +1,9 @@
-Change cursor when in moving mode
-Show airfields and enable airfield spawn
RTB
tanker
scenario dropdown
explosion
wrong name for ground units
-ground units don't move
improve map zIndex
fuel is wrong (either 0 or 1, its is casting it to int somewhere)
-weapons should not be selected
\ No newline at end of file
+weapons should not be selectable
+human symbol if user
\ No newline at end of file
diff --git a/client/package-lock.json b/client/package-lock.json
index 3dee87cc..4e0499d0 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,11 +1,11 @@
{
- "name": "DCS Olympus",
+ "name": "DCSOlympus",
"version": "0.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
- "name": "DCS Olympus",
+ "name": "DCSOlympus",
"version": "0.0.0",
"dependencies": {
"@types/geojson": "^7946.0.10",
@@ -23,6 +23,7 @@
"@types/gtag.js": "^0.0.12",
"browserify": "^17.0.0",
"concurrently": "^7.6.0",
+ "nodemon": "^2.0.20",
"tsify": "^5.0.4",
"typescript": "^4.9.4",
"watchify": "^4.0.0"
@@ -47,6 +48,12 @@
"@types/geojson": "*"
}
},
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -1515,6 +1522,12 @@
}
]
},
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
+ },
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -1979,6 +1992,94 @@
"node": ">= 0.6"
}
},
+ "node_modules/nodemon": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
+ "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "simple-update-notifier": "^1.0.7",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/nodemon/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/nodemon/node_modules/semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/nodemon/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -2186,6 +2287,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
"node_modules/public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -2493,6 +2600,27 @@
}
]
},
+ "node_modules/simple-update-notifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
+ "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
+ "dev": true,
+ "dependencies": {
+ "semver": "~7.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/simple-update-notifier/node_modules/semver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
+ "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -2774,6 +2902,18 @@
"node": ">=8.0"
}
},
+ "node_modules/touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "dev": true,
+ "dependencies": {
+ "nopt": "~1.0.10"
+ },
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -2884,6 +3024,12 @@
"undeclared-identifiers": "bin.js"
}
},
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -3102,6 +3248,12 @@
"@types/geojson": "*"
}
},
+ "abbrev": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
+ "dev": true
+ },
"accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -4294,6 +4446,12 @@
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
},
+ "ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
+ },
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -4661,6 +4819,71 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="
},
+ "nodemon": {
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
+ "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==",
+ "dev": true,
+ "requires": {
+ "chokidar": "^3.5.2",
+ "debug": "^3.2.7",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^5.7.1",
+ "simple-update-notifier": "^1.0.7",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "semver": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
+ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "nopt": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
+ "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@@ -4829,6 +5052,12 @@
"ipaddr.js": "1.9.1"
}
},
+ "pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
"public-encrypt": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
@@ -5083,6 +5312,23 @@
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"dev": true
},
+ "simple-update-notifier": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
+ "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
+ "dev": true,
+ "requires": {
+ "semver": "~7.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz",
+ "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==",
+ "dev": true
+ }
+ }
+ },
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -5314,6 +5560,15 @@
"is-number": "^7.0.0"
}
},
+ "touch": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
+ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
+ "dev": true,
+ "requires": {
+ "nopt": "~1.0.10"
+ }
+ },
"tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -5398,6 +5653,12 @@
"xtend": "^4.0.1"
}
},
+ "undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
+ },
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/client/package.json b/client/package.json
index e26205f6..f96322ca 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,5 +1,5 @@
{
- "name": "DCS Olympus",
+ "name": "DCSOlympus",
"node-main": "./bin/www",
"main": "http://localhost:3000",
"version": "0.0.0",
@@ -25,6 +25,7 @@
"@types/gtag.js": "^0.0.12",
"browserify": "^17.0.0",
"concurrently": "^7.6.0",
+ "nodemon": "^2.0.20",
"tsify": "^5.0.4",
"typescript": "^4.9.4",
"watchify": "^4.0.0"
diff --git a/client/public/images/BEBlue.png b/client/public/images/BEBlue.png
new file mode 100644
index 00000000..93338e8a
Binary files /dev/null and b/client/public/images/BEBlue.png differ
diff --git a/client/public/images/BERed.png b/client/public/images/BERed.png
new file mode 100644
index 00000000..367984f9
Binary files /dev/null and b/client/public/images/BERed.png differ
diff --git a/client/public/images/bullseye.png b/client/public/images/bullseye.png
new file mode 100644
index 00000000..f7024954
Binary files /dev/null and b/client/public/images/bullseye.png differ
diff --git a/client/public/images/bullseye.xcf b/client/public/images/bullseye.xcf
new file mode 100644
index 00000000..097722f6
Binary files /dev/null and b/client/public/images/bullseye.xcf differ
diff --git a/client/public/images/bullseye0.png b/client/public/images/bullseye0.png
new file mode 100644
index 00000000..a51fe39f
Binary files /dev/null and b/client/public/images/bullseye0.png differ
diff --git a/client/public/images/bullseye1.png b/client/public/images/bullseye1.png
new file mode 100644
index 00000000..6d574bbd
Binary files /dev/null and b/client/public/images/bullseye1.png differ
diff --git a/client/public/images/bullseye2.png b/client/public/images/bullseye2.png
new file mode 100644
index 00000000..4e99f79e
Binary files /dev/null and b/client/public/images/bullseye2.png differ
diff --git a/client/public/images/buttons/create.svg b/client/public/images/buttons/create.svg
new file mode 100644
index 00000000..bc76cb21
--- /dev/null
+++ b/client/public/images/buttons/create.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/images/buttons/erase.svg b/client/public/images/buttons/erase.svg
new file mode 100644
index 00000000..f6588839
--- /dev/null
+++ b/client/public/images/buttons/erase.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/images/icons/formation-end.svg b/client/public/images/icons/formation-end.svg
new file mode 100644
index 00000000..f8235368
--- /dev/null
+++ b/client/public/images/icons/formation-end.svg
@@ -0,0 +1,54 @@
+
+
+
+
diff --git a/client/public/images/icons/formation-middle.svg b/client/public/images/icons/formation-middle.svg
new file mode 100644
index 00000000..edf78e19
--- /dev/null
+++ b/client/public/images/icons/formation-middle.svg
@@ -0,0 +1,54 @@
+
+
+
+
diff --git a/client/public/images/icons/formation.png b/client/public/images/icons/formation.png
new file mode 100644
index 00000000..b140e9f6
Binary files /dev/null and b/client/public/images/icons/formation.png differ
diff --git a/client/public/images/icons/leader.png b/client/public/images/icons/leader.png
new file mode 100644
index 00000000..8744386f
Binary files /dev/null and b/client/public/images/icons/leader.png differ
diff --git a/client/public/images/icons/ruler.svg b/client/public/images/icons/ruler.svg
new file mode 100644
index 00000000..30ddf813
--- /dev/null
+++ b/client/public/images/icons/ruler.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/images/icons/singleton.png b/client/public/images/icons/singleton.png
new file mode 100644
index 00000000..06644361
Binary files /dev/null and b/client/public/images/icons/singleton.png differ
diff --git a/client/public/images/pin.png b/client/public/images/pin.png
new file mode 100644
index 00000000..c6222cd2
Binary files /dev/null and b/client/public/images/pin.png differ
diff --git a/client/public/images/pin.svg b/client/public/images/pin.svg
new file mode 100644
index 00000000..19e92ec2
--- /dev/null
+++ b/client/public/images/pin.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/client/public/images/unit.png b/client/public/images/unit.png
new file mode 100644
index 00000000..ae72bc19
Binary files /dev/null and b/client/public/images/unit.png differ
diff --git a/client/public/javascripts/placeholder b/client/public/javascripts/placeholder
new file mode 100644
index 00000000..e69de29b
diff --git a/client/public/stylesheets/layout.css b/client/public/stylesheets/layout.css
index 7e9cbbaa..10c37c90 100644
--- a/client/public/stylesheets/layout.css
+++ b/client/public/stylesheets/layout.css
@@ -56,16 +56,15 @@ body {
}
#unit-control-buttons {
- position: absolute;
+ position: fixed;
top: 10px;
height: fit-content;
width: fit-content;
- right: 10px;
+ right: 270px;
z-index: 1000;
}
#unit-control-panel {
- display: none;
position: absolute;
top: 10px;
height: fit-content;
@@ -77,12 +76,21 @@ body {
#connection-status-panel {
position: absolute;
height: 30px;
- width: 140px;
+ width: 160px;
bottom: 10px;
right: 10px;
z-index: 1000;
}
+#mouse-info-panel {
+ position: absolute;
+ height: fit-content;
+ width: 160px;
+ bottom: 50px;
+ right: 10px;
+ z-index: 1000;
+}
+
@media only screen and (max-width: 1000px) {
#unit-control-buttons {
top: 50px;
diff --git a/client/public/stylesheets/mouseinfopanel.css b/client/public/stylesheets/mouseinfopanel.css
new file mode 100644
index 00000000..3bf331ad
--- /dev/null
+++ b/client/public/stylesheets/mouseinfopanel.css
@@ -0,0 +1,31 @@
+#mouse-info-panel {
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ row-gap: 5px;
+}
+
+#mouse-info-panel .rectangular-container{
+ width: 100%;
+ font-weight: 600;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 10px;
+ background-color: #FFF3;
+}
+
+#mouse-info-panel img {
+ height: 24px;
+}
+
+#measure-position-container{
+ display: none;
+}
+
+#unit-position-container{
+ display: none;
+}
\ No newline at end of file
diff --git a/client/public/stylesheets/selectionscroll.css b/client/public/stylesheets/selectionscroll.css
index 4d3aceff..70cd38d3 100644
--- a/client/public/stylesheets/selectionscroll.css
+++ b/client/public/stylesheets/selectionscroll.css
@@ -1,20 +1,40 @@
.olympus-selection-scroll-container {
- position: fixed;
- background-color: var(--background-color-dark);
+ position: absolute;
font-size: 12px;
- transition: bottom 0.2s;
- border-radius: 15px;
- box-shadow: 0px 2px 5px #000A;
- width: 180px;
+ border-radius: 5px;
+ width: 220px;
height: fit-content;
- z-index: 1000;
+ z-index: 2000;
max-height: 400px;
- padding: 10px;
+ padding: 8px;
+ display: flex;
+ flex-direction: column;
+ row-gap: 5px;
+ align-items: center;
+}
+
+#olympus-selection-scroll-top-bar {
+ color: white;
+ font-size: 14px;
+ opacity: 1;
+ border-radius: 5px;
+ padding: 5px;
+ background-color: #333D;
+ width: 100%;
+ text-align: center;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 40px;
+ padding-left: 15px;
+ padding-right: 15px;
}
.olympus-selection-scroll {
+ overflow-x: hidden;
overflow-y: auto;
- max-height: calc(400px - 60px);
+ height: 100%;
+ width: 100%;
}
.olympus-selection-scroll::-webkit-scrollbar {
@@ -34,48 +54,51 @@
}
.olympus-selection-scroll-element {
- margin: 2px;
+ border-bottom: 1px solid #FFF5;
color: white;
cursor: pointer;
- font-size: 14px;
+ font-size: 13px;
opacity: 1;
- border-radius: 5px;
- margin: 5px;
- padding: 5px;
+ padding-top: 10px;
+ padding-bottom: 10px;
padding-left: 15px;
+ background-color: var(--background-color-dark);
+ font-weight: 600;
}
-.olympus-selection-scroll-element:hover {
- background-color: var(--highlight-color);
+.olympus-selection-scroll:last-child {
+ border-radius: 5px;
+ border-bottom: 0px transparent !important;
}
.olympus-selection-scroll-container label {
- width: 0;
- height: 0;
+ display: inline-block;
+ width: 40px;
+ height: 24px;
}
.olympus-selection-scroll-container input {
+ display: inline-block;
width: 0;
height: 0;
+ margin: 0px;
}
.olympus-selection-scroll-switch {
position: relative;
display: inline-block;
- left: calc(50% - 10px);
- width: 60px;
- height: 34px;
+ width: 40px;
+ height: 24px;
background-color: var(--active-coalition-color);
border-radius: 999px;
cursor: pointer;
- margin-left: -30px;
}
.olympus-selection-scroll-switch:before {
position: absolute;
content: "";
- height: 26px;
- width: 26px;
+ height: 16px;
+ width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
@@ -85,7 +108,12 @@
}
input:checked+.olympus-selection-scroll-switch:before {
- -webkit-transform: translateX(26px);
- -ms-transform: translateX(26px);
- transform: translateX(26px);
+ -webkit-transform: translateX(16px);
+ -ms-transform: translateX(16px);
+ transform: translateX(16px);
+}
+
+.olympus-selection-scroll-title {
+ font-size: 11px;
+ font-weight: 600;
}
\ No newline at end of file
diff --git a/client/public/stylesheets/slider.css b/client/public/stylesheets/slider.css
new file mode 100644
index 00000000..cddd49ad
--- /dev/null
+++ b/client/public/stylesheets/slider.css
@@ -0,0 +1,47 @@
+.slider-container {
+ width: 100%;
+}
+
+.slider {
+ width: 100%;
+ -webkit-appearance: none;
+ appearance: none;
+ height: 2px;
+ background: #d3d3d3;
+ outline: none;
+ opacity: 0.7;
+ -webkit-transition: .2s;
+ transition: opacity .2s;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.slider:hover {
+ opacity: 1;
+}
+
+.slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 20px;
+ height: 20px;
+ background: gray;
+ cursor: pointer;
+ border-radius: 999px;
+}
+
+.active .slider::-webkit-slider-thumb {
+ background: #5ca7ff;
+}
+
+.slider::-moz-range-thumb {
+ width: 20px;
+ height: 20px;
+ background: gray;
+ cursor: pointer;
+ border-radius: 999px;
+}
+
+.active .slider::-moz-range-thumb {
+ background: #5ca7ff;
+}
\ No newline at end of file
diff --git a/client/public/stylesheets/style.css b/client/public/stylesheets/style.css
index a5c77863..b68fd52a 100644
--- a/client/public/stylesheets/style.css
+++ b/client/public/stylesheets/style.css
@@ -1,4 +1,5 @@
@import url("button.css");
+@import url("slider.css");
@import url("dropdown.css");
@import url("selectionwheel.css");
@@ -12,6 +13,7 @@
@import url("unitcontrolpanel.css");
@import url("visibilitycontrolpanel.css");
@import url("unitinfopanel.css");
+@import url("mouseinfopanel.css");
@import url("layout.css");
@@ -19,14 +21,14 @@
/* Variables definitions */
:root {
--background-color-dark: #202831;
- --background-color-light: #aaaaaa;
+ --background-color-light: #AAA;
--title-color: #d3e9ff;
--text-color: white;
- --blue-coalition-color: #2196F3;
+ --blue-coalition-color: #247be2;
--red-coalition-color: #f32121;
- --neutral-coalition-color: #AAAAAA;
+ --neutral-coalition-color: #202831;
--active-coalition-color: var(--blue-coalition-color);
- --highlight-color: #FFFFFFAA;
+ --highlight-color: #FFF5;
}
* {
@@ -69,15 +71,90 @@ html {
}
.rounded-container {
+ position: relative;
padding: 0.5em;
- background-color: #247be2;
- border-radius: 10px;
width: fit-content;
height: fit-content;
text-align: center;
color: white;
font-size: 12px;
border-radius: 9999px;
+ background-color: gray;
+}
+
+.rounded-container.blue {
+ background-color: var(--blue-coalition-color);
+ border: 1px solid var(--blue-coalition-color);
+}
+
+.rounded-container.red {
+ background-color: var(--red-coalition-color);
+ border: 1px solid var(--red-coalition-color);
+}
+
+.rounded-container.neutral {
+ background-color: var(--neutral-coalition-color);
+}
+
+.rounded-container-small {
+ padding: 0.2em;
+ width: fit-content;
+ height: fit-content;
+ text-align: center;
+ color: black;
+ font-weight: 600;
+ background-color: #FFFA;
+ font-size: 11px;
+ border-radius: 9999px;
+ padding-left: 5px;
+ padding-right: 5px;
+}
+
+.rectangular-button {
+ position: relative;
+ padding: 0.5em;
+ width: fit-content;
+ height: fit-content;
+ text-align: center;
+ color: var(--highlight-color);
+ font-size: 12px;
+ border-radius: 5px;
+ background-color: transparent;
+ border: 1px solid var(--highlight-color);
+ cursor: pointer;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ column-gap: 5px;
+}
+
+.rectangular-button.blue {
+ border: 1px solid var(--blue-coalition-color);
+ color: var(--blue-coalition-color);
+}
+
+.rectangular-button.red {
+ border: 1px solid var(--red-coalition-color);
+ color: var(--red-coalition-color);
+}
+
+.rectangular-button.white {
+ border: 1px solid white;
+ color: white;
+}
+
+.rectangular-button.white>img {
+ filter: invert(100%);
+}
+
+.rectangular-button>img {
+ display: inline-block;
+ height: 18px;
+ width: 18px;
+}
+
+.rectangular-button.red {
+ border: 1px solid var(--red-coalition-color);
}
.vl {
@@ -85,3 +162,26 @@ html {
width: 1px !important;
display: inline-block;
}
+
+.hl {
+ border-top: 1px solid #555;
+ height: 1px !important;
+ display: inline-block;
+}
+
+.measure-box {
+ position: absolute;
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+ background-color: #151b20;
+ border-radius: 5px;
+ width: fit-content;
+ height: fit-content;
+ text-align: center;
+ color: white;
+ font-size: 12px;
+ z-index: 2000;
+ font-weight: 600;
+}
\ No newline at end of file
diff --git a/client/public/stylesheets/unitcontrolpanel.css b/client/public/stylesheets/unitcontrolpanel.css
index 20a2a45c..f01d791a 100644
--- a/client/public/stylesheets/unitcontrolpanel.css
+++ b/client/public/stylesheets/unitcontrolpanel.css
@@ -13,24 +13,86 @@
#unit-control-panel {
display: flex;
+ flex-direction: column;
flex-wrap: wrap;
justify-content: space-between;
align-content: flex-start;
- row-gap: 5px;
+ row-gap: 10px;
padding-left: 30px;
padding-right: 30px;
padding-top: 20px;
padding-bottom: 20px;
}
-/* Common */
-#unit-info-panel>div {
- height: 100%;
+#selected-units-container {
+ display: flex;
+ flex-direction: column;
+ row-gap: 5px;
width: 100%;
+ height: 100%;
}
-#unit-control-panel .rounded-container {
- width: 100%;
+#formation-buttons-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ row-gap: 5px;
+ column-gap: 5px;
+ width: 100%;
+ height: 100%;
+}
+
+#roe-buttons-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ row-gap: 5px;
+ column-gap: 5px;
+ width: 100%;
+ height: 100%;
+}
+
+#reaction-to-threat-buttons-container {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ row-gap: 5px;
+ column-gap: 5px;
+ width: 100%;
+ height: 100%;
+}
+
+#selected-units-container .rounded-container {
+ width: calc(100% - 25px);
+ cursor: pointer;
+ margin-left: 25px;
+}
+
+#selected-units-container .rounded-container.not-selected {
+ background-color: transparent;
+}
+
+#selected-units-container .rounded-container .rounded-container-small {
+ display: inline-block;
+ position: absolute;
+ left: 5px;
+ top: 5px;
+}
+
+#selected-units-container img {
+ height: calc(100% + 6px);
+ display: inline-block;
+ position: absolute;
+ left: -32px;
+ top: -3px;
+}
+
+#selected-units-container img.blue {
+ filter: invert(81%) sepia(6%) saturate(1685%) hue-rotate(181deg) brightness(103%) contrast(92%);
+}
+
+#selected-units-container img.red {
+ filter: invert(93%) sepia(97%) saturate(1174%) hue-rotate(291deg) brightness(105%) contrast(97%);
}
#unit-control-panel #title-label {
@@ -38,4 +100,31 @@
font-size: 14px;
width: 100%;
font-weight: 600;
-}
\ No newline at end of file
+}
+
+#unit-control-panel #section-label {
+ color: white;
+ font-size: 13px;
+ width: 100%;
+}
+
+.flight-control-slider {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+}
+
+.flight-control-title {
+ font-size: 13px;
+ color: white;
+}
+
+.flight-control-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: gray;
+}
+
+.active .flight-control-value {
+ color: #5ca7ff;
+}
diff --git a/client/src/controls/selectionscroll.ts b/client/src/controls/selectionscroll.ts
index 3a341605..7a518371 100644
--- a/client/src/controls/selectionscroll.ts
+++ b/client/src/controls/selectionscroll.ts
@@ -1,5 +1,5 @@
import { LatLng } from "leaflet";
-import { setActiveCoalition } from "..";
+import { getActiveCoalition, setActiveCoalition } from "..";
export class SelectionScroll {
#container: HTMLElement | null;
@@ -15,14 +15,17 @@ export class SelectionScroll {
}
}
- show(x: number, y: number, options: any, callback: CallableFunction, showCoalition: boolean) {
+ show(x: number, y: number, title: string, options: any, callback: CallableFunction, showCoalition: boolean) {
/* Hide to remove buttons, if present */
this.hide();
if (this.#container != null && options.length >= 1) {
+ var titleDiv = this.#container.querySelector("#olympus-selection-scroll-top-bar")?.querySelector(".olympus-selection-scroll-title");
+ if (titleDiv)
+ titleDiv.innerHTML = title;
this.#container.style.display = this.#display;
- this.#container.style.left = x - 110 + "px";
- this.#container.style.top = y - 110 + "px";
+ this.#container.style.left = x - this.#container.offsetWidth / 2 + "px";
+ this.#container.style.top = y - 20 + "px";
var scroll = this.#container.querySelector(".olympus-selection-scroll");
if (scroll != null)
{
@@ -40,6 +43,20 @@ export class SelectionScroll {
scroll.appendChild(node);
}
}
+
+ /* Hide the coalition switch if required */
+ var switchContainer = this.#container.querySelector("#olympus-selection-scroll-top-bar")?.querySelector("#coalition-switch-container");
+ if (showCoalition == false) {
+ switchContainer.style.display = "none";
+ document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--neutral-coalition-color"));
+ }
+ else {
+ switchContainer.style.display = "block";
+ if (getActiveCoalition() == "blue")
+ document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--blue-coalition-color"));
+ else
+ document.documentElement.style.setProperty('--active-coalition-color', getComputedStyle(this.#container).getPropertyValue("--red-coalition-color"));
+ }
}
}
diff --git a/client/src/controls/slider.ts b/client/src/controls/slider.ts
new file mode 100644
index 00000000..38b2a49e
--- /dev/null
+++ b/client/src/controls/slider.ts
@@ -0,0 +1,83 @@
+export class Slider {
+ #container: HTMLElement | null;
+ #callback: CallableFunction;
+ #slider: HTMLInputElement | null = null;
+ #value: HTMLElement | null = null;
+ #minValue: number;
+ #maxValue: number;
+ #minValueDiv: HTMLElement | null = null;
+ #maxValueDiv: HTMLElement | null = null;
+ #unit: string;
+ #display: string = "";
+
+ constructor(ID: string, minValue: number, maxValue: number, unit: string, callback: CallableFunction) {
+ this.#container = document.getElementById(ID);
+ this.#callback = callback;
+ this.#minValue = minValue;
+ this.#maxValue = maxValue;
+ this.#unit = unit;
+ if (this.#container != null) {
+ this.#display = this.#container.style.display;
+ this.#slider = this.#container.querySelector("input");
+ if (this.#slider != null)
+ {
+ this.#slider.addEventListener("input", (e: any) => this.#onInput());
+ this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize());
+ }
+ this.#value = this.#container.querySelector("#value");
+ }
+ }
+
+ #onValue()
+ {
+ if (this.#value != null && this.#slider != null)
+ this.#value.innerHTML = this.#minValue + Math.round(parseFloat(this.#slider.value) / 100 * (this.#maxValue - this.#minValue)) + this.#unit
+ this.setActive(true);
+ }
+
+ #onInput()
+ {
+ this.#onValue();
+ }
+
+ #onFinalize()
+ {
+ if (this.#slider != null)
+ this.#callback(this.#minValue + parseFloat(this.#slider.value) / 100 * (this.#maxValue - this.#minValue));
+ }
+
+ show()
+ {
+ if (this.#container != null)
+ this.#container.style.display = this.#display;
+ }
+
+ hide()
+ {
+ if (this.#container != null)
+ this.#container.style.display = 'none';
+ }
+
+ setActive(newActive: boolean)
+ {
+ if (this.#container)
+ {
+ this.#container.classList.toggle("active", newActive);
+ if (!newActive && this.#value != null)
+ this.#value.innerHTML = "Mixed values"
+ }
+ }
+
+ setMinMax(newMinValue: number, newMaxValue: number)
+ {
+ this.#minValue = newMinValue;
+ this.#maxValue = newMaxValue;
+ }
+
+ setValue(newValue: number)
+ {
+ if (this.#slider != null)
+ this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * 100);
+ this.#onValue()
+ }
+}
\ No newline at end of file
diff --git a/client/src/dcs/dcs.ts b/client/src/dcs/dcs.ts
index 581e1a0f..c9ae53f7 100644
--- a/client/src/dcs/dcs.ts
+++ b/client/src/dcs/dcs.ts
@@ -89,7 +89,7 @@ export function attackUnit(ID: number, targetID: number) {
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
- console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " attack " + getUnitsManager().getUnitByID(targetID).unitName);
+ //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " attack " + getUnitsManager().getUnitByID(targetID).unitName);
}
};
@@ -99,29 +99,45 @@ export function attackUnit(ID: number, targetID: number) {
xhr.send(JSON.stringify(data));
}
-export function cloneUnit(ID: number) {
+export function cloneUnit(ID: number, latlng: L.LatLng) {
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
- console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned");
+ //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned");
}
};
- var command = { "ID": ID };
+ var command = { "ID": ID, "location": latlng };
var data = { "cloneUnit": command }
xhr.send(JSON.stringify(data));
}
+export function landAt(ID: number, latlng: L.LatLng) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", RESTaddress);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === 4) {
+ //console.log("Unit " + getUnitsManager().getUnitByID(ID).unitName + " cloned");
+ }
+ };
+
+ var command = { "ID": ID, "location": latlng };
+ var data = { "landAt": command }
+
+ xhr.send(JSON.stringify(data));
+}
+
export function changeSpeed(ID: number, speedChange: string) {
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
- console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
}
};
@@ -131,18 +147,98 @@ export function changeSpeed(ID: number, speedChange: string) {
xhr.send(JSON.stringify(data));
}
+export function setSpeed(ID: number, speed: number) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", RESTaddress);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === 4) {
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
+ }
+ };
+
+ var command = {"ID": ID, "speed": speed}
+ var data = {"setSpeed": command}
+
+ xhr.send(JSON.stringify(data));
+}
+
export function changeAltitude(ID: number, altitudeChange: string) {
var xhr = new XMLHttpRequest();
xhr.open("PUT", RESTaddress);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
- console.log(getUnitsManager().getUnitByID(ID).unitName + " altitude change request: " + altitudeChange);
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " altitude change request: " + altitudeChange);
}
};
var command = {"ID": ID, "change": altitudeChange}
var data = {"changeAltitude": command}
+ xhr.send(JSON.stringify(data));
+}
+
+export function setAltitude(ID: number, altitude: number) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", RESTaddress);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === 4) {
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
+ }
+ };
+
+ var command = {"ID": ID, "altitude": altitude}
+ var data = {"setAltitude": command}
+
+ xhr.send(JSON.stringify(data));
+}
+
+export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", RESTaddress);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === 4) {
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " created formation with: " + wingmenIDs);
+ }
+ };
+
+ var command = {"ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader}
+ var data = {"setLeader": command}
+
+ xhr.send(JSON.stringify(data));
+}
+
+export function setROE(ID: number, ROE: string) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", RESTaddress);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === 4) {
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
+ }
+ };
+
+ var command = {"ID": ID, "ROE": ROE}
+ var data = {"setROE": command}
+
+ xhr.send(JSON.stringify(data));
+}
+
+export function setReactionToThreat(ID: number, reactionToThreat: string) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", RESTaddress);
+ xhr.setRequestHeader("Content-Type", "application/json");
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState === 4) {
+ //console.log(getUnitsManager().getUnitByID(ID).unitName + " speed change request: " + speedChange);
+ }
+ };
+
+ var command = {"ID": ID, "reactionToThreat": reactionToThreat}
+ var data = {"setReactionToThreat": command}
+
xhr.send(JSON.stringify(data));
}
\ No newline at end of file
diff --git a/client/src/index.ts b/client/src/index.ts
index 94f0330c..6534ade8 100644
--- a/client/src/index.ts
+++ b/client/src/index.ts
@@ -8,19 +8,25 @@ import { Dropdown } from "./controls/dropdown";
import { ConnectionStatusPanel } from "./panels/connectionstatuspanel";
import { Button } from "./controls/button";
import { MissionData } from "./missiondata/missiondata";
+import { UnitControlPanel } from "./panels/unitcontrolpanel";
+import { MouseInfoPanel } from "./panels/mouseInfoPanel";
+import { Slider } from "./controls/slider";
/* TODO: should this be a class? */
var map: Map;
var selectionWheel: SelectionWheel;
var selectionScroll: SelectionScroll;
+
var unitsManager: UnitsManager;
+var missionData: MissionData;
+
var unitInfoPanel: UnitInfoPanel;
-var activeCoalition: string;
+var connectionStatusPanel: ConnectionStatusPanel;
+var unitControlPanel: UnitControlPanel;
+var mouseInfoPanel: MouseInfoPanel;
+
var scenarioDropdown: Dropdown;
var mapSourceDropdown: Dropdown;
-var connected: boolean;
-var connectionStatusPanel: ConnectionStatusPanel;
-var missionData: MissionData;
var slowButton: Button;
var fastButton: Button;
@@ -31,6 +37,12 @@ var aiVisibilityButton: Button;
var weaponVisibilityButton: Button;
var deadVisibilityButton: Button;
+var altitudeSlider: Slider;
+var airspeedSlider: Slider;
+
+var connected: boolean;
+var activeCoalition: string;
+
function setup() {
/* Initialize */
map = new Map('map-container');
@@ -38,9 +50,11 @@ function setup() {
selectionScroll = new SelectionScroll("selection-scroll");
unitsManager = new UnitsManager();
unitInfoPanel = new UnitInfoPanel("unit-info-panel");
+ unitControlPanel = new UnitControlPanel("unit-control-panel");
scenarioDropdown = new Dropdown("scenario-dropdown", ["Caucasus", "Marianas", "Nevada", "South Atlantic", "Syria", "The Channel"], () => { });
mapSourceDropdown = new Dropdown("map-source-dropdown", map.getLayers(), (option: string) => map.setLayer(option));
connectionStatusPanel = new ConnectionStatusPanel("connection-status-panel");
+ mouseInfoPanel = new MouseInfoPanel("mouse-info-panel");
missionData = new MissionData();
/* Unit control buttons */
@@ -49,6 +63,10 @@ function setup() {
climbButton = new Button("climb-button", ["images/buttons/climb.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("climb"); });
descendButton = new Button("descend-button", ["images/buttons/descend.svg"], () => { getUnitsManager().selectedUnitsChangeAltitude("descend"); });
+ /* Unit control sliders */
+ altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => getUnitsManager().selectedUnitsSetAltitude(value * 0.3048));
+ airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => getUnitsManager().selectedUnitsSetSpeed(value / 1.94384));
+
/* Visibility buttons */
userVisibilityButton = new Button("user-visibility-button", ["images/buttons/user-full.svg", "images/buttons/user-partial.svg", "images/buttons/user-none.svg", "images/buttons/user-hidden.svg"], () => { });
aiVisibilityButton = new Button("ai-visibility-button", ["images/buttons/ai-full.svg", "images/buttons/ai-partial.svg", "images/buttons/ai-none.svg", "images/buttons/ai-hidden.svg"], () => { });
@@ -93,6 +111,10 @@ export function getMap() {
return map;
}
+export function getMissionData() {
+ return missionData;
+}
+
export function getSelectionWheel() {
return selectionWheel;
}
@@ -109,6 +131,14 @@ export function getUnitInfoPanel() {
return unitInfoPanel;
}
+export function getUnitControlPanel() {
+ return unitControlPanel;
+}
+
+export function getMouseInfoPanel() {
+ return mouseInfoPanel;
+}
+
export function setActiveCoalition(newActiveCoalition: string) {
activeCoalition = newActiveCoalition;
}
@@ -173,4 +203,12 @@ export function getVisibilitySettings() {
return visibility;
}
+export function getVisibilityButtons() {
+ return {user: userVisibilityButton, ai: aiVisibilityButton, weapon: weaponVisibilityButton, dead: deadVisibilityButton}
+}
+
+export function getUnitControlSliders() {
+ return {altitude: altitudeSlider, airspeed: airspeedSlider}
+}
+
window.onload = setup;
\ No newline at end of file
diff --git a/client/src/map/boxselect.ts b/client/src/map/boxselect.ts
index 2f44853e..5a742bea 100644
--- a/client/src/map/boxselect.ts
+++ b/client/src/map/boxselect.ts
@@ -45,7 +45,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseDown: function (e: any) {
- if (((e.which !== 3) && (e.button !== 2))) { return false; }
+ if (((e.which !== 1) && (e.button !== 0))) { return false; }
// Clear the deferred resetState if it hasn't executed yet, otherwise it
// will interrupt the interaction and orphan a box element in the container.
@@ -107,7 +107,7 @@ export var BoxSelect = Handler.extend({
},
_onMouseUp: function (e: any) {
- if ((e.which !== 3) && (e.button !== 2)) { return; }
+ if ((e.which !== 1) && (e.button !== 0)) { return; }
this._finish();
diff --git a/client/src/map/map.ts b/client/src/map/map.ts
index febe7e67..5dbc9089 100644
--- a/client/src/map/map.ts
+++ b/client/src/map/map.ts
@@ -1,6 +1,7 @@
import * as L from "leaflet"
-import { getSelectionWheel, getSelectionScroll, getUnitsManager, getActiveCoalition } from "..";
+import { getSelectionWheel, getSelectionScroll, getUnitsManager, getActiveCoalition, getMouseInfoPanel } from "..";
import { spawnAircraft, spawnGroundUnit, spawnSmoke } from "../dcs/dcs";
+import { bearing, distance, zeroAppend } from "../other/utils";
import { payloadNames } from "../units/payloadNames";
import { unitTypes } from "../units/unitTypes";
import { BoxSelect } from "./boxselect";
@@ -21,8 +22,14 @@ export interface SpawnEvent extends ClickEvent{
export class Map extends L.Map {
#state: string;
#layer?: L.TileLayer;
- #preventRightClick: boolean = false;
- #rightClickTimer: number = 0;
+ #preventLeftClick: boolean = false;
+ #leftClickTimer: number = 0;
+ #measurePoint: L.LatLng | null;
+ #measureIcon: L.Icon;
+ #measureMarker: L.Marker;
+ #measureLine: L.Polyline = new L.Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1, interactive: false });
+ #measureLineDiv: HTMLElement;
+ #lastMousePosition: L.Point = new L.Point(0, 0);
constructor(ID: string) {
/* Init the leaflet map */
@@ -34,12 +41,25 @@ export class Map extends L.Map {
/* Init the state machine */
this.#state = "IDLE";
+ this.#measurePoint = null;
+
+ this.#measureIcon = new L.Icon({ iconUrl: 'images/pin.png', iconAnchor: [16, 32]});
+ this.#measureMarker = new L.Marker([0, 0], {icon: this.#measureIcon, interactive: false});
+ this.#measureLineDiv = document.createElement("div");
+ this.#measureLineDiv.classList.add("measure-box");
+ this.#measureLineDiv.style.display = 'none';
+
+ document.body.appendChild(this.#measureLineDiv);
/* Register event handles */
this.on("click", (e: any) => this.#onClick(e));
this.on("dblclick", (e: any) => this.#onDoubleClick(e));
this.on("contextmenu", (e: any) => this.#onContextMenu(e));
this.on('selectionend', (e: any) => this.#onSelectionEnd(e));
+ this.on('mousedown', (e: any) => this.#onMouseDown(e));
+ this.on('mouseup', (e: any) => this.#onMouseUp(e));
+ this.on('mousemove', (e: any) => this.#onMouseMove(e));
+ this.on('zoom', (e: any) => this.#onZoom(e));
}
setLayer(layerName: string) {
@@ -124,63 +144,133 @@ export class Map extends L.Map {
}
/* Selection scroll */
- showSelectionScroll(e: ClickEvent | SpawnEvent, options: any, callback: CallableFunction, showCoalition: boolean = false) {
+ showSelectionScroll(e: ClickEvent | SpawnEvent, title: string, options: any, callback: CallableFunction, showCoalition: boolean = false) {
var x = e.x;
var y = e.y;
- getSelectionScroll().show(x, y, options, callback, showCoalition);
+ getSelectionScroll().show(x, y, title, options, callback, showCoalition);
}
hideSelectionScroll() {
getSelectionScroll().hide();
}
+ getMousePosition() {
+ return this.#lastMousePosition;
+ }
+
+ getMouseCoordinates() {
+ return this.containerPointToLatLng(this.#lastMousePosition);
+ }
+
/* Event handlers */
#onClick(e: any) {
- this.hideSelectionWheel();
- this.hideSelectionScroll();
- if (this.#state === "IDLE") {
-
- }
- else if (this.#state === "MOVE_UNIT") {
- if (!e.originalEvent.ctrlKey) {
- getUnitsManager().clearDestinations();
+ if (!this.#preventLeftClick) {
+ this.hideSelectionWheel();
+ this.hideSelectionScroll();
+ if (this.#state === "IDLE") {
+ if (e.originalEvent.ctrlKey)
+ if (!this.#measurePoint)
+ {
+ this.#measurePoint = e.latlng;
+ this.#measureMarker.setLatLng(e.latlng);
+ this.#measureMarker.addTo(this);
+ }
+ else
+ {
+ this.#measurePoint = null;
+ if (this.hasLayer(this.#measureMarker))
+ this.removeLayer(this.#measureMarker);
+ }
}
- getUnitsManager().addDestination(e.latlng)
- }
- }
-
- #onDoubleClick(e: any) {
- var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: null, coalitionID: null};
- if (this.#state == "IDLE") {
- var options = [
- { "tooltip": "Spawn air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(spawnEvent) },
- { "tooltip": "Spawn ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(spawnEvent) },
- { "tooltip": "Smoke", "src": "spawnSmoke.png", "callback": () => this.#smokeSpawnMenu(spawnEvent) },
- //{ "tooltip": "Explosion", "src": "spawnExplosion.png", "callback": () => this.#explosionSpawnMenu(e) }
- ]
- this.showSelectionScroll(spawnEvent, options, () => {}, true);
- }
- }
-
- #onContextMenu(e: any) {
- this.#rightClickTimer = setTimeout(() => {
- if (!this.#preventRightClick) {
+ else if (this.#state === "MOVE_UNIT") {
this.setState("IDLE");
getUnitsManager().deselectAllUnits();
this.hideSelectionWheel();
this.hideSelectionScroll();
}
- this.#preventRightClick = false;
- }, 200);
+ }
+ }
+
+ #onDoubleClick(e: any) {
+
+ }
+
+ #onContextMenu(e: any) {
+ this.hideSelectionWheel();
+ this.hideSelectionScroll();
+ if (this.#state === "IDLE") {
+ var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: null, coalitionID: null};
+ if (this.#state == "IDLE") {
+ var options = [
+ { "tooltip": "Spawn air unit", "src": "spawnAir.png", "callback": () => this.#aircraftSpawnMenu(spawnEvent) },
+ { "tooltip": "Spawn ground unit", "src": "spawnGround.png", "callback": () => this.#groundUnitSpawnMenu(spawnEvent) },
+ { "tooltip": "Smoke", "src": "spawnSmoke.png", "callback": () => this.#smokeSpawnMenu(spawnEvent) },
+ //{ "tooltip": "Explosion", "src": "spawnExplosion.png", "callback": () => this.#explosionSpawnMenu(e) }
+ ]
+ this.showSelectionScroll(spawnEvent, "Action", options, () => {}, false);
+ }
+ }
+ else if (this.#state === "MOVE_UNIT") {
+ if (!e.originalEvent.ctrlKey) {
+ getUnitsManager().selectedUnitsClearDestinations();
+ }
+ getUnitsManager().selectedUnitsAddDestination(e.latlng)
+ }
}
#onSelectionEnd(e: any)
{
- clearTimeout(this.#rightClickTimer);
- this.#preventRightClick = true;
+ clearTimeout(this.#leftClickTimer);
+ this.#preventLeftClick = true;
+ this.#leftClickTimer = setTimeout(() => {
+ this.#preventLeftClick = false;
+ }, 200);
getUnitsManager().selectFromBounds(e.selectionBounds);
}
+ #onMouseDown(e: any)
+ {
+ if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0))
+ {
+ this.dragging.disable();
+ }
+ }
+
+ #onMouseUp(e: any)
+ {
+ if ((e.originalEvent.which == 1) && (e.originalEvent.button == 0))
+ {
+ this.dragging.enable();
+ }
+ }
+
+ #onMouseMove(e: any)
+ {
+ var selectedUnitPosition = null;
+ var selectedUnits = getUnitsManager().getSelectedUnits();
+ if (selectedUnits && selectedUnits.length == 1)
+ {
+ selectedUnitPosition = new L.LatLng(selectedUnits[0].latitude, selectedUnits[0].longitude);
+ }
+ getMouseInfoPanel().update(e.latlng, this.#measurePoint, selectedUnitPosition);
+
+ this.#lastMousePosition.x = e.originalEvent.x;
+ this.#lastMousePosition.y = e.originalEvent.y;
+
+ if ( this.#measurePoint)
+ this.#drawMeasureLine();
+ else
+ this.#hideMeasureLine();
+ }
+
+ #onZoom(e: any)
+ {
+ if (this.#measurePoint)
+ this.#drawMeasureLine();
+ else
+ this.#hideMeasureLine();
+ }
+
/* Spawn from air base */
spawnFromAirbase(e: SpawnEvent)
{
@@ -198,7 +288,7 @@ export class Map extends L.Map {
{'coalition': true, 'tooltip': 'Radar', 'src': 'spawnRadar.png', 'callback': () => this.#selectGroundUnit(e, "Radar")},
{'coalition': true, 'tooltip': 'Unarmed', 'src': 'spawnUnarmed.png', 'callback': () => this.#selectGroundUnit(e, "Unarmed")}
]
- this.showSelectionScroll(e, options, () => {}, true);
+ this.showSelectionScroll(e, "Spawn ground unit", options, () => {}, true);
}
#smokeSpawnMenu(e: SpawnEvent) {
@@ -211,7 +301,7 @@ export class Map extends L.Map {
{'tooltip': 'Green smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('green', e.latlng)}, 'tint': 'green'},
{'tooltip': 'Orange smoke', 'src': 'spawnSmoke.png', 'callback': () => {this.hideSelectionWheel(); this.hideSelectionScroll(); spawnSmoke('orange', e.latlng)}, 'tint': 'orange'},
]
- this.showSelectionScroll(e, options, () => {}, true);
+ this.showSelectionScroll(e, "Spawn smoke", options, () => {}, false);
}
#explosionSpawnMenu(e: SpawnEvent) {
@@ -228,7 +318,10 @@ export class Map extends L.Map {
{ 'coalition': true, 'tooltip': 'Drone', 'src': 'spawnDrone.png', 'callback': () => this.#selectAircraft(e, "drone") },
{ 'coalition': true, 'tooltip': 'Transport', 'src': 'spawnTransport.png', 'callback': () => this.#selectAircraft(e, "transport") },
]
- this.showSelectionScroll(e, options, () => {}, true);
+ if (e.airbaseName != null)
+ this.showSelectionScroll(e, "Spawn at " + e.airbaseName, options, () => {}, true);
+ else
+ this.showSelectionScroll(e, "Spawn air unit", options, () => {}, true);
}
/* Show unit selection for air units */
@@ -240,11 +333,11 @@ export class Map extends L.Map {
options.sort();
else
options = [];
- this.showSelectionScroll(e, options, (unitType: string) => {
+ this.showSelectionScroll(e, "Select aircraft", options, (unitType: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
this.#unitSelectPayload(e, unitType);
- });
+ }, true);
}
/* Show weapon selection for air units */
@@ -255,11 +348,11 @@ export class Map extends L.Map {
options = payloadNames[unitType]
if (options != undefined && options.length > 0) {
options.sort();
- this.showSelectionScroll({x: e.x, y: e.y, latlng: e.latlng}, options, (payloadName: string) => {
+ this.showSelectionScroll({x: e.x, y: e.y, latlng: e.latlng}, "Select loadout", options, (payloadName: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
spawnAircraft(unitType, e.latlng, getActiveCoalition(), payloadName, e.airbaseName);
- });
+ }, true);
}
else {
spawnAircraft(unitType, e.latlng, getActiveCoalition());
@@ -273,10 +366,49 @@ export class Map extends L.Map {
this.hideSelectionScroll();
var options = unitTypes.vehicles[group];
options.sort();
- this.showSelectionScroll(e, options, (unitType: string) => {
+ this.showSelectionScroll(e, "Select ground unit", options, (unitType: string) => {
this.hideSelectionWheel();
this.hideSelectionScroll();
spawnGroundUnit(unitType, e.latlng, getActiveCoalition());
- });
+ }, true);
+ }
+
+ #drawMeasureLine()
+ {
+ var mouseLatLng = this.containerPointToLatLng(this.#lastMousePosition);
+ if (this.#measurePoint != null)
+ {
+ var points = [this.#measurePoint, mouseLatLng];
+ this.#measureLine.setLatLngs(points);
+ var dist = distance(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng);
+ var bear = bearing(this.#measurePoint.lat, this.#measurePoint.lng, mouseLatLng.lat, mouseLatLng.lng);
+ var startXY = this.latLngToContainerPoint(this.#measurePoint);
+ var dx = (this.#lastMousePosition.x - startXY.x);
+ var dy = (this.#lastMousePosition.y - startXY.y);
+
+ var angle = Math.atan2(dy, dx);
+ if (angle > Math.PI / 2)
+ angle = angle - Math.PI;
+
+ if (angle < -Math.PI / 2)
+ angle = angle + Math.PI;
+
+ this.#measureLineDiv.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
+ this.#measureLineDiv.style.left = (this.#lastMousePosition.x + startXY.x) / 2 - this.#measureLineDiv.offsetWidth / 2 + "px";
+ this.#measureLineDiv.style.top = (this.#lastMousePosition.y + startXY.y) / 2 - this.#measureLineDiv.offsetHeight / 2 + "px";
+ this.#measureLineDiv.style.rotate = angle + "rad";
+ this.#measureLineDiv.style.display = "";
+ }
+
+ if (!this.hasLayer(this.#measureLine))
+ this.#measureLine.addTo(this);
+ }
+
+ #hideMeasureLine()
+ {
+ this.#measureLineDiv.style.display = "none";
+
+ if (this.hasLayer(this.#measureLine))
+ this.removeLayer(this.#measureLine)
}
}
diff --git a/client/src/missiondata/missiondata.ts b/client/src/missiondata/missiondata.ts
index 392b3600..f5ccedad 100644
--- a/client/src/missiondata/missiondata.ts
+++ b/client/src/missiondata/missiondata.ts
@@ -1,41 +1,56 @@
-import { Marker, LatLng } from "leaflet";
-import { getMap } from "..";
+import { Marker, LatLng, Icon } from "leaflet";
+import { getMap, getUnitsManager } from "..";
import { SpawnEvent } from "../map/map";
import { AirbaseMarker } from "./airbasemarker";
+var bullseyeIcons = [
+ new Icon({ iconUrl: 'images/bullseye0.png', iconAnchor: [30, 30]}),
+ new Icon({ iconUrl: 'images/bullseye1.png', iconAnchor: [30, 30]}),
+ new Icon({ iconUrl: 'images/bullseye2.png', iconAnchor: [30, 30]})
+]
+
export class MissionData
{
- //#bullseye : any; //TODO declare interface
- //#bullseyeMarker : Marker;
+ #bullseyes : any; //TODO declare interface
+ #bullseyeMarkers: any;
#airbases : any; //TODO declare interface
#airbasesMarkers: {[name: string]: AirbaseMarker};
constructor()
{
- //this.#bullseye = undefined;
- //this.#bullseyeMarker = undefined;
+ this.#bullseyes = undefined;
+ this.#bullseyeMarkers = [
+ new Marker([0, 0], {icon: bullseyeIcons[0]}).addTo(getMap()),
+ new Marker([0, 0], {icon: bullseyeIcons[1]}).addTo(getMap()),
+ new Marker([0, 0], {icon: bullseyeIcons[2]}).addTo(getMap())
+ ]
this.#airbasesMarkers = {};
}
update(data: any)
{
- //this.#bullseye = data.missionData.bullseye;
+ this.#bullseyes = data.bullseye;
this.#airbases = data.airbases;
- //this.#drawBullseye();
- this.#drawAirbases();
+ if (this.#bullseyes != null && this.#airbases != null)
+ {
+ this.#drawBullseye();
+ this.#drawAirbases();
+ }
}
- //#drawBullseye()
- //{
- // if (this.#bullseyeMarker === undefined)
- // {
- // this.#bullseyeMarker = new Marker([this.#bullseye.lat, this.#bullseye.lng]).addTo(map.getMap());
- // }
- // else
- // {
- // this.#bullseyeMarker.setLatLng(new LatLng(this.#bullseye.lat, this.#bullseye.lng));
- // }
- //}
+ getBullseyes()
+ {
+ return this.#bullseyes;
+ }
+
+ #drawBullseye()
+ {
+ for (let idx in this.#bullseyes)
+ {
+ var bullseye = this.#bullseyes[idx];
+ this.#bullseyeMarkers[idx].setLatLng(new LatLng(bullseye.lat, bullseye.lng));
+ }
+ }
#drawAirbases()
{
@@ -48,7 +63,7 @@ export class MissionData
position: new LatLng(airbase.lat, airbase.lng),
name: airbase.callsign,
src: "images/airbase.png"}).addTo(getMap());
- this.#airbasesMarkers[idx].on('click', (e) => this.#onAirbaseClick(e));
+ this.#airbasesMarkers[idx].on('contextmenu', (e) => this.#onAirbaseClick(e));
}
else
{
@@ -59,7 +74,25 @@ export class MissionData
#onAirbaseClick(e: any)
{
- var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: e.sourceTarget.getName(), coalitionID: e.sourceTarget.getCoalitionID()};
- getMap().spawnFromAirbase(spawnEvent);
+ var options = [];
+ if (getUnitsManager().getSelectedUnits().length > 0)
+ options = ["Spawn unit", "Land here"];
+ else
+ options = ["Spawn unit"];
+ getMap().showSelectionScroll(e.originalEvent, e.sourceTarget.getName(), options, (option: string) => this.#onAirbaseOptionSelection(e, option), false);
+
+ }
+
+ #onAirbaseOptionSelection(e: any, option: string) {
+ if (option === "Spawn unit") {
+ var spawnEvent: SpawnEvent = {x: e.originalEvent.x, y: e.originalEvent.y, latlng: e.latlng, airbaseName: e.sourceTarget.getName(), coalitionID: e.sourceTarget.getCoalitionID()};
+ getMap().spawnFromAirbase(spawnEvent);
+ }
+ else if (option === "Land here")
+ {
+ getMap().hideSelectionWheel();
+ getMap().hideSelectionScroll();
+ getUnitsManager().selectedUnitsLandAt(e.latlng);
+ }
}
}
\ No newline at end of file
diff --git a/client/src/other/utils.ts b/client/src/other/utils.ts
index 182ba265..4b169360 100644
--- a/client/src/other/utils.ts
+++ b/client/src/other/utils.ts
@@ -26,7 +26,7 @@ export function bearing(lat1: number, lon1: number, lat2: number, lon2: number)
return brng;
}
-const zeroPad = function (num: number, places: number) {
+export const zeroPad = function (num: number, places: number) {
var string = String(num);
while (string.length < places) {
string += "0";
@@ -34,6 +34,14 @@ const zeroPad = function (num: number, places: number) {
return string;
}
+export const zeroAppend = function (num: number, places: number) {
+ var string = String(num);
+ while (string.length < places) {
+ string = "0" + string;
+ }
+ return string;
+}
+
export function ConvertDDToDMS(D: number, lng: boolean) {
var dir = D < 0 ? (lng ? "W" : "S") : lng ? "E" : "N";
var deg = 0 | (D < 0 ? (D = -D) : D);
diff --git a/client/src/panels/mouseInfoPanel.ts b/client/src/panels/mouseInfoPanel.ts
new file mode 100644
index 00000000..74ed0a72
--- /dev/null
+++ b/client/src/panels/mouseInfoPanel.ts
@@ -0,0 +1,73 @@
+import { LatLng } from "leaflet";
+import { getMissionData } from "..";
+import { distance, bearing, zeroPad, zeroAppend } from "../other/utils";
+import { Unit } from "../units/unit";
+
+export class MouseInfoPanel {
+ #element: HTMLElement
+ #display: string;
+
+ constructor(ID: string) {
+ this.#element = document.getElementById(ID);
+ this.#display = '';
+ if (this.#element != null) {
+ this.#display = this.#element.style.display;
+ var el = this.#element.querySelector(`#measure-position`);
+ this.show();
+ }
+ }
+
+ show() {
+ this.#element.style.display = this.#display;
+ }
+
+ hide() {
+ this.#element.style.display = "none";
+ }
+
+ update(mousePosition: LatLng, measurePosition: LatLng | null, unitPosition: LatLng | null) {
+ var bullseyes = getMissionData().getBullseyes();
+ for (let idx in bullseyes)
+ {
+ var dist = distance(bullseyes[idx].lat, bullseyes[idx].lng, mousePosition.lat, mousePosition.lng);
+ var bear = bearing(bullseyes[idx].lat, bullseyes[idx].lng, mousePosition.lat, mousePosition.lng);
+ var el = this.#element.querySelector(`#bullseye-${idx}`);
+ if (el != null)
+ el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
+ }
+
+ if (measurePosition) {
+ var dist = distance(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng);
+ var bear = bearing(measurePosition.lat, measurePosition.lng, mousePosition.lat, mousePosition.lng);
+ var el = this.#element.querySelector(`#measure-position`);
+ if (el != null)
+ {
+ el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
+ if (el.parentElement != null)
+ el.parentElement.style.display = 'flex'; //TODO: don't like that its hardcoded
+ }
+ }
+ else {
+ var el = this.#element.querySelector(`#measure-position`);
+ if (el != null && el.parentElement != null)
+ el.parentElement.style.display = 'none';
+ }
+
+ if (unitPosition) {
+ var dist = distance(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng);
+ var bear = bearing(unitPosition.lat, unitPosition.lng, mousePosition.lat, mousePosition.lng);
+ var el = this.#element.querySelector(`#unit-position`);
+ if (el != null)
+ {
+ el.innerHTML = `${zeroAppend(Math.floor(bear), 3)}° / ${zeroAppend(Math.floor(dist*0.000539957), 3)} NM`
+ if (el.parentElement != null)
+ el.parentElement.style.display = 'flex'; //TODO: don't like that its hardcoded
+ }
+ }
+ else {
+ var el = this.#element.querySelector(`#unit-position`);
+ if (el != null && el.parentElement != null)
+ el.parentElement.style.display = 'none';
+ }
+ }
+}
\ No newline at end of file
diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts
new file mode 100644
index 00000000..9a143ffd
--- /dev/null
+++ b/client/src/panels/unitcontrolpanel.ts
@@ -0,0 +1,363 @@
+import { imageOverlay } from "leaflet";
+import { getUnitControlSliders, getUnitsManager } from "..";
+import { ConvertDDToDMS, rad2deg } from "../other/utils";
+import { Aircraft, AirUnit, GroundUnit, Helicopter, NavyUnit, Unit } from "../units/unit";
+
+export class UnitControlPanel {
+ #element: HTMLElement
+ #display: string;
+
+ constructor(ID: string) {
+ this.#element = document.getElementById(ID);
+ this.#display = '';
+ if (this.#element != null) {
+ this.#display = this.#element.style.display;
+ var formationCreationContainer = (this.#element.querySelector("#formation-creation-container"));
+ if (formationCreationContainer != null)
+ {
+ var createButton = formationCreationContainer.querySelector("#create-formation");
+ createButton?.addEventListener("click", () => getUnitsManager().selectedUnitsCreateFormation());
+
+ var undoButton = formationCreationContainer.querySelector("#undo-formation");
+ undoButton?.addEventListener("click", () => getUnitsManager().selectedUnitsUndoFormation());
+ }
+ var ROEButtonsContainer = (this.#element.querySelector("#roe-buttons-container"));
+ if (ROEButtonsContainer != null)
+ {
+ (ROEButtonsContainer.querySelector("#free"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Free"));
+ (ROEButtonsContainer.querySelector("#designated-free"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Designated free"));
+ (ROEButtonsContainer.querySelector("#designated"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Designated"));
+ (ROEButtonsContainer.querySelector("#return"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Return"));
+ (ROEButtonsContainer.querySelector("#hold"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetROE("Hold"));
+ }
+
+ var reactionToThreatButtonsContainer = (this.#element.querySelector("#reaction-to-threat-buttons-container"));
+ if (reactionToThreatButtonsContainer != null)
+ {
+ (reactionToThreatButtonsContainer.querySelector("#none"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("None"));
+ (reactionToThreatButtonsContainer.querySelector("#passive"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Passive"));
+ (reactionToThreatButtonsContainer.querySelector("#evade"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Evade"));
+ (reactionToThreatButtonsContainer.querySelector("#escape"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Escape"));
+ (reactionToThreatButtonsContainer.querySelector("#abort"))?.addEventListener("click", () => getUnitsManager().selectedUnitsSetReactionToThreat("Abort"));
+ }
+ this.hide();
+ }
+ }
+
+ show() {
+ this.#element.style.display = this.#display;
+ }
+
+ hide() {
+ this.#element.style.display = "none";
+ }
+
+ update(units: Unit[]) {
+ if (this.#element != null)
+ {
+ var selectedUnitsContainer = (this.#element.querySelector("#selected-units-container"));
+ var formationCreationContainer = (this.#element.querySelector("#formation-creation-container"));
+ if (selectedUnitsContainer != null && formationCreationContainer != null)
+ {
+ this.#addUnitsButtons(units, selectedUnitsContainer);
+ this.#showFlightControlSliders(units);
+ this.#showFormationButtons(units, formationCreationContainer);
+ }
+
+ var ROEButtonsContainer = (this.#element.querySelector("#roe-buttons-container"));
+ if (ROEButtonsContainer != null)
+ {
+ (ROEButtonsContainer.querySelector("#free"))?.classList.toggle("white", this.#getROE(units) === "Free");
+ (ROEButtonsContainer.querySelector("#designated-free"))?.classList.toggle("white", this.#getROE(units) === "Designated free");
+ (ROEButtonsContainer.querySelector("#designated"))?.classList.toggle("white", this.#getROE(units) === "Designated");
+ (ROEButtonsContainer.querySelector("#return"))?.classList.toggle("white", this.#getROE(units) === "Return");
+ (ROEButtonsContainer.querySelector("#hold"))?.classList.toggle("white", this.#getROE(units) === "Hold");
+ }
+
+ var reactionToThreatButtonsContainer = (this.#element.querySelector("#reaction-to-threat-buttons-container"));
+ if (reactionToThreatButtonsContainer != null)
+ {
+ (reactionToThreatButtonsContainer.querySelector("#none"))?.classList.toggle("white", this.#getReactionToThreat(units) === "None");
+ (reactionToThreatButtonsContainer.querySelector("#passive"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Passive");
+ (reactionToThreatButtonsContainer.querySelector("#evade"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Evade");
+ (reactionToThreatButtonsContainer.querySelector("#escape"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Escape");
+ (reactionToThreatButtonsContainer.querySelector("#abort"))?.classList.toggle("white", this.#getReactionToThreat(units) === "Abort");
+ }
+ }
+ }
+
+ #showFlightControlSliders(units: Unit[])
+ {
+ var sliders = getUnitControlSliders();
+ sliders.airspeed.show();
+ sliders.altitude.show();
+
+ if (this.#checkAllUnitsAircraft(units))
+ {
+ sliders.airspeed.setMinMax(100, 600);
+ sliders.altitude.setMinMax(0, 50000);
+ }
+ else if (this.#checkAllUnitsHelicopter(units))
+ {
+ sliders.airspeed.setMinMax(0, 200);
+ sliders.altitude.setMinMax(0, 10000);
+ }
+ else if (this.#checkAllUnitsGroundUnit(units))
+ {
+ sliders.airspeed.setMinMax(0, 60);
+ sliders.altitude.hide();
+ }
+ else if (this.#checkAllUnitsNavyUnit(units))
+ {
+ sliders.airspeed.setMinMax(0, 60);
+ sliders.altitude.hide();
+ }
+ else {
+ sliders.airspeed.hide();
+ sliders.altitude.hide();
+ }
+
+ var targetSpeed = this.#getTargetAirspeed(units);
+ if (targetSpeed != null)
+ {
+ sliders.airspeed.setActive(true);
+ sliders.airspeed.setValue(targetSpeed * 1.94384);
+ }
+ else
+ {
+ sliders.airspeed.setActive(false);
+ }
+
+ var targetAltitude = this.#getTargetAltitude(units);
+ if (targetAltitude != null)
+ {
+ sliders.altitude.setActive(true);
+ sliders.altitude.setValue(targetAltitude / 0.3048);
+ }
+ else
+ {
+ sliders.altitude.setActive(false);
+ }
+ }
+
+ #addUnitsButtons(units: Unit[], selectedUnitsContainer: HTMLElement)
+ {
+ /* Remove any pre-existing unit button */
+ var elements = selectedUnitsContainer.getElementsByClassName("js-unit-container");
+ while (elements.length > 0)
+ selectedUnitsContainer.removeChild(elements[0])
+
+ /* Create all the units buttons */
+ for (let unit of units)
+ {
+ this.#addUnitButton(unit, selectedUnitsContainer);
+ if (unit.isLeader)
+ for (let wingman of unit.getWingmen())
+ this.#addUnitButton(wingman, selectedUnitsContainer);
+ }
+ }
+
+ #addUnitButton(unit: Unit, container: HTMLElement)
+ {
+ var el = document.createElement("div");
+
+ /* Unit name (actually type, but DCS calls it name for some reason) */
+ var nameDiv = document.createElement("div");
+ nameDiv.classList.add("rounded-container-small");
+ if (unit.name.length >= 7)
+ nameDiv.innerHTML = `${unit.name.substring(0, 4)} ...`;
+ else
+ nameDiv.innerHTML = `${unit.name}`;
+
+ /* Unit icon */
+ var icon = document.createElement("img");
+ if (unit.isLeader)
+ icon.src = "images/icons/formation.png"
+ else if (unit.isWingman)
+ {
+ var wingmen = unit.getLeader()?.getWingmen();
+ if (wingmen && wingmen.lastIndexOf(unit) == wingmen.length - 1)
+ icon.src = "images/icons/formation-end.svg"
+ else
+ icon.src = "images/icons/formation-middle.svg"
+ }
+
+ else
+ icon.src = "images/icons/singleton.png"
+
+ el.innerHTML = unit.unitName;
+
+ el.prepend(nameDiv);
+
+ /* Show the icon only for air units */
+ if ((unit instanceof AirUnit))
+ el.append(icon);
+
+ el.classList.add("rounded-container", "js-unit-container");
+
+ if (!unit.getSelected())
+ el.classList.add("not-selected")
+
+ /* Set background color */
+ if (unit.coalitionID == 1)
+ {
+ el.classList.add("red");
+ icon.classList.add("red");
+ }
+ else if (unit.coalitionID == 2)
+ {
+ el.classList.add("blue");
+ icon.classList.add("blue");
+ }
+ else
+ {
+ el.classList.add("neutral");
+ icon.classList.add("neutral");
+ }
+
+ el.addEventListener("click", () => getUnitsManager().selectUnit(unit.ID));
+ container.appendChild(el);
+ }
+
+ #showFormationButtons(units: Unit[], formationCreationContainer: HTMLElement)
+ {
+ var createButton = formationCreationContainer.querySelector("#create-formation");
+ var undoButton = formationCreationContainer.querySelector("#undo-formation");
+ if (createButton && undoButton && this.#checkAllUnitsAir(units))
+ {
+ if (!this.#checkUnitsAlreadyInFormation(units))
+ {
+ createButton.style.display = '';
+ undoButton.style.display = 'none';
+ }
+ else if (this.#checkUnitsAlreadyInFormation(units) && this.#checkAllUnitsSameFormation(units))
+ {
+ createButton.style.display = 'none';
+ undoButton.style.display = '';
+ }
+ else
+ {
+ createButton.style.display = 'none';
+ undoButton.style.display = 'none';
+ }
+ }
+ }
+
+ #checkAllUnitsAir(units: Unit[])
+ {
+ for (let unit of units)
+ if (!(unit instanceof AirUnit))
+ return false
+ return true
+ }
+
+ #checkAllUnitsAircraft(units: Unit[])
+ {
+ for (let unit of units)
+ if (!(unit instanceof Aircraft))
+ return false
+ return true
+ }
+
+ #checkAllUnitsHelicopter(units: Unit[])
+ {
+ for (let unit of units)
+ if (!(unit instanceof Helicopter))
+ return false
+ return true
+ }
+
+ #checkAllUnitsGroundUnit(units: Unit[])
+ {
+ for (let unit of units)
+ if (!(unit instanceof GroundUnit))
+ return false
+ return true
+ }
+
+ #checkAllUnitsNavyUnit(units: Unit[])
+ {
+ for (let unit of units)
+ if (!(unit instanceof NavyUnit))
+ return false
+ return true
+ }
+
+ #checkAllUnitsSameFormation(units: Unit[])
+ {
+ var leaderFound = false;
+ for (let unit of units)
+ {
+ if (unit.isLeader)
+ {
+ if (leaderFound)
+ return false
+ else
+ leaderFound = true;
+ }
+ if (!unit.isLeader)
+ return false
+ }
+ return true
+ }
+
+ #checkUnitsAlreadyInFormation(units: Unit[])
+ {
+ for (let unit of units)
+ if (unit.isLeader)
+ return true
+ return false
+ }
+
+ #getTargetAirspeed(units: Unit[])
+ {
+ var airspeed = null;
+ for (let unit of units)
+ {
+ if (unit.targetSpeed != airspeed && airspeed != null)
+ return null
+ else
+ airspeed = unit.targetSpeed;
+ }
+ return airspeed;
+ }
+
+ #getTargetAltitude(units: Unit[])
+ {
+ var altitude = null;
+ for (let unit of units)
+ {
+ if (unit.targetAltitude != altitude && altitude != null)
+ return null
+ else
+ altitude = unit.targetAltitude;
+ }
+ return altitude;
+ }
+
+ #getROE(units: Unit[])
+ {
+ var ROE = null;
+ for (let unit of units)
+ {
+ if (unit.ROE !== ROE && ROE != null)
+ return null
+ else
+ ROE = unit.ROE;
+ }
+ return ROE;
+ }
+
+ #getReactionToThreat(units: Unit[])
+ {
+ var reactionToThreat = null;
+ for (let unit of units)
+ {
+ if (unit.reactionToThreat !== reactionToThreat && reactionToThreat != null)
+ return null
+ else
+ reactionToThreat = unit.reactionToThreat;
+ }
+ return reactionToThreat;
+ }
+}
\ No newline at end of file
diff --git a/client/src/panels/unitinfopanel.ts b/client/src/panels/unitinfopanel.ts
index b4741024..070d1dda 100644
--- a/client/src/panels/unitinfopanel.ts
+++ b/client/src/panels/unitinfopanel.ts
@@ -48,6 +48,15 @@ export class UnitInfoPanel {
this.#element.querySelector("#latitude")!.innerHTML = ConvertDDToDMS(unit.latitude, false);
this.#element.querySelector("#longitude")!.innerHTML = ConvertDDToDMS(unit.longitude, true);
this.#element.querySelector("#task")!.innerHTML = unit.currentTask !== ""? unit.currentTask: "Not controlled";
+
+ this.#element.querySelector("#task")!.classList.remove("red", "blue", "neutral");
+ if (unit.coalitionID == 1)
+ this.#element.querySelector("#task")!.classList.add("red");
+ else if (unit.coalitionID == 2)
+ this.#element.querySelector("#task")!.classList.add("blue");
+ else
+ this.#element.querySelector("#task")!.classList.add("neutral");
+
}
}
}
\ No newline at end of file
diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts
index 856b2749..723aac23 100644
--- a/client/src/units/unit.ts
+++ b/client/src/units/unit.ts
@@ -2,7 +2,7 @@ import { Marker, LatLng, Polyline, Icon } from 'leaflet';
import { ConvertDDToDMS } from '../other/utils';
import { getMap, getUnitsManager, getVisibilitySettings } from '..';
import { UnitMarker, MarkerOptions, AircraftMarker, HelicopterMarker, GroundUnitMarker, NavyUnitMarker, WeaponMarker } from './unitmarker';
-import { addDestination, attackUnit, changeAltitude, changeSpeed } from '../dcs/dcs';
+import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, landAt, setAltitude, setReactionToThreat, setROE, setSpeed } from '../dcs/dcs';
var pathIcon = new Icon({
iconUrl: 'images/marker-icon.png',
@@ -12,9 +12,6 @@ var pathIcon = new Icon({
export class Unit {
ID: number = -1;
- leader: boolean = false;
- wingman: boolean = false;
- wingmen: Unit[] = [];
formation: string = "";
name: string = "";
unitName: string = "";
@@ -33,7 +30,17 @@ export class Unit {
activePath: any = null;
ammo: any = null;
targets: any = null;
-
+ hasTask: boolean = false;
+ isLeader: boolean = false;
+ isWingman: boolean = false;
+ leaderID: number = 0;
+ wingmen: Unit[] = [];
+ wingmenIDs: number[] = [];
+ targetSpeed: number = 0;
+ targetAltitude: number = 0;
+ ROE: string = "";
+ reactionToThreat: string = "";
+
#selectable: boolean;
#selected: boolean = false;
#preventClick: boolean = false;
@@ -61,42 +68,36 @@ export class Unit {
this.#marker = marker;
this.#marker.on('click', (e) => this.#onClick(e));
this.#marker.on('dblclick', (e) => this.#onDoubleClick(e));
+ this.#marker.on('contextmenu', (e) => this.#onContextMenu(e));
this.#pathPolyline = new Polyline([], { color: '#2d3e50', weight: 3, opacity: 0.5, smoothFactor: 1 });
this.#pathPolyline.addTo(getMap());
this.#targetsPolylines = [];
}
- update(response: JSON) {
+ update(response: any) {
for (let entry in response) {
- // @ts-ignore
+ // @ts-ignore TODO handle better
this[entry] = response[entry];
}
+ // TODO handle better
+ if (response['activePath'] == undefined)
+ this.activePath = null
+
/* Dead units can't be selected */
this.setSelected(this.getSelected() && this.alive)
this.#updateMarker();
this.#clearTargets();
- if (this.getSelected())
+ if (this.getSelected() && this.activePath != null)
{
this.#drawPath();
this.#drawTargets();
}
else
- this.#clearPath();
-
- /*
- this.wingmen = [];
- if (response["wingmenIDs"] != null)
- {
- for (let ID of response["wingmenIDs"])
- {
- this.wingmen.push(unitsManager.getUnitByID(ID));
- }
- }
- */
+ this.#clearPath();
}
setSelected(selected: boolean) {
@@ -105,6 +106,7 @@ export class Unit {
this.#selected = selected;
this.#marker.setSelected(selected);
getUnitsManager().onUnitSelection();
+
}
}
@@ -140,6 +142,28 @@ export class Unit {
return false;
}
+ getLeader() {
+ return getUnitsManager().getUnitByID(this.leaderID);
+ }
+
+ getFormation() {
+ return [this].concat(this.getWingmen())
+ }
+
+ getWingmen() {
+ var wingmen: Unit[] = [];
+ if (this.wingmenIDs != null)
+ {
+ for (let ID of this.wingmenIDs)
+ {
+ var unit = getUnitsManager().getUnitByID(ID)
+ if (unit)
+ wingmen.push(unit);
+ }
+ }
+ return wingmen;
+ }
+
#onClick(e: any) {
this.#timer = setTimeout(() => {
if (!this.#preventClick) {
@@ -157,22 +181,21 @@ export class Unit {
#onDoubleClick(e: any) {
clearTimeout(this.#timer);
this.#preventClick = true;
+ }
+ #onContextMenu(e: any) {
var options = [
'Attack',
+ 'Follow'
]
- //if (!this.leader && !this.wingman) {
- // options.push({ 'tooltip': 'Create formation', 'src': 'formation.png', 'callback': () => { getMap().hideSelectionWheel(); /*unitsManager.createFormation(this.ID);*/ } });
- //}
-
- getMap().showSelectionScroll(e.originalEvent, options, (action: string) => this.#executeAction(action));
+ getMap().showSelectionScroll(e.originalEvent, "Action: " + this.unitName, options, (action: string) => this.#executeAction(action));
}
#executeAction(action: string) {
getMap().hideSelectionScroll();
if (action === "Attack")
- getUnitsManager().attackUnit(this.ID);
+ getUnitsManager().selectedUnitsAttackUnit(this.ID);
}
#updateMarker() {
@@ -199,8 +222,8 @@ export class Unit {
#drawPath() {
if (this.activePath != null) {
- var _points = [];
- _points.push(new LatLng(this.latitude, this.longitude));
+ var points = [];
+ points.push(new LatLng(this.latitude, this.longitude));
/* Add markers if missing */
while (this.#pathMarkers.length < Object.keys(this.activePath).length) {
@@ -218,8 +241,8 @@ export class Unit {
for (let WP in this.activePath) {
var destination = this.activePath[WP];
this.#pathMarkers[parseInt(WP) - 1].setLatLng([destination.lat, destination.lng]);
- _points.push(new LatLng(destination.lat, destination.lng));
- this.#pathPolyline.setLatLngs(_points);
+ points.push(new LatLng(destination.lat, destination.lng));
+ this.#pathPolyline.setLatLngs(points);
}
}
}
@@ -232,7 +255,6 @@ export class Unit {
this.#pathPolyline.setLatLngs([]);
}
-
#drawTargets()
{
for (let typeIndex in this.targets)
@@ -278,7 +300,6 @@ export class Unit {
}
}
-
attackUnit(targetID: number) {
/* Call DCS attackUnit function */
if (this.ID != targetID) {
@@ -289,7 +310,11 @@ export class Unit {
}
}
-
+ landAt(latlng: LatLng)
+ {
+ landAt(this.ID, latlng);
+ }
+
changeSpeed(speedChange: string)
{
changeSpeed(this.ID, speedChange);
@@ -300,6 +325,26 @@ export class Unit {
changeAltitude(this.ID, altitudeChange);
}
+ setSpeed(speed: number)
+ {
+ setSpeed(this.ID, speed);
+ }
+
+ setAltitude(altitude: number)
+ {
+ setAltitude(this.ID, altitude);
+ }
+
+ setROE(ROE: string)
+ {
+ setROE(this.ID, ROE);
+ }
+
+ setReactionToThreat(reactionToThreat: string)
+ {
+ setReactionToThreat(this.ID, reactionToThreat);
+ }
+
/*
setformation(formation)
{
@@ -318,25 +363,13 @@ export class Unit {
xhr.send(JSON.stringify(data));
}
-
- setLeader(wingmenIDs)
- {
- // TODO move in dedicated file
- var xhr = new XMLHttpRequest();
- xhr.open("PUT", RESTaddress);
- xhr.setRequestHeader("Content-Type", "application/json");
- xhr.onreadystatechange = () => {
- if (xhr.readyState === 4) {
- console.log(this.unitName + " created formation with: " + wingmenIDs);
- }
- };
-
- var command = {"ID": this.ID, "wingmenIDs": wingmenIDs}
- var data = {"setLeader": command}
-
- xhr.send(JSON.stringify(data));
- }
*/
+
+ setLeader(isLeader: boolean, wingmenIDs: number[] = [])
+ {
+ setLeader(this.ID, isLeader, wingmenIDs);
+ }
+
}
export class AirUnit extends Unit {
diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts
index 4533d988..1149661b 100644
--- a/client/src/units/unitsmanager.ts
+++ b/client/src/units/unitsmanager.ts
@@ -1,6 +1,7 @@
import { LatLng, LatLngBounds } from "leaflet";
-import { getMap, getUnitInfoPanel } from "..";
+import { getMap, getUnitControlPanel, getUnitInfoPanel } from "..";
import { Unit, GroundUnit } from "./unit";
+import { cloneUnit } from "../dcs/dcs";
export class UnitsManager {
#units: { [ID: number]: Unit };
@@ -9,6 +10,24 @@ export class UnitsManager {
constructor() {
this.#units = {};
this.#copiedUnits = [];
+
+ document.addEventListener('copy', () => this.copyUnits());
+ document.addEventListener('paste', () => this.pasteUnits());
+ }
+
+ #updateUnitControlPanel() {
+ /* Update the unit control panel */
+ if (this.getSelectedUnits().length > 0) {
+ getUnitControlPanel().show();
+ getUnitControlPanel().update(this.getSelectedLeaders().concat(this.getSelectedSingletons()));
+ }
+ else {
+ getUnitControlPanel().hide();
+ }
+ }
+
+ getUnits() {
+ return this.#units;
}
addUnit(ID: number, data: any) {
@@ -27,7 +46,10 @@ export class UnitsManager {
}
getUnitByID(ID: number) {
- return this.#units[ID];
+ if (ID in this.#units)
+ return this.#units[ID];
+ else
+ return null;
}
removeUnit(ID: number) {
@@ -40,6 +62,13 @@ export class UnitsManager {
}
}
+ selectUnit(ID: number, deselectAllUnits: boolean = true)
+ {
+ if (deselectAllUnits)
+ this.deselectAllUnits();
+ this.#units[ID]?.setSelected(true);
+ }
+
update(data: any) {
for (let ID in data["units"]) {
/* Create the unit if missing from the local array, then update the data. Drawing is handled by leaflet. */
@@ -49,6 +78,7 @@ export class UnitsManager {
this.#units[parseInt(ID)].update(data["units"][ID]);
}
+ /* Update the unit info panel */
if (this.getSelectedUnits().length == 1) {
getUnitInfoPanel().show();
getUnitInfoPanel().update(this.getSelectedUnits()[0]);
@@ -67,6 +97,8 @@ export class UnitsManager {
getMap().setState("IDLE");
//unitControlPanel.setEnabled(false);
}
+
+ this.#updateUnitControlPanel();
}
selectFromBounds(bounds: LatLngBounds)
@@ -92,7 +124,35 @@ export class UnitsManager {
return selectedUnits;
}
- addDestination(latlng: L.LatLng) {
+ getSelectedLeaders() {
+ var leaders: Unit[] = [];
+ for (let idx in this.getSelectedUnits())
+ {
+ var unit = this.getSelectedUnits()[idx];
+ if (unit.isLeader)
+ leaders.push(unit);
+ else if (unit.isWingman)
+ {
+ var leader = unit.getLeader();
+ if (leader && !leaders.includes(leader))
+ leaders.push(leader);
+ }
+ }
+ return leaders;
+ }
+
+ getSelectedSingletons() {
+ var singletons: Unit[] = [];
+ for (let idx in this.getSelectedUnits())
+ {
+ var unit = this.getSelectedUnits()[idx];
+ if (!unit.isLeader && !unit.isWingman)
+ singletons.push(unit);
+ }
+ return singletons;
+ }
+
+ selectedUnitsAddDestination(latlng: L.LatLng) {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
var commandedUnit = selectedUnits[idx];
@@ -104,7 +164,7 @@ export class UnitsManager {
}
}
- clearDestinations() {
+ selectedUnitsClearDestinations() {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
var commandedUnit = selectedUnits[idx];
@@ -116,6 +176,15 @@ export class UnitsManager {
}
}
+ selectedUnitsLandAt(latlng: LatLng)
+ {
+ var selectedUnits = this.getSelectedUnits();
+ for (let idx in selectedUnits)
+ {
+ selectedUnits[idx].landAt(latlng);
+ }
+ }
+
selectedUnitsChangeSpeed(speedChange: string)
{
var selectedUnits = this.getSelectedUnits();
@@ -123,6 +192,8 @@ export class UnitsManager {
{
selectedUnits[idx].changeSpeed(speedChange);
}
+
+ setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail
}
selectedUnitsChangeAltitude(altitudeChange: string)
@@ -132,35 +203,66 @@ export class UnitsManager {
{
selectedUnits[idx].changeAltitude(altitudeChange);
}
+
+ setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail
}
- // handleKeyEvent(e)
- // {
- // if (e.originalEvent.code === 'KeyC' && e.originalEvent.ctrlKey)
- // {
- // this.copyUnits();
- // }
- // else if (e.originalEvent.code === 'KeyV' && e.originalEvent.ctrlKey)
- // {
- // this.pasteUnits();
- // }
- // }
+ selectedUnitsSetSpeed(speed: number)
+ {
+ var selectedUnits = this.getSelectedUnits();
+ for (let idx in selectedUnits)
+ {
+ selectedUnits[idx].setSpeed(speed);
+ }
+ }
- // copyUnits()
- // {
- // this.#copiedUnits = this.getSelectedUnits();
- // }
+ selectedUnitsSetAltitude(altitude: number)
+ {
+ var selectedUnits = this.getSelectedUnits();
+ for (let idx in selectedUnits)
+ {
+ selectedUnits[idx].setAltitude(altitude);
+ }
+ }
- // pasteUnits()
- // {
- // for (let idx in this.#copiedUnits)
- // {
- // var unit = this.#copiedUnits[idx];
- // cloneUnit(unit.ID);
- // }
- // }
+ selectedUnitsSetROE(ROE: string)
+ {
+ var selectedUnits = this.getSelectedUnits();
+ for (let idx in selectedUnits)
+ {
+ selectedUnits[idx].setROE(ROE);
+ }
- attackUnit(ID: number) {
+ setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail
+ }
+
+ selectedUnitsSetReactionToThreat(reactionToThreat: string)
+ {
+ var selectedUnits = this.getSelectedUnits();
+ for (let idx in selectedUnits)
+ {
+ selectedUnits[idx].setReactionToThreat(reactionToThreat);
+ }
+
+ setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail
+ }
+
+
+ copyUnits()
+ {
+ this.#copiedUnits = this.getSelectedUnits();
+ }
+
+ pasteUnits()
+ {
+ for (let idx in this.#copiedUnits)
+ {
+ var unit = this.#copiedUnits[idx];
+ cloneUnit(unit.ID, getMap().getMouseCoordinates());
+ }
+ }
+
+ selectedUnitsAttackUnit(ID: number) {
var selectedUnits = this.getSelectedUnits();
for (let idx in selectedUnits) {
/* If a unit is a wingman, send the command to its leader */
@@ -173,59 +275,59 @@ export class UnitsManager {
}
}
- // createFormation(ID)
- // {
- // var selectedUnits = this.getSelectedUnits();
- // var wingmenIDs = [];
- // for (let idx in selectedUnits)
- // {
- // if (selectedUnits[idx].wingman)
- // {
- // showMessage(selectedUnits[idx].unitName + " is already in a formation.");
- // return;
- // }
- // else if (selectedUnits[idx].leader)
- // {
- // showMessage(selectedUnits[idx].unitName + " is already in a formation.");
- // return;
- // }
- // else
- // {
- // /* TODO
- // if (selectedUnits[idx].category !== this.getUnitByID(ID).category)
- // {
- // showMessage("All units must be of the same category to create a formation.");
- // }
- // */
- // if (selectedUnits[idx].ID != ID)
- // {
- // wingmenIDs.push(selectedUnits[idx].ID);
- // }
- // }
- // }
- // if (wingmenIDs.length > 0)
- // {
- // this.getUnitByID(ID).setLeader(wingmenIDs);
- // }
- // else
- // {
- // showMessage("At least 2 units must be selected to create a formation.");
- // }
- // }
+ selectedUnitsCreateFormation(ID: number | null = null)
+ {
+ var selectedUnits = this.getSelectedUnits();
+ if (selectedUnits.length >= 2)
+ {
+ if (ID == null)
+ ID = selectedUnits[0].ID
+
+ var wingmenIDs = [];
+ for (let idx in selectedUnits)
+ {
+ if (selectedUnits[idx].isWingman)
+ {
+ console.log(selectedUnits[idx].unitName + " is already in a formation.");
+ return;
+ }
+ else if (selectedUnits[idx].isLeader)
+ {
+ console.log(selectedUnits[idx].unitName + " is already in a formation.");
+ return;
+ }
+ else
+ {
+ /* TODO
+ if (selectedUnits[idx].category !== this.getUnitByID(ID).category)
+ {
+ showMessage("All units must be of the same category to create a formation.");
+ }
+ */
+ if (selectedUnits[idx].ID != ID)
+ {
+ wingmenIDs.push(selectedUnits[idx].ID);
+ }
+ }
+ }
+ if (wingmenIDs.length > 0)
+ {
+ this.getUnitByID(ID)?.setLeader(true, wingmenIDs);
+ }
+ else
+ {
+ console.log("At least 2 units must be selected to create a formation.");
+ }
+ }
+ setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail
+ }
- // getLeader(ID)
- // {
- // for (let idx in this.#units)
- // {
- // var unit = this.#units[idx];
- // if (unit.leader)
- // {
- // if (unit.wingmen.includes(this.getUnitByID(ID)))
- // {
- // return unit;
- // }
- // }
- // }
- // showMessage("Error: no leader found for this unit")
- // }
+ selectedUnitsUndoFormation(ID: number | null = null)
+ {
+ for (let leader of this.getSelectedLeaders())
+ {
+ leader.setLeader(false);
+ }
+ setTimeout(() => this.#updateUnitControlPanel(), 300); // TODO find better method, may fail
+ }
}
\ No newline at end of file
diff --git a/client/views/index.ejs b/client/views/index.ejs
index 02a7903b..fea64c45 100644
--- a/client/views/index.ejs
+++ b/client/views/index.ejs
@@ -30,6 +30,7 @@
<%- include('unitcontrolpanel.ejs') %>
<%- include('visibilitycontrolpanel.ejs') %>
<%- include('connectionstatuspanel.ejs') %>
+ <%- include('mouseinfopanel.ejs') %>