From 1dd4014e61919661827170eb012783f5aeb247f0 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 1 Jun 2023 17:18:14 +0200 Subject: [PATCH] Completed frontend controls --- client/copy.bat | 1 - client/demo.js | 15 +- client/public/javascripts/svg-inject.js | 697 ++++++++++++++++++ client/public/stylesheets/layout/layout.css | 1 + client/public/stylesheets/olympus.css | 12 +- .../public/stylesheets/other/contextmenus.css | 46 +- .../public/stylesheets/panels/unitcontrol.css | 104 +-- client/public/stylesheets/panels/unitinfo.css | 76 +- client/public/themes/olympus/theme.css | 1 + client/src/@types/unit.d.ts | 22 +- client/src/constants/constants.ts | 102 +++ client/src/controls/control.ts | 13 + client/src/controls/dropdown.ts | 15 +- client/src/controls/mapcontextmenu.ts | 42 +- client/src/controls/slider.ts | 84 ++- client/src/controls/switch.ts | 26 +- client/src/index.ts | 2 - client/src/map/destinationpreviewmarker.ts | 2 +- client/src/map/map.ts | 118 +-- client/src/missionhandler/missionhandler.ts | 5 +- client/src/panels/unitcontrolpanel.ts | 128 ++-- client/src/panels/unitinfopanel.ts | 52 +- client/src/server/server.ts | 103 ++- client/src/units/unit.ts | 87 +-- client/src/units/unitsmanager.ts | 54 +- client/views/other/contextmenus.ejs | 15 +- client/views/panels/unitcontrol.ejs | 11 +- client/views/panels/unitinfo.ejs | 2 +- olympus.json | 2 +- 29 files changed, 1297 insertions(+), 541 deletions(-) create mode 100644 client/public/javascripts/svg-inject.js create mode 100644 client/src/constants/constants.ts diff --git a/client/copy.bat b/client/copy.bat index 1acc220e..f163f70e 100644 --- a/client/copy.bat +++ b/client/copy.bat @@ -1,3 +1,2 @@ copy .\\node_modules\\leaflet\\dist\\leaflet.css .\\public\\stylesheets\\leaflet\\leaflet.css -copy .\\node_modules\\@iconfu\\svg-inject\\dist\\svg-inject.js .\\public\\javascripts\\svg-inject.js copy .\\node_modules\\leaflet.nauticscale\\dist\\leaflet.nauticscale.js .\\public\\javascripts\\leaflet.nauticscale.js diff --git a/client/demo.js b/client/demo.js index 292e2f52..4dc6a2b4 100644 --- a/client/demo.js +++ b/client/demo.js @@ -50,16 +50,11 @@ const DEMO_UNIT_DATA = { currentState: "Idle", activePath: undefined, targetSpeed: 400, + targetSpeedType: "CAS", targetAltitude: 3000, + targetAltitudeType: "ASL", isTanker: false, - TACANOn: false, - TACANChannel: 32, - TACANXY: "Y", - TACANCallsign: "ASD", - radioFrequency: 123.750, - radioCallsign: 2, - radioCallsignNumber: 3, - radioAMFM: "FM" + }, optionsData: { ROE: "Designated", @@ -145,7 +140,8 @@ const DEMO_UNIT_DATA = { currentTask: "Example task", activePath: undefined, targetSpeed: 400, - targetAltitude: 3000 + targetAltitude: 3000, + onOff: false }, optionsData: { ROE: "None", @@ -157,7 +153,6 @@ const DEMO_UNIT_DATA = { AI: true, name: "2S6 Tunguska", unitName: "Olympus 1-4", - groupName: "Group 1", alive: true, category: "GroundUnit", }, diff --git a/client/public/javascripts/svg-inject.js b/client/public/javascripts/svg-inject.js new file mode 100644 index 00000000..4e74af07 --- /dev/null +++ b/client/public/javascripts/svg-inject.js @@ -0,0 +1,697 @@ +/** + * SVGInject - Version 1.2.3 + * A tiny, intuitive, robust, caching solution for injecting SVG files inline into the DOM. + * + * https://github.com/iconfu/svg-inject + * + * Copyright (c) 2018 INCORS, the creators of iconfu.com + * @license MIT License - https://github.com/iconfu/svg-inject/blob/master/LICENSE + */ + +(function(window, document) { + // constants for better minification + var _CREATE_ELEMENT_ = 'createElement'; + var _GET_ELEMENTS_BY_TAG_NAME_ = 'getElementsByTagName'; + var _LENGTH_ = 'length'; + var _STYLE_ = 'style'; + var _TITLE_ = 'title'; + var _UNDEFINED_ = 'undefined'; + var _SET_ATTRIBUTE_ = 'setAttribute'; + var _GET_ATTRIBUTE_ = 'getAttribute'; + + var NULL = null; + + // constants + var __SVGINJECT = '__svgInject'; + var ID_SUFFIX = '--inject-'; + var ID_SUFFIX_REGEX = new RegExp(ID_SUFFIX + '\\d+', "g"); + var LOAD_FAIL = 'LOAD_FAIL'; + var SVG_NOT_SUPPORTED = 'SVG_NOT_SUPPORTED'; + var SVG_INVALID = 'SVG_INVALID'; + var ATTRIBUTE_EXCLUSION_NAMES = ['src', 'alt', 'onload', 'onerror']; + var A_ELEMENT = document[_CREATE_ELEMENT_]('a'); + var IS_SVG_SUPPORTED = typeof SVGRect != _UNDEFINED_; + var DEFAULT_OPTIONS = { + useCache: true, + copyAttributes: true, + makeIdsUnique: true + }; + // Map of IRI referenceable tag names to properties that can reference them. This is defined in + // https://www.w3.org/TR/SVG11/linking.html#processingIRI + var IRI_TAG_PROPERTIES_MAP = { + clipPath: ['clip-path'], + 'color-profile': NULL, + cursor: NULL, + filter: NULL, + linearGradient: ['fill', 'stroke'], + marker: ['marker', 'marker-end', 'marker-mid', 'marker-start'], + mask: NULL, + pattern: ['fill', 'stroke'], + radialGradient: ['fill', 'stroke'] + }; + var INJECTED = 1; + var FAIL = 2; + + var uniqueIdCounter = 1; + var xmlSerializer; + var domParser; + + + // creates an SVG document from an SVG string + function svgStringToSvgDoc(svgStr) { + domParser = domParser || new DOMParser(); + return domParser.parseFromString(svgStr, 'text/xml'); + } + + + // searializes an SVG element to an SVG string + function svgElemToSvgString(svgElement) { + xmlSerializer = xmlSerializer || new XMLSerializer(); + return xmlSerializer.serializeToString(svgElement); + } + + + // Returns the absolute url for the specified url + function getAbsoluteUrl(url) { + A_ELEMENT.href = url; + return A_ELEMENT.href; + } + + + // Load svg with an XHR request + function loadSvg(url, callback, errorCallback) { + if (url) { + var req = new XMLHttpRequest(); + req.onreadystatechange = function() { + if (req.readyState == 4) { + // readyState is DONE + var status = req.status; + if (status == 200) { + // request status is OK + callback(req.responseXML, req.responseText.trim()); + } else if (status >= 400) { + // request status is error (4xx or 5xx) + errorCallback(); + } else if (status == 0) { + // request status 0 can indicate a failed cross-domain call + errorCallback(); + } + } + }; + req.open('GET', url, true); + req.send(); + } + } + + + // Copy attributes from img element to svg element + function copyAttributes(imgElem, svgElem) { + var attribute; + var attributeName; + var attributeValue; + var attributes = imgElem.attributes; + for (var i = 0; i < attributes[_LENGTH_]; i++) { + attribute = attributes[i]; + attributeName = attribute.name; + // Only copy attributes not explicitly excluded from copying + if (ATTRIBUTE_EXCLUSION_NAMES.indexOf(attributeName) == -1) { + attributeValue = attribute.value; + // If img attribute is "title", insert a title element into SVG element + if (attributeName == _TITLE_) { + var titleElem; + var firstElementChild = svgElem.firstElementChild; + if (firstElementChild && firstElementChild.localName.toLowerCase() == _TITLE_) { + // If the SVG element's first child is a title element, keep it as the title element + titleElem = firstElementChild; + } else { + // If the SVG element's first child element is not a title element, create a new title + // ele,emt and set it as the first child + titleElem = document[_CREATE_ELEMENT_ + 'NS']('http://www.w3.org/2000/svg', _TITLE_); + svgElem.insertBefore(titleElem, firstElementChild); + } + // Set new title content + titleElem.textContent = attributeValue; + } else { + // Set img attribute to svg element + svgElem[_SET_ATTRIBUTE_](attributeName, attributeValue); + } + } + } + } + + + // This function appends a suffix to IDs of referenced elements in the in order to to avoid ID collision + // between multiple injected SVGs. The suffix has the form "--inject-X", where X is a running number which is + // incremented with each injection. References to the IDs are adjusted accordingly. + // We assume tha all IDs within the injected SVG are unique, therefore the same suffix can be used for all IDs of one + // injected SVG. + // If the onlyReferenced argument is set to true, only those IDs will be made unique that are referenced from within the SVG + function makeIdsUnique(svgElem, onlyReferenced) { + var idSuffix = ID_SUFFIX + uniqueIdCounter++; + // Regular expression for functional notations of an IRI references. This will find occurences in the form + // url(#anyId) or url("#anyId") (for Internet Explorer) and capture the referenced ID + var funcIriRegex = /url\("?#([a-zA-Z][\w:.-]*)"?\)/g; + // Get all elements with an ID. The SVG spec recommends to put referenced elements inside elements, but + // this is not a requirement, therefore we have to search for IDs in the whole SVG. + var idElements = svgElem.querySelectorAll('[id]'); + var idElem; + // An object containing referenced IDs as keys is used if only referenced IDs should be uniquified. + // If this object does not exist, all IDs will be uniquified. + var referencedIds = onlyReferenced ? [] : NULL; + var tagName; + var iriTagNames = {}; + var iriProperties = []; + var changed = false; + var i, j; + + if (idElements[_LENGTH_]) { + // Make all IDs unique by adding the ID suffix and collect all encountered tag names + // that are IRI referenceable from properities. + for (i = 0; i < idElements[_LENGTH_]; i++) { + tagName = idElements[i].localName; // Use non-namespaced tag name + // Make ID unique if tag name is IRI referenceable + if (tagName in IRI_TAG_PROPERTIES_MAP) { + iriTagNames[tagName] = 1; + } + } + // Get all properties that are mapped to the found IRI referenceable tags + for (tagName in iriTagNames) { + (IRI_TAG_PROPERTIES_MAP[tagName] || [tagName]).forEach(function (mappedProperty) { + // Add mapped properties to array of iri referencing properties. + // Use linear search here because the number of possible entries is very small (maximum 11) + if (iriProperties.indexOf(mappedProperty) < 0) { + iriProperties.push(mappedProperty); + } + }); + } + if (iriProperties[_LENGTH_]) { + // Add "style" to properties, because it may contain references in the form 'style="fill:url(#myFill)"' + iriProperties.push(_STYLE_); + } + // Run through all elements of the SVG and replace IDs in references. + // To get all descending elements, getElementsByTagName('*') seems to perform faster than querySelectorAll('*'). + // Since svgElem.getElementsByTagName('*') does not return the svg element itself, we have to handle it separately. + var descElements = svgElem[_GET_ELEMENTS_BY_TAG_NAME_]('*'); + var element = svgElem; + var propertyName; + var value; + var newValue; + for (i = -1; element != NULL;) { + if (element.localName == _STYLE_) { + // If element is a style element, replace IDs in all occurences of "url(#anyId)" in text content + value = element.textContent; + newValue = value && value.replace(funcIriRegex, function(match, id) { + if (referencedIds) { + referencedIds[id] = 1; + } + return 'url(#' + id + idSuffix + ')'; + }); + if (newValue !== value) { + element.textContent = newValue; + } + } else if (element.hasAttributes()) { + // Run through all property names for which IDs were found + for (j = 0; j < iriProperties[_LENGTH_]; j++) { + propertyName = iriProperties[j]; + value = element[_GET_ATTRIBUTE_](propertyName); + newValue = value && value.replace(funcIriRegex, function(match, id) { + if (referencedIds) { + referencedIds[id] = 1; + } + return 'url(#' + id + idSuffix + ')'; + }); + if (newValue !== value) { + element[_SET_ATTRIBUTE_](propertyName, newValue); + } + } + // Replace IDs in xlink:ref and href attributes + ['xlink:href', 'href'].forEach(function(refAttrName) { + var iri = element[_GET_ATTRIBUTE_](refAttrName); + if (/^\s*#/.test(iri)) { // Check if iri is non-null and internal reference + iri = iri.trim(); + element[_SET_ATTRIBUTE_](refAttrName, iri + idSuffix); + if (referencedIds) { + // Add ID to referenced IDs + referencedIds[iri.substring(1)] = 1; + } + } + }); + } + element = descElements[++i]; + } + for (i = 0; i < idElements[_LENGTH_]; i++) { + idElem = idElements[i]; + // If set of referenced IDs exists, make only referenced IDs unique, + // otherwise make all IDs unique. + if (!referencedIds || referencedIds[idElem.id]) { + // Add suffix to element's ID + idElem.id += idSuffix; + changed = true; + } + } + } + // return true if SVG element has changed + return changed; + } + + + // For cached SVGs the IDs are made unique by simply replacing the already inserted unique IDs with a + // higher ID counter. This is much more performant than a call to makeIdsUnique(). + function makeIdsUniqueCached(svgString) { + return svgString.replace(ID_SUFFIX_REGEX, ID_SUFFIX + uniqueIdCounter++); + } + + + // Inject SVG by replacing the img element with the SVG element in the DOM + function inject(imgElem, svgElem, absUrl, options) { + if (svgElem) { + svgElem[_SET_ATTRIBUTE_]('data-inject-url', absUrl); + var parentNode = imgElem.parentNode; + if (parentNode) { + if (options.copyAttributes) { + copyAttributes(imgElem, svgElem); + } + // Invoke beforeInject hook if set + var beforeInject = options.beforeInject; + var injectElem = (beforeInject && beforeInject(imgElem, svgElem)) || svgElem; + // Replace img element with new element. This is the actual injection. + parentNode.replaceChild(injectElem, imgElem); + // Mark img element as injected + imgElem[__SVGINJECT] = INJECTED; + removeOnLoadAttribute(imgElem); + // Invoke afterInject hook if set + var afterInject = options.afterInject; + if (afterInject) { + afterInject(imgElem, injectElem); + } + } + } else { + svgInvalid(imgElem, options); + } + } + + + // Merges any number of options objects into a new object + function mergeOptions() { + var mergedOptions = {}; + var args = arguments; + // Iterate over all specified options objects and add all properties to the new options object + for (var i = 0; i < args[_LENGTH_]; i++) { + var argument = args[i]; + for (var key in argument) { + if (argument.hasOwnProperty(key)) { + mergedOptions[key] = argument[key]; + } + } + } + return mergedOptions; + } + + + // Adds the specified CSS to the document's element + function addStyleToHead(css) { + var head = document[_GET_ELEMENTS_BY_TAG_NAME_]('head')[0]; + if (head) { + var style = document[_CREATE_ELEMENT_](_STYLE_); + style.type = 'text/css'; + style.appendChild(document.createTextNode(css)); + head.appendChild(style); + } + } + + + // Builds an SVG element from the specified SVG string + function buildSvgElement(svgStr, verify) { + if (verify) { + var svgDoc; + try { + // Parse the SVG string with DOMParser + svgDoc = svgStringToSvgDoc(svgStr); + } catch(e) { + return NULL; + } + if (svgDoc[_GET_ELEMENTS_BY_TAG_NAME_]('parsererror')[_LENGTH_]) { + // DOMParser does not throw an exception, but instead puts parsererror tags in the document + return NULL; + } + return svgDoc.documentElement; + } else { + var div = document.createElement('div'); + div.innerHTML = svgStr; + return div.firstElementChild; + } + } + + + function removeOnLoadAttribute(imgElem) { + // Remove the onload attribute. Should only be used to remove the unstyled image flash protection and + // make the element visible, not for removing the event listener. + imgElem.removeAttribute('onload'); + } + + + function errorMessage(msg) { + console.error('SVGInject: ' + msg); + } + + + function fail(imgElem, status, options) { + imgElem[__SVGINJECT] = FAIL; + if (options.onFail) { + options.onFail(imgElem, status); + } else { + errorMessage(status); + } + } + + + function svgInvalid(imgElem, options) { + removeOnLoadAttribute(imgElem); + fail(imgElem, SVG_INVALID, options); + } + + + function svgNotSupported(imgElem, options) { + removeOnLoadAttribute(imgElem); + fail(imgElem, SVG_NOT_SUPPORTED, options); + } + + + function loadFail(imgElem, options) { + fail(imgElem, LOAD_FAIL, options); + } + + + function removeEventListeners(imgElem) { + imgElem.onload = NULL; + imgElem.onerror = NULL; + } + + + function imgNotSet(msg) { + errorMessage('no img element'); + } + + + function createSVGInject(globalName, options) { + var defaultOptions = mergeOptions(DEFAULT_OPTIONS, options); + var svgLoadCache = {}; + + if (IS_SVG_SUPPORTED) { + // If the browser supports SVG, add a small stylesheet that hides the elements until + // injection is finished. This avoids showing the unstyled SVGs before style is applied. + addStyleToHead('img[onload^="' + globalName + '("]{visibility:hidden;}'); + } + + + /** + * SVGInject + * + * Injects the SVG specified in the `src` attribute of the specified `img` element or array of `img` + * elements. Returns a Promise object which resolves if all passed in `img` elements have either been + * injected or failed to inject (Only if a global Promise object is available like in all modern browsers + * or through a polyfill). + * + * Options: + * useCache: If set to `true` the SVG will be cached using the absolute URL. Default value is `true`. + * copyAttributes: If set to `true` the attributes will be copied from `img` to `svg`. Dfault value + * is `true`. + * makeIdsUnique: If set to `true` the ID of elements in the `` element that can be references by + * property values (for example 'clipPath') are made unique by appending "--inject-X", where X is a + * running number which increases with each injection. This is done to avoid duplicate IDs in the DOM. + * beforeLoad: Hook before SVG is loaded. The `img` element is passed as a parameter. If the hook returns + * a string it is used as the URL instead of the `img` element's `src` attribute. + * afterLoad: Hook after SVG is loaded. The loaded `svg` element and `svg` string are passed as a + * parameters. If caching is active this hook will only get called once for injected SVGs with the + * same absolute path. Changes to the `svg` element in this hook will be applied to all injected SVGs + * with the same absolute path. It's also possible to return an `svg` string or `svg` element which + * will then be used for the injection. + * beforeInject: Hook before SVG is injected. The `img` and `svg` elements are passed as parameters. If + * any html element is returned it gets injected instead of applying the default SVG injection. + * afterInject: Hook after SVG is injected. The `img` and `svg` elements are passed as parameters. + * onAllFinish: Hook after all `img` elements passed to an SVGInject() call have either been injected or + * failed to inject. + * onFail: Hook after injection fails. The `img` element and a `status` string are passed as an parameter. + * The `status` can be either `'SVG_NOT_SUPPORTED'` (the browser does not support SVG), + * `'SVG_INVALID'` (the SVG is not in a valid format) or `'LOAD_FAILED'` (loading of the SVG failed). + * + * @param {HTMLImageElement} img - an img element or an array of img elements + * @param {Object} [options] - optional parameter with [options](#options) for this injection. + */ + function SVGInject(img, options) { + options = mergeOptions(defaultOptions, options); + + var run = function(resolve) { + var allFinish = function() { + var onAllFinish = options.onAllFinish; + if (onAllFinish) { + onAllFinish(); + } + resolve && resolve(); + }; + + if (img && typeof img[_LENGTH_] != _UNDEFINED_) { + // an array like structure of img elements + var injectIndex = 0; + var injectCount = img[_LENGTH_]; + + if (injectCount == 0) { + allFinish(); + } else { + var finish = function() { + if (++injectIndex == injectCount) { + allFinish(); + } + }; + + for (var i = 0; i < injectCount; i++) { + SVGInjectElement(img[i], options, finish); + } + } + } else { + // only one img element + SVGInjectElement(img, options, allFinish); + } + }; + + // return a Promise object if globally available + return typeof Promise == _UNDEFINED_ ? run() : new Promise(run); + } + + + // Injects a single svg element. Options must be already merged with the default options. + function SVGInjectElement(imgElem, options, callback) { + if (imgElem) { + var svgInjectAttributeValue = imgElem[__SVGINJECT]; + if (!svgInjectAttributeValue) { + removeEventListeners(imgElem); + + if (!IS_SVG_SUPPORTED) { + svgNotSupported(imgElem, options); + callback(); + return; + } + // Invoke beforeLoad hook if set. If the beforeLoad returns a value use it as the src for the load + // URL path. Else use the imgElem's src attribute value. + var beforeLoad = options.beforeLoad; + var src = (beforeLoad && beforeLoad(imgElem)) || imgElem[_GET_ATTRIBUTE_]('src'); + + if (!src) { + // If no image src attribute is set do no injection. This can only be reached by using javascript + // because if no src attribute is set the onload and onerror events do not get called + if (src === '') { + loadFail(imgElem, options); + } + callback(); + return; + } + + // set array so later calls can register callbacks + var onFinishCallbacks = []; + imgElem[__SVGINJECT] = onFinishCallbacks; + + var onFinish = function() { + callback(); + onFinishCallbacks.forEach(function(onFinishCallback) { + onFinishCallback(); + }); + }; + + var absUrl = getAbsoluteUrl(src); + var useCacheOption = options.useCache; + var makeIdsUniqueOption = options.makeIdsUnique; + + var setSvgLoadCacheValue = function(val) { + if (useCacheOption) { + svgLoadCache[absUrl].forEach(function(svgLoad) { + svgLoad(val); + }); + svgLoadCache[absUrl] = val; + } + }; + + if (useCacheOption) { + var svgLoad = svgLoadCache[absUrl]; + + var handleLoadValue = function(loadValue) { + if (loadValue === LOAD_FAIL) { + loadFail(imgElem, options); + } else if (loadValue === SVG_INVALID) { + svgInvalid(imgElem, options); + } else { + var hasUniqueIds = loadValue[0]; + var svgString = loadValue[1]; + var uniqueIdsSvgString = loadValue[2]; + var svgElem; + + if (makeIdsUniqueOption) { + if (hasUniqueIds === NULL) { + // IDs for the SVG string have not been made unique before. This may happen if previous + // injection of a cached SVG have been run with the option makedIdsUnique set to false + svgElem = buildSvgElement(svgString, false); + hasUniqueIds = makeIdsUnique(svgElem, false); + + loadValue[0] = hasUniqueIds; + loadValue[2] = hasUniqueIds && svgElemToSvgString(svgElem); + } else if (hasUniqueIds) { + // Make IDs unique for already cached SVGs with better performance + svgString = makeIdsUniqueCached(uniqueIdsSvgString); + } + } + + svgElem = svgElem || buildSvgElement(svgString, false); + + inject(imgElem, svgElem, absUrl, options); + } + onFinish(); + }; + + if (typeof svgLoad != _UNDEFINED_) { + // Value for url exists in cache + if (svgLoad.isCallbackQueue) { + // Same url has been cached, but value has not been loaded yet, so add to callbacks + svgLoad.push(handleLoadValue); + } else { + handleLoadValue(svgLoad); + } + return; + } else { + var svgLoad = []; + // set property isCallbackQueue to Array to differentiate from array with cached loaded values + svgLoad.isCallbackQueue = true; + svgLoadCache[absUrl] = svgLoad; + } + } + + // Load the SVG because it is not cached or caching is disabled + loadSvg(absUrl, function(svgXml, svgString) { + // Use the XML from the XHR request if it is an instance of Document. Otherwise + // (for example of IE9), create the svg document from the svg string. + var svgElem = svgXml instanceof Document ? svgXml.documentElement : buildSvgElement(svgString, true); + + var afterLoad = options.afterLoad; + if (afterLoad) { + // Invoke afterLoad hook which may modify the SVG element. After load may also return a new + // svg element or svg string + var svgElemOrSvgString = afterLoad(svgElem, svgString) || svgElem; + if (svgElemOrSvgString) { + // Update svgElem and svgString because of modifications to the SVG element or SVG string in + // the afterLoad hook, so the modified SVG is also used for all later cached injections + var isString = typeof svgElemOrSvgString == 'string'; + svgString = isString ? svgElemOrSvgString : svgElemToSvgString(svgElem); + svgElem = isString ? buildSvgElement(svgElemOrSvgString, true) : svgElemOrSvgString; + } + } + + if (svgElem instanceof SVGElement) { + var hasUniqueIds = NULL; + if (makeIdsUniqueOption) { + hasUniqueIds = makeIdsUnique(svgElem, false); + } + + if (useCacheOption) { + var uniqueIdsSvgString = hasUniqueIds && svgElemToSvgString(svgElem); + // set an array with three entries to the load cache + setSvgLoadCacheValue([hasUniqueIds, svgString, uniqueIdsSvgString]); + } + + inject(imgElem, svgElem, absUrl, options); + } else { + svgInvalid(imgElem, options); + setSvgLoadCacheValue(SVG_INVALID); + } + onFinish(); + }, function() { + loadFail(imgElem, options); + setSvgLoadCacheValue(LOAD_FAIL); + onFinish(); + }); + } else { + if (Array.isArray(svgInjectAttributeValue)) { + // svgInjectAttributeValue is an array. Injection is not complete so register callback + svgInjectAttributeValue.push(callback); + } else { + callback(); + } + } + } else { + imgNotSet(); + } + } + + + /** + * Sets the default [options](#options) for SVGInject. + * + * @param {Object} [options] - default [options](#options) for an injection. + */ + SVGInject.setOptions = function(options) { + defaultOptions = mergeOptions(defaultOptions, options); + }; + + + // Create a new instance of SVGInject + SVGInject.create = createSVGInject; + + + /** + * Used in onerror Event of an `` element to handle cases when the loading the original src fails + * (for example if file is not found or if the browser does not support SVG). This triggers a call to the + * options onFail hook if available. The optional second parameter will be set as the new src attribute + * for the img element. + * + * @param {HTMLImageElement} img - an img element + * @param {String} [fallbackSrc] - optional parameter fallback src + */ + SVGInject.err = function(img, fallbackSrc) { + if (img) { + if (img[__SVGINJECT] != FAIL) { + removeEventListeners(img); + + if (!IS_SVG_SUPPORTED) { + svgNotSupported(img, defaultOptions); + } else { + removeOnLoadAttribute(img); + loadFail(img, defaultOptions); + } + if (fallbackSrc) { + removeOnLoadAttribute(img); + img.src = fallbackSrc; + } + } + } else { + imgNotSet(); + } + }; + + window[globalName] = SVGInject; + + return SVGInject; + } + + var SVGInjectInstance = createSVGInject('SVGInject'); + + if (typeof module == 'object' && typeof module.exports == 'object') { + module.exports = SVGInjectInstance; + } +})(window, document); \ No newline at end of file diff --git a/client/public/stylesheets/layout/layout.css b/client/public/stylesheets/layout/layout.css index 43bd9917..31cb44fc 100644 --- a/client/public/stylesheets/layout/layout.css +++ b/client/public/stylesheets/layout/layout.css @@ -62,6 +62,7 @@ position: absolute; width: fit-content; z-index: 1000; + padding: 24px 30px; } #info-popup { diff --git a/client/public/stylesheets/olympus.css b/client/public/stylesheets/olympus.css index b540f9d2..1af78c87 100644 --- a/client/public/stylesheets/olympus.css +++ b/client/public/stylesheets/olympus.css @@ -67,6 +67,7 @@ button>img:first-child { position: relative; aspect-ratio: initial; height: 100%; + pointer-events: none; } form { @@ -316,16 +317,15 @@ form>div { .ol-panel-board>.panel-section { border-right: 1px solid #555; - margin: 10px 0; padding: 0 30px; } .ol-panel-board>.panel-section:first-child { - padding-left: 20px; + padding-left: 0px; } .ol-panel-board>.panel-section:last-child { - padding-right: 20px; + padding-right: 0px; } .ol-panel-board>.panel-section:last-of-type { @@ -861,6 +861,7 @@ nav.ol-panel> :last-child { .ol-destination-preview-icon { background-color: var(--secondary-yellow); border-radius: 999px; + cursor: grab; height: 52px; pointer-events: none; width: 52px; @@ -1130,9 +1131,12 @@ input[type=number]::-webkit-outer-spin-button { .ol-switch[data-value="false"]>.ol-switch-fill::before { transform: translateX(calc(var(--width) - 100%)); - } .ol-switch[data-value="true"]>.ol-switch-fill::after { transform: translateX(calc(var(--width) - var(--height))); +} + +.ol-switch[data-value="undefined"]>.ol-switch-fill::after { + transform: translateX(calc((var(--width) - var(--height)) * 0.5)); } \ No newline at end of file diff --git a/client/public/stylesheets/other/contextmenus.css b/client/public/stylesheets/other/contextmenus.css index cc1dbd34..e621d13d 100644 --- a/client/public/stylesheets/other/contextmenus.css +++ b/client/public/stylesheets/other/contextmenus.css @@ -29,8 +29,22 @@ width: fit-content; } -#context-menu-switch { +#coalition-switch { margin-right: 10px; + height: 25px; + width: 50px; +} + +#coalition-switch[data-value="false"]>.ol-switch-fill { + background-color: var(--primary-blue); +} + +#coalition-switch[data-value="true"]>.ol-switch-fill { + background-color: var(--primary-red); +} + +#coalition-switch[data-value="undefined"]>.ol-switch-fill { + background-color: var(--primary-neutral); } #map-contextmenu>div:nth-child(2) { @@ -109,11 +123,6 @@ border-top-right-radius: var(--border-radius-sm); } -#context-menu-switch .ol-switch-fill { - width: 40; -} - -[data-active-coalition="blue"].ol-switch-fill, [data-active-coalition="blue"].unit-spawn-button:hover, [data-active-coalition="blue"].unit-spawn-button.is-open, [data-active-coalition="blue"]#active-coalition-label, @@ -122,7 +131,6 @@ background-color: var(--primary-blue) } -[data-active-coalition="red"].ol-switch-fill, [data-active-coalition="red"].unit-spawn-button:hover, [data-active-coalition="red"].unit-spawn-button.is-open, [data-active-coalition="red"]#active-coalition-label, @@ -131,7 +139,6 @@ background-color: var(--primary-red) } -[data-active-coalition="neutral"].ol-switch-fill, [data-active-coalition="neutral"].unit-spawn-button:hover, [data-active-coalition="neutral"].unit-spawn-button.is-open, [data-active-coalition="neutral"]#active-coalition-label, @@ -158,18 +165,6 @@ cursor: default; } -[data-active-coalition="blue"].ol-switch-fill::after { - transform: translateX(0%); -} - -[data-active-coalition="red"].ol-switch-fill::after { - transform: translateX(100%); -} - -[data-active-coalition="neutral"].ol-switch-fill::after { - transform: translateX(50%); -} - [data-active-coalition="blue"]#active-coalition-label::after { content: "Create blue unit"; } @@ -250,6 +245,17 @@ background-color: orange; } +#aircraft-spawn-menu .ol-slider-value { + color: var(--accent-light-blue); + cursor: pointer; + font-size: 14px; + font-weight: bold; +} + +#aircraft-spawn-altitude-slider { + padding: 0px 10px; +} + /* Unit context menu */ #unit-contextmenu { display: flex; diff --git a/client/public/stylesheets/panels/unitcontrol.css b/client/public/stylesheets/panels/unitcontrol.css index 2b3ef0c7..0ad67eb6 100644 --- a/client/public/stylesheets/panels/unitcontrol.css +++ b/client/public/stylesheets/panels/unitcontrol.css @@ -29,10 +29,10 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { display: flex; font-size: 11px; height: 32px; + justify-content: space-between; margin-right: 5px; position: relative; width: calc(100% - 5px); - justify-content: space-between; } #unit-control-panel #selected-units-container button::after { @@ -41,8 +41,8 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { content: attr(data-label); font-size: 10px; padding: 4px 6px; - width: fit-content; white-space: nowrap; + width: fit-content; } #unit-control-panel #selected-units-container button:hover::after { @@ -139,11 +139,11 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { content: "ASL"; } -#airspeed-type-switch[data-value="true"]>.ol-switch-fill::before { +#speed-type-switch[data-value="true"]>.ol-switch-fill::before { content: "GS"; } -#airspeed-type-switch[data-value="false"]>.ol-switch-fill::before { +#speed-type-switch[data-value="false"]>.ol-switch-fill::before { content: "CAS"; } @@ -154,48 +154,48 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { font-weight: bold; } -#ai-on-off { +#unit-control-panel .switch-control { align-items: center; display: grid; - grid-template-columns: 1.35fr 0.65fr ; + grid-template-columns: 1.35fr 0.65fr; } -#ai-on-off>*:nth-child(2) { +#unit-control-panel .switch-control>*:nth-child(2) { justify-self: end; } -#ai-on-off>*:nth-child(3) { +#unit-control-panel .switch-control>*:nth-child(3) { color: var(--secondary-semitransparent-white); } -#ai-on-off h4 { +#unit-control-panel .switch-control h4 { margin: 0px; } -#on-off-switch { - width: 60px; +#unit-control-panel .switch-control .ol-switch { height: 25px; + width: 60px; } -#on-off-switch>.ol-switch-fill { +#unit-control-panel .switch-control .ol-switch-fill { background-color: var(--accent-light-blue); } -#on-off-switch>.ol-switch-fill::after { +#unit-control-panel .switch-control .ol-switch-fill::after { background-color: white; } -#on-off-switch[data-value="true"]>.ol-switch-fill::before { - content: "ON"; +#unit-control-panel .switch-control .ol-switch[data-value="true"]>.ol-switch-fill::before { + content: "YES"; } -#on-off-switch[data-value="false"]>.ol-switch-fill::before { - content: "OFF"; +#unit-control-panel .switch-control .ol-switch[data-value="false"]>.ol-switch-fill::before { + content: "NO"; } #advanced-settings-div { - display: flex; column-gap: 5px; + display: flex; } #advanced-settings-div>*:nth-child(2) { @@ -207,58 +207,20 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { } /* Element visibility control */ -#unit-control-panel:not([data-show-categories-tooltip]) #categories-tooltip { - display: none; -} - -#unit-control-panel:not([data-show-airspeed-slider]) #airspeed-slider { - display: none; -} - -#unit-control-panel:not([data-show-altitude-slider]) #altitude-slider { - display: none; -} - -#unit-control-panel:not([data-show-roe]) #roe { - display: none; -} - -#unit-control-panel:not([data-show-threat]) #threat { - display: none; -} - -#unit-control-panel:not([data-show-emissions-countermeasures]) #emissions-countermeasures { - display: none; -} - -#unit-control-panel:not([data-show-on-off]) #ai-on-off { - display: none; -} - -#unit-control-panel:not([data-show-advanced-settings-button]) #advanced-settings-button { - display: none; -} - -#advanced-settings-dialog:not([data-show-settings]) #general-settings { - display: none; -} - -#advanced-settings-dialog:not([data-show-tasking]) #tasking { - display: none; -} - -#advanced-settings-dialog:not([data-show-tanker]) #tanker-checkbox { - display: none; -} - -#advanced-settings-dialog:not([data-show-AWACS]) #AWACS-checkbox { - display: none; -} - -#advanced-settings-dialog:not([data-show-TACAN]) #TACAN-options { - display: none; -} - +#unit-control-panel:not([data-show-categories-tooltip]) #categories-tooltip, +#unit-control-panel:not([data-show-speed-slider]) #speed-slider, +#unit-control-panel:not([data-show-altitude-slider]) #altitude-slider, +#unit-control-panel:not([data-show-roe]) #roe, +#unit-control-panel:not([data-show-threat]) #threat, +#unit-control-panel:not([data-show-emissions-countermeasures]) #emissions-countermeasures, +#unit-control-panel:not([data-show-on-off]) #ai-on-off, +#unit-control-panel:not([data-show-follow-roads]) #follow-roads, +#unit-control-panel:not([data-show-advanced-settings-button]) #advanced-settings-button, +#advanced-settings-dialog:not([data-show-settings]) #general-settings, +#advanced-settings-dialog:not([data-show-tasking]) #tasking, +#advanced-settings-dialog:not([data-show-tanker]) #tanker-checkbox, +#advanced-settings-dialog:not([data-show-AWACS]) #AWACS-checkbox, +#advanced-settings-dialog:not([data-show-TACAN]) #TACAN-options, #advanced-settings-dialog:not([data-show-radio]) #radio-options { display: none; -} +} \ No newline at end of file diff --git a/client/public/stylesheets/panels/unitinfo.css b/client/public/stylesheets/panels/unitinfo.css index 8e20e07d..95703f8a 100644 --- a/client/public/stylesheets/panels/unitinfo.css +++ b/client/public/stylesheets/panels/unitinfo.css @@ -1,15 +1,38 @@ -#unit-info-panel #unit-name { - padding: 0px 0; - margin-bottom: 4px; +#unit-info-panel>* { + position: relative; + min-height: 100px; + bottom: 0px; } -#unit-info-panel #current-task { +#general { + display: flex; + flex-direction: column; + justify-content: space-between; + row-gap: 4px; + position: relative; +} + +#unit-label { + font-weight: bold; +} + +#unit-control { + color: var(--secondary-lighter-grey); + font-weight: bold; +} + +#unit-name { + margin-bottom: 4px; + padding: 0px 0; +} + +#current-task { border-radius: var(--border-radius-lg); - margin-top: 8px; + margin-top: auto; padding: 6px 16px; } -#unit-info-panel #current-task::after { +#current-task::after { content: attr(data-current-task); display: block; } @@ -17,25 +40,21 @@ #loadout { display: flex; overflow: visible; + width: 100%; + min-width: 125px; +} + +#loadout-container { + display: flex; + flex-direction: column; + justify-content: space-between; } #loadout-silhouette { - align-items: center; - display: flex; - justify-content: center; - width: 100px; -} - -#loadout-silhouette::before { - background-image: var(--loadout-background-image); - background-repeat: no-repeat; - background-size: 75px 75px; - content: ""; - display: block; filter: invert(100%); - height: 75px; - translate: -10px 0; - width: 75px; + height: 100px; + margin-right: 25px; + width: 100px; } #loadout-items { @@ -45,37 +64,37 @@ row-gap: 8px; } - #loadout-items>* { align-items: center; column-gap: 8px; display: flex; - justify-content: flex-end; white-space: nowrap; } #loadout-items>*::before { align-items: center; background-color: var(--secondary-light-grey); - border-radius: var(--border-radius-sm); + border-radius: 999px; content: attr(data-qty); display: flex; - font-weight: var(--font-weight-bolder); - padding: 1px 4px; + font-size: 11px; + font-weight: bold; + padding: 4px 6px; } #loadout-items>*::after { content: attr(data-item); + max-width: 125px; overflow: hidden; position: relative; text-overflow: ellipsis; - width: 80px; + width: 100%; } - #fuel-percentage { align-items: center; display: flex; + margin-top: auto; } #fuel-percentage::before { @@ -91,7 +110,6 @@ content: attr(data-percentage) "%"; } - #fuel-display { background-color: var(--background-grey); border-radius: var(--border-radius-md); diff --git a/client/public/themes/olympus/theme.css b/client/public/themes/olympus/theme.css index 2ce06a95..57f6bb3f 100644 --- a/client/public/themes/olympus/theme.css +++ b/client/public/themes/olympus/theme.css @@ -31,6 +31,7 @@ --secondary-dark-steel: #181e25; --secondary-gunmetal-grey: #2f2f2f; + --secondary-lighter-grey: #949ba7; --secondary-light-grey: #797e83; --secondary-semitransparent-white: #FFFFFFAA; --secondary-transparent-white: #FFFFFF30; diff --git a/client/src/@types/unit.d.ts b/client/src/@types/unit.d.ts index e6b1b345..d6163f68 100644 --- a/client/src/@types/unit.d.ts +++ b/client/src/@types/unit.d.ts @@ -37,9 +37,13 @@ interface TaskData { currentTask: string; activePath: any; targetSpeed: number; + targetSpeedType: string; targetAltitude: number; + targetAltitudeType: string; isTanker: boolean; isAWACS: boolean; + onOff: boolean; + followRoads: boolean; } interface OptionsData { @@ -51,15 +55,6 @@ interface OptionsData { generalSettings: GeneralSettings; } -interface UnitData { - baseData: BaseData; - flightData: FlightData; - missionData: MissionData; - formationData: FormationData; - taskData: TaskData; - optionsData: OptionsData; -} - interface TACAN { isOn: boolean; channel: number; @@ -91,4 +86,13 @@ interface UnitIconOptions { showAmmo: boolean, showSummary: boolean, rotateToHeading: boolean +} + +interface UnitData { + baseData: BaseData; + flightData: FlightData; + missionData: MissionData; + formationData: FormationData; + taskData: TaskData; + optionsData: OptionsData; } \ No newline at end of file diff --git a/client/src/constants/constants.ts b/client/src/constants/constants.ts new file mode 100644 index 00000000..0f91ed25 --- /dev/null +++ b/client/src/constants/constants.ts @@ -0,0 +1,102 @@ +import { LatLng, LatLngBounds, TileLayer, tileLayer } from "leaflet"; + +export const ROEs: string[] = ["Hold", "Return", "Designated", "Free"]; +export const reactionsToThreat: string[] = ["None", "Manoeuvre", "Passive", "Evade"]; +export const emissionsCountermeasures: string[] = ["Silent", "Attack", "Defend", "Free"]; + +export const ROEDescriptions: string[] = ["Hold (Never fire)", "Return (Only fire if fired upon)", "Designated (Attack the designated target only)", "Free (Attack anyone)"]; +export const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Manoeuvre (no countermeasures)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"]; +export const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar OFF, no ECM)", "Attack (Radar only for targeting, ECM only if locked)", "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)"]; + +export const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 }; +export const maxSpeedValues: { [key: string]: number } = { Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60 }; +export const speedIncrements: { [key: string]: number } = { Aircraft: 25, Helicopter: 10, NavyUnit: 5, GroundUnit: 5 }; +export const minAltitudeValues: { [key: string]: number } = { Aircraft: 0, Helicopter: 0 }; +export const maxAltitudeValues: { [key: string]: number } = { Aircraft: 50000, Helicopter: 10000 }; +export const altitudeIncrements: { [key: string]: number } = { Aircraft: 500, Helicopter: 100 }; + +export const minimapBoundaries = [ + [ // NTTR + new LatLng(39.7982463, -119.985425), + new LatLng(34.4037128, -119.7806729), + new LatLng(34.3483316, -112.4529351), + new LatLng(39.7372411, -112.1130805), + new LatLng(39.7982463, -119.985425) + ], + [ // Syria + new LatLng(37.3630556, 29.2686111), + new LatLng(31.8472222, 29.8975), + new LatLng(32.1358333, 42.1502778), + new LatLng(37.7177778, 42.3716667), + new LatLng(37.3630556, 29.2686111) + ], + [ // Caucasus + new LatLng(39.6170191, 27.634935), + new LatLng(38.8735863, 47.1423108), + new LatLng(47.3907982, 49.3101946), + new LatLng(48.3955879, 26.7753625), + new LatLng(39.6170191, 27.634935) + ], + [ // Persian Gulf + new LatLng(32.9355285, 46.5623682), + new LatLng(21.729393, 47.572675), + new LatLng(21.8501348, 63.9734737), + new LatLng(33.131584, 64.7313594), + new LatLng(32.9355285, 46.5623682) + ], + [ // Marianas + new LatLng(22.09, 135.0572222), + new LatLng(10.5777778, 135.7477778), + new LatLng(10.7725, 149.3918333), + new LatLng(22.5127778, 149.5427778), + new LatLng(22.09, 135.0572222) + ] +]; + +export const mapBounds = { + "Syria": { bounds: new LatLngBounds([31.8472222, 29.8975], [37.7177778, 42.3716667]), zoom: 5 }, + "MarianaIslands": { bounds: new LatLngBounds([10.5777778, 135.7477778], [22.5127778, 149.5427778]), zoom: 5 }, + "Nevada": { bounds: new LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]), zoom: 5 }, + "PersianGulf": { bounds: new LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]), zoom: 5 }, + "Caucasus": { bounds: new LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]), zoom: 4 }, + // TODO "Falklands" +} + +export const layers = { + "ArcGIS Satellite": { + urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", + maxZoom: 20, + minZoom: 1, + attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" + }, + "USGS Topo": { + urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', + minZoom: 1, + maxZoom: 20, + attribution: 'Tiles courtesy of the U.S. Geological Survey' + }, + "OpenStreetMap Mapnik": { + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 19, + attribution: '© OpenStreetMap contributors' + }, + "OPENVKarte": { + urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 18, + attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' + }, + "Esri.DeLorme": { + urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', + minZoom: 1, + maxZoom: 11, + attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', + }, + "CyclOSM": { + urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', + minZoom: 1, + maxZoom: 20, + attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' + } +} \ No newline at end of file diff --git a/client/src/controls/control.ts b/client/src/controls/control.ts index 2c111152..4786a862 100644 --- a/client/src/controls/control.ts +++ b/client/src/controls/control.ts @@ -1,5 +1,6 @@ export class Control { #container: HTMLElement | null; + expectedValue: any = null; constructor(ID: string) { this.#container = document.getElementById(ID); @@ -18,4 +19,16 @@ export class Control { getContainer() { return this.#container; } + + setExpectedValue(expectedValue: any) { + this.expectedValue = expectedValue; + } + + resetExpectedValue() { + this.expectedValue = null; + } + + checkExpectedValue(value: any) { + return this.expectedValue === null || value === this.expectedValue; + } } \ No newline at end of file diff --git a/client/src/controls/dropdown.ts b/client/src/controls/dropdown.ts index 502f1481..2877d80d 100644 --- a/client/src/controls/dropdown.ts +++ b/client/src/controls/dropdown.ts @@ -19,14 +19,12 @@ export class Dropdown { } this.#value.addEventListener("click", (ev) => { - this.#element.classList.toggle("is-open"); - this.#options.classList.toggle("scrollbar-visible", this.#options.scrollHeight > this.#options.clientHeight); - this.#clip(); + this.#toggle(); }); document.addEventListener("click", (ev) => { if (!(this.#value.contains(ev.target as Node) || this.#options.contains(ev.target as Node) || this.#element.contains(ev.target as Node))) { - this.#element.classList.remove("is-open"); + this.#close(); } }); @@ -46,12 +44,7 @@ export class Dropdown { button.addEventListener("click", (e: MouseEvent) => { e.stopPropagation(); - this.#value = document.createElement("div"); - this.#value.classList.add("ol-ellipsed"); - this.#value.innerText = option; - this.#close(); - this.#callback(option, e); - this.#index = idx; + this.selectValue(idx); }); return div; })); @@ -113,6 +106,8 @@ export class Dropdown { #open() { this.#element.classList.add("is-open"); + this.#options.classList.toggle("scrollbar-visible", this.#options.scrollHeight > this.#options.clientHeight); + this.#clip(); } #toggle() { diff --git a/client/src/controls/mapcontextmenu.ts b/client/src/controls/mapcontextmenu.ts index f381ca79..38cbba2c 100644 --- a/client/src/controls/mapcontextmenu.ts +++ b/client/src/controls/mapcontextmenu.ts @@ -5,6 +5,8 @@ import { aircraftDatabase } from "../units/aircraftdatabase"; import { groundUnitsDatabase } from "../units/groundunitsdatabase"; import { ContextMenu } from "./contextmenu"; import { Dropdown } from "./dropdown"; +import { Switch } from "./switch"; +import { Slider } from "./slider"; export interface SpawnOptions { role: string; @@ -13,24 +15,29 @@ export interface SpawnOptions { coalition: string; loadout: string | null; airbaseName: string | null; + altitude: number | null; } export class MapContextMenu extends ContextMenu { + #coalitionSwitch: Switch; #aircraftRoleDropdown: Dropdown; #aircraftTypeDropdown: Dropdown; #aircraftLoadoutDropdown: Dropdown; + #aircrafSpawnAltitudeSlider: Slider; #groundUnitRoleDropdown: Dropdown; #groundUnitTypeDropdown: Dropdown; - #spawnOptions: SpawnOptions = { role: "", type: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null }; - + #spawnOptions: SpawnOptions = { role: "", type: "", latlng: new LatLng(0, 0), loadout: null, coalition: "blue", airbaseName: null, altitude: 20000 }; + constructor(id: string) { super(id); - this.getContainer()?.querySelector("#context-menu-switch")?.addEventListener('click', (e) => this.#onToggleLeftClick(e)); - this.getContainer()?.querySelector("#context-menu-switch")?.addEventListener('contextmenu', (e) => this.#onToggleRightClick(e)); + this.#coalitionSwitch = new Switch("coalition-switch", this.#onSwitchClick); + this.#coalitionSwitch.setValue(false); + this.#coalitionSwitch.getContainer()?.addEventListener("contextmenu", (e) => this.#onSwitchRightClick(e)); this.#aircraftRoleDropdown = new Dropdown("aircraft-role-options", (role: string) => this.#setAircraftRole(role)); this.#aircraftTypeDropdown = new Dropdown("aircraft-type-options", (type: string) => this.#setAircraftType(type)); this.#aircraftLoadoutDropdown = new Dropdown("loadout-options", (loadout: string) => this.#setAircraftLoadout(loadout)); + this.#aircrafSpawnAltitudeSlider = new Slider("aircraft-spawn-altitude-slider", 0, 50000, "ft", (value: number) => {this.#spawnOptions.altitude = value;}); this.#groundUnitRoleDropdown = new Dropdown("ground-unit-role-options", (role: string) => this.#setGroundUnitRole(role)); this.#groundUnitTypeDropdown = new Dropdown("ground-unit-type-options", (type: string) => this.#setGroundUnitType(type)); @@ -61,6 +68,10 @@ export class MapContextMenu extends ContextMenu { spawnSmoke(e.detail.color, this.getLatLng()); }); + this.#aircrafSpawnAltitudeSlider.setIncrement(500); + this.#aircrafSpawnAltitudeSlider.setValue(20000); + this.#aircrafSpawnAltitudeSlider.setActive(true); + this.hide(); } @@ -102,26 +113,13 @@ export class MapContextMenu extends ContextMenu { this.#spawnOptions.latlng = latlng; } - #onToggleLeftClick(e: any) { - if (this.getContainer() != null) { - if (e.srcElement.dataset.activeCoalition == "blue") - setActiveCoalition("neutral"); - else if (e.srcElement.dataset.activeCoalition == "neutral") - setActiveCoalition("red"); - else - setActiveCoalition("blue"); - } + #onSwitchClick(value: boolean) { + value? setActiveCoalition("red"): setActiveCoalition("blue"); } - #onToggleRightClick(e: any) { - if (this.getContainer() != null) { - if (e.srcElement.dataset.activeCoalition == "red") - setActiveCoalition("neutral"); - else if (e.srcElement.dataset.activeCoalition == "neutral") - setActiveCoalition("blue"); - else - setActiveCoalition("red"); - } + #onSwitchRightClick(e: any) { + this.#coalitionSwitch.setValue(undefined); + setActiveCoalition("neutral"); } /********* Aircraft spawn menu *********/ diff --git a/client/src/controls/slider.ts b/client/src/controls/slider.ts index 656a9cbb..edfb18a1 100644 --- a/client/src/controls/slider.ts +++ b/client/src/controls/slider.ts @@ -2,38 +2,38 @@ import { zeroPad } from "../other/utils"; import { Control } from "./control"; export class Slider extends Control { - #callback: CallableFunction; + #callback: CallableFunction | null = null; #slider: HTMLInputElement | null = null; #valueText: HTMLElement | null = null; - #minValue: number; - #maxValue: number; - #increment: number; + #minValue: number = 0; + #maxValue: number = 0; + #increment: number = 0; #minMaxValueDiv: HTMLElement | null = null; - #unit: string; + #unitOfMeasure: string; #dragged: boolean = false; #value: number = 0; - constructor(ID: string, minValue: number, maxValue: number, unit: string, callback: CallableFunction) { + constructor(ID: string, minValue: number, maxValue: number, unitOfMeasure: string, callback: CallableFunction) { super(ID); - this.#callback = callback; - this.#minValue = minValue; - this.#maxValue = maxValue; - this.#increment = 1; - this.#unit = unit; + this.#callback = callback; + this.#unitOfMeasure = unitOfMeasure; this.#slider = this.getContainer()?.querySelector("input") as HTMLInputElement; if (this.#slider != null) { - this.#slider.addEventListener("input", (e: any) => this.#onInput()); + this.#slider.addEventListener("input", (e: any) => this.#update()); this.#slider.addEventListener("mousedown", (e: any) => this.#onStart()); this.#slider.addEventListener("mouseup", (e: any) => this.#onFinalize()); } this.#valueText = this.getContainer()?.querySelector(".ol-slider-value") as HTMLElement; this.#minMaxValueDiv = this.getContainer()?.querySelector(".ol-slider-min-max") as HTMLElement; + + this.setIncrement(1); + this.setMinMax(minValue, maxValue); } setActive(newActive: boolean) { - if (!this.#dragged) { + if (!this.getDragged()) { this.getContainer()?.classList.toggle("active", newActive); if (!newActive && this.#valueText != null) this.#valueText.innerText = "Mixed values"; @@ -41,27 +41,31 @@ export class Slider extends Control { } setMinMax(newMinValue: number, newMaxValue: number) { - this.#minValue = newMinValue; - this.#maxValue = newMaxValue; - this.#updateMax(); - if (this.#minMaxValueDiv != null) { - this.#minMaxValueDiv.setAttribute('data-min-value', `${this.#minValue}${this.#unit}`); - this.#minMaxValueDiv.setAttribute('data-max-value', `${this.#maxValue}${this.#unit}`); + if (this.#minValue != newMinValue || this.#maxValue != newMaxValue) { + this.#minValue = newMinValue; + this.#maxValue = newMaxValue; + this.#updateMaxValue(); + + if (this.#minMaxValueDiv != null) { + this.#minMaxValueDiv.setAttribute('data-min-value', `${this.#minValue}${this.#unitOfMeasure}`); + this.#minMaxValueDiv.setAttribute('data-max-value', `${this.#maxValue}${this.#unitOfMeasure}`); + } } } setIncrement(newIncrement: number) { - this.#increment = newIncrement; - this.#updateMax(); + if (this.#increment != newIncrement) { + this.#increment = newIncrement; + this.#updateMaxValue(); + } } - setValue(newValue: number) { - // Disable value setting if the user is dragging the element - if (!this.#dragged) { + setValue(newValue: number, ignoreExpectedValue: boolean = true) { + if (!this.getDragged() && (ignoreExpectedValue || this.checkExpectedValue(newValue))) { this.#value = newValue; if (this.#slider != null) this.#slider.value = String((newValue - this.#minValue) / (this.#maxValue - this.#minValue) * parseFloat(this.#slider.max)); - this.#onValue() + this.#update(); } } @@ -69,45 +73,51 @@ export class Slider extends Control { return this.#value; } + setDragged(newDragged: boolean) { + this.#dragged = newDragged; + } + getDragged() { return this.#dragged; } - #updateMax() { + #updateMaxValue() { var oldValue = this.getValue(); if (this.#slider != null) this.#slider.max = String((this.#maxValue - this.#minValue) / this.#increment); this.setValue(oldValue); } - #onValue() { + #update() { if (this.#valueText != null && this.#slider != null) { + /* Update the text value */ var value = this.#minValue + Math.round(parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)); var strValue = String(value); if (value > 1000) strValue = String(Math.floor(value / 1000)) + "," + zeroPad(value - Math.floor(value / 1000) * 1000, 3); - this.#valueText.innerText = strValue + " " + this.#unit.toUpperCase(); + this.#valueText.innerText = `${strValue} ${this.#unitOfMeasure.toUpperCase()}`; + /* Update the position of the slider */ var percentValue = parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * 90 + 5; - this.#slider.style.background = 'linear-gradient(to right, var(--accent-light-blue) 5%, var(--accent-light-blue) ' + percentValue + '%, var(--background-grey) ' + percentValue + '%, var(--background-grey) 100%)' + this.#slider.style.background = `linear-gradient(to right, var(--accent-light-blue) 5%, var(--accent-light-blue) ${percentValue}%, var(--background-grey) ${percentValue}%, var(--background-grey) 100%)` } this.setActive(true); } - #onInput() { - this.#onValue(); - } - #onStart() { - this.#dragged = true; + this.setDragged(true); } #onFinalize() { - this.#dragged = false; + this.setDragged(false); if (this.#slider != null) { - this.#value = this.#minValue + parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue); - this.#callback(this.getValue()); + this.resetExpectedValue(); + this.setValue(this.#minValue + parseFloat(this.#slider.value) / parseFloat(this.#slider.max) * (this.#maxValue - this.#minValue)); + if (this.#callback) { + this.#callback(this.getValue()); + this.setExpectedValue(this.getValue()); + } } } } \ No newline at end of file diff --git a/client/src/controls/switch.ts b/client/src/controls/switch.ts index b22ed0a5..60f9e3e5 100644 --- a/client/src/controls/switch.ts +++ b/client/src/controls/switch.ts @@ -1,12 +1,16 @@ import { Control } from "./control"; export class Switch extends Control { - #value: boolean = false; - constructor(ID: string, initialValue?: boolean) { + #value: boolean | undefined = false; + #callback: CallableFunction | null = null; + + constructor(ID: string, callback: CallableFunction, initialValue?: boolean) { super(ID); this.getContainer()?.addEventListener('click', (e) => this.#onToggle()); this.setValue(initialValue !== undefined? initialValue: true); + this.#callback = callback; + /* Add the toggle itself to the document */ const container = this.getContainer(); if (container != undefined){ @@ -14,14 +18,17 @@ export class Switch extends Control { const height = getComputedStyle(container).height; var el = document.createElement("div"); el.classList.add("ol-switch-fill"); - el.style.setProperty("--width", width? width: "0px"); - el.style.setProperty("--height", height? height: "0px"); + el.style.setProperty("--width", width? width: "0"); + el.style.setProperty("--height", height? height: "0"); this.getContainer()?.appendChild(el); } } - setValue(value: boolean) { - this.#value = value; - this.getContainer()?.setAttribute("data-value", String(value)); + + setValue(newValue: boolean | undefined, ignoreExpectedValue: boolean = true) { + if (ignoreExpectedValue || this.checkExpectedValue(newValue)) { + this.#value = newValue; + this.getContainer()?.setAttribute("data-value", String(newValue)); + } } getValue() { @@ -29,6 +36,11 @@ export class Switch extends Control { } #onToggle() { + this.resetExpectedValue(); this.setValue(!this.getValue()); + if (this.#callback) { + this.#callback(this.getValue()); + this.setExpectedValue(this.getValue()); + } } } \ No newline at end of file diff --git a/client/src/index.ts b/client/src/index.ts index 06f416ff..ac14f5c6 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -98,8 +98,6 @@ function readConfig(config: any) { } function setupEvents() { - window.onanimationiteration = console.log; - /* Generic clicks */ document.addEventListener("click", (ev) => { if (ev instanceof MouseEvent && ev.target instanceof HTMLElement) { diff --git a/client/src/map/destinationpreviewmarker.ts b/client/src/map/destinationpreviewmarker.ts index 1146992d..aa76f395 100644 --- a/client/src/map/destinationpreviewmarker.ts +++ b/client/src/map/destinationpreviewmarker.ts @@ -6,7 +6,7 @@ export class DestinationPreviewMarker extends CustomMarker { this.setIcon(new DivIcon({ iconSize: [52, 52], iconAnchor: [26, 26], - className: "leaflet-destination-preview" + className: "leaflet-destination-preview", })); var el = document.createElement("div"); el.classList.add("ol-destination-preview-icon"); diff --git a/client/src/map/map.ts b/client/src/map/map.ts index b4516bdd..6a75186b 100644 --- a/client/src/map/map.ts +++ b/client/src/map/map.ts @@ -12,6 +12,7 @@ import { DestinationPreviewMarker } from "./destinationpreviewmarker"; import { TemporaryUnitMarker } from "./temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' +import { layers as mapLayers, mapBounds, minimapBoundaries } from "../constants/constants"; L.Map.addInitHook('addHandler', 'boxSelect', BoxSelect); @@ -58,10 +59,10 @@ export class Map extends L.Map { super(ID, { doubleClickZoom: false, zoomControl: false, boxZoom: false, boxSelect: true, zoomAnimation: true, maxBoundsViscosity: 1.0, minZoom: 7, keyboard: true, keyboardPanDelta: 0 }); this.setView([37.23, -115.8], 10); - this.setLayer("ArcGIS Satellite"); + this.setLayer(Object.keys(mapLayers)[0]); /* Minimap */ - var minimapLayer = new L.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { minZoom: 0, maxZoom: 13 }); + var minimapLayer = new L.TileLayer(mapLayers[Object.keys(mapLayers)[0] as keyof typeof mapLayers].urlTemplate, { minZoom: 0, maxZoom: 13 }); this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); var miniMapPolyline = new L.Polyline(this.#getMinimapBoundaries(), { color: '#202831' }); miniMapPolyline.addTo(this.#miniMapLayerGroup); @@ -124,59 +125,30 @@ export class Map extends L.Map { } setLayer(layerName: string) { - if (this.#layer != null) { + if (this.#layer != null) this.removeLayer(this.#layer) + + if (layerName in mapLayers){ + const layerData = mapLayers[layerName as keyof typeof mapLayers]; + var options: L.TileLayerOptions = { + attribution: layerData.attribution, + minZoom: layerData.minZoom, + maxZoom: layerData.maxZoom + }; + this.#layer = new L.TileLayer(layerData.urlTemplate, options); } - if (layerName == "ArcGIS Satellite") { - this.#layer = L.tileLayer("https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", { - attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community" - }); - } - else if (layerName == "USGS Topo") { - this.#layer = L.tileLayer('https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}', { - maxZoom: 20, - attribution: 'Tiles courtesy of the U.S. Geological Survey' - }); - } - else if (layerName == "OpenStreetMap Mapnik") { - this.#layer = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap contributors' - }); - } - else if (layerName == "OPENVKarte") { - this.#layer = L.tileLayer('https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png', { - maxZoom: 18, - attribution: 'Map memomaps.de CC-BY-SA, map data © OpenStreetMap contributors' - }); - } - else if (layerName == "Esri.DeLorme") { - this.#layer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Copyright: ©2012 DeLorme', - minZoom: 1, - maxZoom: 11 - }); - } - else if (layerName == "CyclOSM") { - this.#layer = L.tileLayer('https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png', { - maxZoom: 20, - attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' - }); - } this.#layer?.addTo(this); } getLayers() { - return ["ArcGIS Satellite", "USGS Topo", "OpenStreetMap Mapnik", "OPENVKarte", "Esri.DeLorme", "CyclOSM"] + return Object.keys(mapLayers); } /* State machine */ setState(state: string) { this.#state = state; if (this.#state === IDLE) { - L.DomUtil.removeClass(this.getContainer(), 'crosshair-cursor-enabled'); - /* Remove all the destination preview markers */ this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { this.removeLayer(marker); @@ -188,8 +160,6 @@ export class Map extends L.Map { this.#destinationRotationCenter = null; } else if (this.#state === MOVE_UNIT) { - L.DomUtil.addClass(this.getContainer(), 'crosshair-cursor-enabled'); - /* Remove all the exising destination preview markers */ this.#destinationPreviewMarkers.forEach((marker: L.Marker) => { this.removeLayer(marker); @@ -199,7 +169,7 @@ export class Map extends L.Map { if (getUnitsManager().getSelectedUnits({ excludeHumans: true }).length > 1 && getUnitsManager().getSelectedUnits({ excludeHumans: true }).length < 20) { /* Create the unit destination preview markers */ this.#destinationPreviewMarkers = getUnitsManager().getSelectedUnits({ excludeHumans: true }).map((unit: Unit) => { - var marker = new DestinationPreviewMarker(this.getMouseCoordinates()); + var marker = new DestinationPreviewMarker(this.getMouseCoordinates(), {interactive: false}); marker.addTo(this); return marker; }) @@ -295,20 +265,9 @@ export class Map extends L.Map { setTheatre(theatre: string) { var bounds = new L.LatLngBounds([-90, -180], [90, 180]); var miniMapZoom = 5; - if (theatre == "Syria") - bounds = new L.LatLngBounds([31.8472222, 29.8975], [37.7177778, 42.3716667]); - else if (theatre == "MarianaIslands") - bounds = new L.LatLngBounds([10.5777778, 135.7477778], [22.5127778, 149.5427778]); - else if (theatre == "Nevada") - bounds = new L.LatLngBounds([34.4037128, -119.7806729], [39.7372411, -112.1130805]) - else if (theatre == "PersianGulf") - bounds = new L.LatLngBounds([21.729393, 47.572675], [33.131584, 64.7313594]) - else if (theatre == "Falklands") { - // TODO - } - else if (theatre == "Caucasus") { - bounds = new L.LatLngBounds([39.6170191, 27.634935], [47.3907982, 49.3101946]) - miniMapZoom = 4; + if (theatre in mapBounds) { + bounds = mapBounds[theatre as keyof typeof mapBounds].bounds; + miniMapZoom = mapBounds[theatre as keyof typeof mapBounds].zoom; } this.setView(bounds.getCenter(), 8); @@ -426,7 +385,7 @@ export class Map extends L.Map { if (!e.originalEvent.ctrlKey) { getUnitsManager().selectedUnitsClearDestinations(); } - getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, !e.originalEvent.shiftKey, this.#destinationGroupRotation) + getUnitsManager().selectedUnitsAddDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : e.latlng, e.originalEvent.shiftKey, this.#destinationGroupRotation) this.#destinationGroupRotation = 0; this.#destinationRotationCenter = null; this.#computeDestinationRotation = false; @@ -481,48 +440,13 @@ export class Map extends L.Map { #getMinimapBoundaries() { /* Draw the limits of the maps in the minimap*/ - return [[ // NTTR - new L.LatLng(39.7982463, -119.985425), - new L.LatLng(34.4037128, -119.7806729), - new L.LatLng(34.3483316, -112.4529351), - new L.LatLng(39.7372411, -112.1130805), - new L.LatLng(39.7982463, -119.985425) - ], - [ // Syria - new L.LatLng(37.3630556, 29.2686111), - new L.LatLng(31.8472222, 29.8975), - new L.LatLng(32.1358333, 42.1502778), - new L.LatLng(37.7177778, 42.3716667), - new L.LatLng(37.3630556, 29.2686111) - ], - [ // Caucasus - new L.LatLng(39.6170191, 27.634935), - new L.LatLng(38.8735863, 47.1423108), - new L.LatLng(47.3907982, 49.3101946), - new L.LatLng(48.3955879, 26.7753625), - new L.LatLng(39.6170191, 27.634935) - ], - [ // Persian Gulf - new L.LatLng(32.9355285, 46.5623682), - new L.LatLng(21.729393, 47.572675), - new L.LatLng(21.8501348, 63.9734737), - new L.LatLng(33.131584, 64.7313594), - new L.LatLng(32.9355285, 46.5623682) - ], - [ // Marianas - new L.LatLng(22.09, 135.0572222), - new L.LatLng(10.5777778, 135.7477778), - new L.LatLng(10.7725, 149.3918333), - new L.LatLng(22.5127778, 149.5427778), - new L.LatLng(22.09, 135.0572222) - ] - ]; + return minimapBoundaries; } #updateDestinationPreview(e: any) { Object.values(getUnitsManager().selectedUnitsComputeGroupDestination(this.#computeDestinationRotation && this.#destinationRotationCenter != null ? this.#destinationRotationCenter : this.getMouseCoordinates(), this.#destinationGroupRotation)).forEach((latlng: L.LatLng, idx: number) => { if (idx < this.#destinationPreviewMarkers.length) - this.#destinationPreviewMarkers[idx].setLatLng(!e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); + this.#destinationPreviewMarkers[idx].setLatLng(e.originalEvent.shiftKey ? latlng : this.getMouseCoordinates()); }) } diff --git a/client/src/missionhandler/missionhandler.ts b/client/src/missionhandler/missionhandler.ts index 93b13192..2471719e 100644 --- a/client/src/missionhandler/missionhandler.ts +++ b/client/src/missionhandler/missionhandler.ts @@ -37,7 +37,7 @@ export class MissionHandler for (let idx in data.airbases) { var airbase = data.airbases[idx] - if (this.#airbases[idx] === undefined) + if (this.#airbases[idx] === undefined && airbase.callsign != '') { this.#airbases[idx] = new Airbase({ position: new LatLng(airbase.latitude, airbase.longitude), @@ -45,7 +45,8 @@ export class MissionHandler }).addTo(getMap()); this.#airbases[idx].on('contextmenu', (e) => this.#onAirbaseClick(e)); } - if (airbase.latitude && airbase.longitude && airbase.coalition) + + if (this.#airbases[idx] != undefined && airbase.latitude && airbase.longitude && airbase.coalition) { this.#airbases[idx].setLatLng(new LatLng(airbase.latitude, airbase.longitude)); this.#airbases[idx].setCoalition(airbase.coalition); diff --git a/client/src/panels/unitcontrolpanel.ts b/client/src/panels/unitcontrolpanel.ts index 8a84bdf8..27e68b79 100644 --- a/client/src/panels/unitcontrolpanel.ts +++ b/client/src/panels/unitcontrolpanel.ts @@ -3,38 +3,21 @@ import { getUnitsManager } from ".."; import { Dropdown } from "../controls/dropdown"; import { Slider } from "../controls/slider"; import { aircraftDatabase } from "../units/aircraftdatabase"; -import { groundUnitsDatabase } from "../units/groundunitsdatabase"; -import { Aircraft, GroundUnit, Unit } from "../units/unit"; -import { UnitDatabase } from "../units/unitdatabase"; +import { Unit } from "../units/unit"; import { Panel } from "./panel"; import { Switch } from "../controls/switch"; - -const ROEs: string[] = ["Hold", "Return", "Designated", "Free"]; -const reactionsToThreat: string[] = ["None", "Manoeuvre", "Passive", "Evade"]; -const emissionsCountermeasures: string[] = ["Silent", "Attack", "Defend", "Free"]; - -const ROEDescriptions: string[] = ["Hold (Never fire)", "Return (Only fire if fired upon)", "Designated (Attack the designated target only)", "Free (Attack anyone)"]; -const reactionsToThreatDescriptions: string[] = ["None (No reaction)", "Manoeuvre (no countermeasures)", "Passive (Countermeasures only, no manoeuvre)", "Evade (Countermeasures and manoeuvers)"]; -const emissionsCountermeasuresDescriptions: string[] = ["Silent (Radar OFF, no ECM)", "Attack (Radar only for targeting, ECM only if locked)", "Defend (Radar for searching, ECM if locked)", "Always on (Radar and ECM always on)"]; - -const minSpeedValues: { [key: string]: number } = { Aircraft: 100, Helicopter: 0, NavyUnit: 0, GroundUnit: 0 }; -const maxSpeedValues: { [key: string]: number } = { Aircraft: 800, Helicopter: 300, NavyUnit: 60, GroundUnit: 60 }; -const speedIncrements: { [key: string]: number } = { Aircraft: 25, Helicopter: 10, NavyUnit: 5, GroundUnit: 5 }; -const minAltitudeValues: { [key: string]: number } = { Aircraft: 0, Helicopter: 0 }; -const maxAltitudeValues: { [key: string]: number } = { Aircraft: 50000, Helicopter: 10000 }; -const altitudeIncrements: { [key: string]: number } = { Aircraft: 500, Helicopter: 100 }; +import { ROEDescriptions, ROEs, altitudeIncrements, emissionsCountermeasures, emissionsCountermeasuresDescriptions, maxAltitudeValues, maxSpeedValues, minAltitudeValues, minSpeedValues, reactionsToThreat, reactionsToThreatDescriptions, speedIncrements } from "../constants/constants"; export class UnitControlPanel extends Panel { #altitudeSlider: Slider; #altitudeTypeSwitch: Switch; - #airspeedSlider: Slider; - #airspeedTypeSwitch: Switch; + #speedSlider: Slider; + #speedTypeSwitch: Switch; #onOffSwitch: Switch; + #followRoadsSwitch: Switch; #TACANXYDropdown: Dropdown; #radioDecimalsDropdown: Dropdown; #radioCallsignDropdown: Dropdown; - #expectedAltitude: number = -1; - #expectedSpeed: number = -1; #optionButtons: { [key: string]: HTMLButtonElement[] } = {} #advancedSettingsDialog: HTMLElement; @@ -42,17 +25,11 @@ export class UnitControlPanel extends Panel { super(ID); /* Unit control sliders */ - this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { - this.#expectedAltitude = value; - getUnitsManager().selectedUnitsSetAltitude(value * 0.3048); - }); - this.#altitudeTypeSwitch = new Switch("altitude-type-switch"); + this.#altitudeSlider = new Slider("altitude-slider", 0, 100, "ft", (value: number) => { getUnitsManager().selectedUnitsSetAltitude(value * 0.3048); }); + this.#altitudeTypeSwitch = new Switch("altitude-type-switch", (value: boolean) => { getUnitsManager().selectedUnitsSetAltitudeType(value? "AGL": "ASL"); }); - this.#airspeedSlider = new Slider("airspeed-slider", 0, 100, "kts", (value: number) => { - this.#expectedSpeed = value; - getUnitsManager().selectedUnitsSetSpeed(value / 1.94384); - }); - this.#airspeedTypeSwitch = new Switch("airspeed-type-switch"); + this.#speedSlider = new Slider("speed-slider", 0, 100, "kts", (value: number) => { getUnitsManager().selectedUnitsSetSpeed(value / 1.94384); }); + this.#speedTypeSwitch = new Switch("speed-type-switch", (value: boolean) => { getUnitsManager().selectedUnitsSetSpeedType(value? "GS": "CAS"); }); /* Option buttons */ this.#optionButtons["ROE"] = ROEs.map((option: string, index: number) => { @@ -72,7 +49,14 @@ export class UnitControlPanel extends Panel { this.getElement().querySelector("#emissions-countermeasures-buttons-container")?.append(...this.#optionButtons["emissionsCountermeasures"]); /* On off switch */ - this.#onOffSwitch = new Switch("on-off-switch"); + this.#onOffSwitch = new Switch("on-off-switch", (value: boolean) => { + getUnitsManager().selectedUnitsSetOnOff(value); + }); + + /* Follow roads switch */ + this.#followRoadsSwitch = new Switch("follow-roads-switch", (value: boolean) => { + getUnitsManager().selectedUnitsSetFollowRoads(value); + }); /* Advanced settings dialog */ this.#advancedSettingsDialog = document.querySelector("#advanced-settings-dialog"); @@ -98,26 +82,18 @@ export class UnitControlPanel extends Panel { this.hide(); } - // Do this after panel is hidden (make sure there's a reset) - hide() { - super.hide(); - - this.#expectedAltitude = -1; - this.#expectedSpeed = -1; + show() { + super.show(); + this.#speedTypeSwitch.resetExpectedValue(); + this.#altitudeTypeSwitch.resetExpectedValue(); + this.#onOffSwitch.resetExpectedValue(); + this.#followRoadsSwitch.resetExpectedValue(); } addButtons() { var units = getUnitsManager().getSelectedUnits(); if (units.length < 20) { this.getElement().querySelector("#selected-units-container")?.replaceChildren(...units.map((unit: Unit, index: number) => { - let database: UnitDatabase | null; - if (unit instanceof Aircraft) - database = aircraftDatabase; - else if (unit instanceof GroundUnit) - database = groundUnitsDatabase; - else - database = null; // TODO add databases for other unit types - var button = document.createElement("button"); var callsign = unit.getBaseData().unitName || ""; var label = unit.getDatabase()?.getByName(unit.getBaseData().name)?.label || unit.getBaseData().name; @@ -150,42 +126,42 @@ export class UnitControlPanel extends Panel { if (element != null && units.length > 0) { /* Toggle visibility of control elements */ element.toggleAttribute("data-show-categories-tooltip", selectedUnitsTypes.length > 1); - element.toggleAttribute("data-show-airspeed-slider", selectedUnitsTypes.length == 1); + element.toggleAttribute("data-show-speed-slider", selectedUnitsTypes.length == 1); element.toggleAttribute("data-show-altitude-slider", selectedUnitsTypes.length == 1 && (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter"))); element.toggleAttribute("data-show-roe", true); element.toggleAttribute("data-show-threat", (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter")) && !(selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit"))); element.toggleAttribute("data-show-emissions-countermeasures", (selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter")) && !(selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit"))); element.toggleAttribute("data-show-on-off", (selectedUnitsTypes.includes("GroundUnit") || selectedUnitsTypes.includes("NavyUnit")) && !(selectedUnitsTypes.includes("Aircraft") || selectedUnitsTypes.includes("Helicopter"))); + element.toggleAttribute("data-show-follow-roads", (selectedUnitsTypes.length == 1 && selectedUnitsTypes.includes("GroundUnit"))); element.toggleAttribute("data-show-advanced-settings-button", units.length == 1); /* Flight controls */ - var targetAltitude = getUnitsManager().getSelectedUnitsTargetAltitude(); - var targetSpeed = getUnitsManager().getSelectedUnitsTargetSpeed(); + var targetAltitude: number | undefined = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetAltitude}); + var targetAltitudeType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetAltitudeType}); + var targetSpeed = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetSpeed}); + var targetSpeedType = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().targetSpeedType}); + var onOff = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().onOff}); + var followRoads = getUnitsManager().getSelectedUnitsVariable((unit: Unit) => {return unit.getTaskData().followRoads}); if (selectedUnitsTypes.length == 1) { - this.#airspeedSlider.setMinMax(minSpeedValues[selectedUnitsTypes[0]], maxSpeedValues[selectedUnitsTypes[0]]); + this.#altitudeTypeSwitch.setValue(targetAltitudeType != undefined? targetAltitudeType == "AGL": undefined, false); + this.#speedTypeSwitch.setValue(targetSpeedType != undefined? targetSpeedType == "GS": undefined, false); + + this.#speedSlider.setMinMax(minSpeedValues[selectedUnitsTypes[0]], maxSpeedValues[selectedUnitsTypes[0]]); this.#altitudeSlider.setMinMax(minAltitudeValues[selectedUnitsTypes[0]], maxAltitudeValues[selectedUnitsTypes[0]]); - this.#airspeedSlider.setIncrement(speedIncrements[selectedUnitsTypes[0]]); + this.#speedSlider.setIncrement(speedIncrements[selectedUnitsTypes[0]]); this.#altitudeSlider.setIncrement(altitudeIncrements[selectedUnitsTypes[0]]); - this.#airspeedSlider.setActive(targetSpeed != undefined); - if (targetSpeed != undefined) { - targetSpeed *= 1.94384; - if (this.#updateCanSetSpeedSlider(targetSpeed)) { - this.#airspeedSlider.setValue(targetSpeed); - } - } + this.#speedSlider.setActive(targetSpeed != undefined); + if (targetSpeed != undefined) + this.#speedSlider.setValue(targetSpeed * 1.94384, false); this.#altitudeSlider.setActive(targetAltitude != undefined); - if (targetAltitude != undefined) { - targetAltitude /= 0.3048; - if (this.#updateCanSetAltitudeSlider(targetAltitude)) { - this.#altitudeSlider.setValue(targetAltitude); - } - } + if (targetAltitude != undefined) + this.#altitudeSlider.setValue(targetAltitude / 0.3048, false); } else { - this.#airspeedSlider.setActive(false); + this.#speedSlider.setActive(false); this.#altitudeSlider.setActive(false); } @@ -201,27 +177,13 @@ export class UnitControlPanel extends Panel { this.#optionButtons["emissionsCountermeasures"].forEach((button: HTMLButtonElement) => { button.classList.toggle("selected", units.every((unit: Unit) => unit.getOptionsData().emissionsCountermeasures === button.value)) }); + + this.#onOffSwitch.setValue(onOff, false); + this.#followRoadsSwitch.setValue(followRoads, false); } } } - /* Update function will only be allowed to update the sliders once it's matched the expected value for the first time (due to lag of Ajax request) */ - #updateCanSetAltitudeSlider(altitude: number) { - if (this.#expectedAltitude < 0 || altitude === this.#expectedAltitude) { - this.#expectedAltitude = -1; - return true; - } - return false; - } - - #updateCanSetSpeedSlider(altitude: number) { - if (this.#expectedSpeed < 0 || altitude === this.#expectedSpeed) { - this.#expectedSpeed = -1; - return true; - } - return false; - } - #updateAdvancedSettingsDialog(units: Unit[]) { if (units.length == 1) diff --git a/client/src/panels/unitinfopanel.ts b/client/src/panels/unitinfopanel.ts index 4d4eb77e..3410a983 100644 --- a/client/src/panels/unitinfopanel.ts +++ b/client/src/panels/unitinfopanel.ts @@ -16,7 +16,7 @@ export class UnitInfoPanel extends Panel { #latitude: HTMLElement; #longitude: HTMLElement; #loadoutContainer: HTMLElement; - #silhouette: HTMLElement; + #silhouette: HTMLImageElement; #unitControl: HTMLElement; #unitLabel: HTMLElement; #unitName: HTMLElement; @@ -24,21 +24,21 @@ export class UnitInfoPanel extends Panel { constructor(ID: string) { super(ID); - this.#altitude = (this.getElement().querySelector("#altitude")); - this.#currentTask = (this.getElement().querySelector("#current-task")); - this.#groundSpeed = (this.getElement().querySelector("#ground-speed")); - this.#fuelBar = (this.getElement().querySelector("#fuel-bar")); - this.#fuelPercentage = (this.getElement().querySelector("#fuel-percentage")); - this.#groupName = (this.getElement().querySelector("#group-name")); - this.#heading = (this.getElement().querySelector("#heading")); - this.#name = (this.getElement().querySelector("#name")); - this.#latitude = (this.getElement().querySelector("#latitude")); - this.#loadoutContainer = (this.getElement().querySelector("#loadout-container")); - this.#longitude = (this.getElement().querySelector("#longitude")); - this.#silhouette = (this.getElement().querySelector("#loadout-silhouette")); - this.#unitControl = (this.getElement().querySelector("#unit-control")); - this.#unitLabel = (this.getElement().querySelector("#unit-label")); - this.#unitName = (this.getElement().querySelector("#unit-name")); + this.#altitude = (this.getElement().querySelector("#altitude")) as HTMLElement; + this.#currentTask = (this.getElement().querySelector("#current-task")) as HTMLElement; + this.#groundSpeed = (this.getElement().querySelector("#ground-speed")) as HTMLElement; + this.#fuelBar = (this.getElement().querySelector("#fuel-bar")) as HTMLElement; + this.#fuelPercentage = (this.getElement().querySelector("#fuel-percentage")) as HTMLElement; + this.#groupName = (this.getElement().querySelector("#group-name")) as HTMLElement; + this.#heading = (this.getElement().querySelector("#heading")) as HTMLElement; + this.#name = (this.getElement().querySelector("#name")) as HTMLElement; + this.#latitude = (this.getElement().querySelector("#latitude")) as HTMLElement; + this.#loadoutContainer = (this.getElement().querySelector("#loadout-container")) as HTMLElement; + this.#longitude = (this.getElement().querySelector("#longitude")) as HTMLElement; + this.#silhouette = (this.getElement().querySelector("#loadout-silhouette")) as HTMLImageElement; + this.#unitControl = (this.getElement().querySelector("#unit-control")) as HTMLElement; + this.#unitLabel = (this.getElement().querySelector("#unit-label")) as HTMLElement; + this.#unitName = (this.getElement().querySelector("#unit-name")) as HTMLElement; document.addEventListener("unitsSelection", (e: CustomEvent) => this.#onUnitsSelection(e.detail)); document.addEventListener("unitsDeselection", (e: CustomEvent) => this.#onUnitsDeselection(e.detail)); @@ -69,18 +69,15 @@ export class UnitInfoPanel extends Panel { this.#currentTask.dataset.currentTask = unit.getTaskData().currentTask !== ""? unit.getTaskData().currentTask: "No task"; this.#currentTask.dataset.coalition = unit.getMissionData().coalition; - this.#silhouette.setAttribute( "style", `--loadout-background-image:url('/images/units/${aircraftDatabase.getByName( baseData.name )?.filename}');` );; - + this.#silhouette.src = `/images/units/${unit.getDatabase()?.getByName(baseData.name)?.filename}`; + this.#silhouette.classList.toggle("hide", unit.getDatabase()?.getByName(baseData.name)?.filename == undefined || unit.getDatabase()?.getByName(baseData.name)?.filename == ''); + /* Add the loadout elements */ const items = this.#loadoutContainer.querySelector( "#loadout-items" ); - if ( items ) { - const ammo = Object.values( unit.getMissionData().ammo ); - if ( ammo.length > 0 ) { - items.replaceChildren(...Object.values(unit.getMissionData().ammo).map( (ammo: any) => { var el = document.createElement("div"); @@ -91,25 +88,28 @@ export class UnitInfoPanel extends Panel { )); } else { - - items.innerText = "No loadout"; - + items.innerText = "No loadout"; } - } } } #onUnitsSelection(units: Unit[]){ if (units.length == 1) + { this.show(); + this.#onUnitUpdate(units[0]); + } else this.hide(); } #onUnitsDeselection(units: Unit[]){ if (units.length == 1) + { this.show(); + this.#onUnitUpdate(units[0]); + } else this.hide(); } diff --git a/client/src/server/server.ts b/client/src/server/server.ts index 2786852c..7ee189b1 100644 --- a/client/src/server/server.ts +++ b/client/src/server/server.ts @@ -29,7 +29,7 @@ export function setCredentials(newUsername: string, newCredentials: string) { credentials = newCredentials; } -export function GET(callback: CallableFunction, uri: string, options?: {time?: number}) { +export function GET(callback: CallableFunction, uri: string, options?: { time?: number }) { var xmlHttp = new XMLHttpRequest(); /* Assemble the request options string */ @@ -37,15 +37,14 @@ export function GET(callback: CallableFunction, uri: string, options?: {time?: n if (options?.time != undefined) optionsString = `time=${options.time}`; - - xmlHttp.open("GET", `${demoEnabled? DEMO_ADDRESS: REST_ADDRESS}/${uri}${optionsString? `?${optionsString}`: ''}`, true); + + xmlHttp.open("GET", `${demoEnabled ? DEMO_ADDRESS : REST_ADDRESS}/${uri}${optionsString ? `?${optionsString}` : ''}`, true); if (credentials) xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); xmlHttp.onload = function (e) { if (xmlHttp.status == 200) { var data = JSON.parse(xmlHttp.responseText); - if (uri !== UNITS_URI || (options?.time == 0) || parseInt(data.time) > lastUpdateTime) - { + if (uri !== UNITS_URI || (options?.time == 0) || parseInt(data.time) > lastUpdateTime) { callback(data); lastUpdateTime = parseInt(data.time); if (isNaN(lastUpdateTime)) @@ -66,14 +65,14 @@ export function GET(callback: CallableFunction, uri: string, options?: {time?: n xmlHttp.send(null); } -export function POST(request: object, callback: CallableFunction){ +export function POST(request: object, callback: CallableFunction) { var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("PUT", demoEnabled? DEMO_ADDRESS: REST_ADDRESS); + xmlHttp.open("PUT", demoEnabled ? DEMO_ADDRESS : REST_ADDRESS); xmlHttp.setRequestHeader("Content-Type", "application/json"); if (credentials) xmlHttp.setRequestHeader("Authorization", "Basic " + credentials); - xmlHttp.onreadystatechange = () => { - callback(); + xmlHttp.onreadystatechange = () => { + callback(); }; xmlHttp.send(JSON.stringify(request)); } @@ -113,7 +112,7 @@ export function getMission(callback: CallableFunction) { } export function getUnits(callback: CallableFunction, refresh: boolean = false) { - GET(callback, `${UNITS_URI}`, {time: refresh? 0: lastUpdateTime}); + GET(callback, `${UNITS_URI}`, { time: refresh ? 0 : lastUpdateTime }); } export function addDestination(ID: number, path: any) { @@ -135,7 +134,7 @@ export function spawnGroundUnit(spawnOptions: SpawnOptions) { } export function spawnAircraft(spawnOptions: SpawnOptions) { - var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "payloadName": spawnOptions.loadout != null? spawnOptions.loadout: "", "airbaseName": spawnOptions.airbaseName != null? spawnOptions.airbaseName: ""}; + var command = { "type": spawnOptions.type, "location": spawnOptions.latlng, "coalition": spawnOptions.coalition, "altitude": spawnOptions.altitude, "payloadName": spawnOptions.loadout != null ? spawnOptions.loadout : "", "airbaseName": spawnOptions.airbaseName != null ? spawnOptions.airbaseName : "" }; var data = { "spawnAir": command } POST(data, () => { }); } @@ -146,12 +145,12 @@ export function attackUnit(ID: number, targetID: number) { POST(data, () => { }); } -export function followUnit(ID: number, targetID: number, offset: {"x": number, "y": number, "z": number}) { +export function followUnit(ID: number, targetID: number, offset: { "x": number, "y": number, "z": number }) { // X: front-rear, positive front // Y: top-bottom, positive bottom // Z: left-right, positive right - - var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"]}; + + var command = { "ID": ID, "targetID": targetID, "offsetX": offset["x"], "offsetY": offset["y"], "offsetZ": offset["z"] }; var data = { "followUnit": command } POST(data, () => { }); } @@ -162,8 +161,8 @@ export function cloneUnit(ID: number, latlng: L.LatLng) { POST(data, () => { }); } -export function deleteUnit(ID: number) { - var command = { "ID": ID}; +export function deleteUnit(ID: number, explosion: boolean) { + var command = { "ID": ID, "explosion": explosion }; var data = { "deleteUnit": command } POST(data, () => { }); } @@ -175,50 +174,74 @@ export function landAt(ID: number, latlng: L.LatLng) { } export function changeSpeed(ID: number, speedChange: string) { - var command = {"ID": ID, "change": speedChange} - var data = {"changeSpeed": command} + var command = { "ID": ID, "change": speedChange } + var data = { "changeSpeed": command } POST(data, () => { }); } export function setSpeed(ID: number, speed: number) { - var command = {"ID": ID, "speed": speed} - var data = {"setSpeed": command} + var command = { "ID": ID, "speed": speed } + var data = { "setSpeed": command } + POST(data, () => { }); +} + +export function setSpeedType(ID: number, speedType: string) { + var command = { "ID": ID, "speedType": speedType } + var data = { "setSpeedType": command } POST(data, () => { }); } export function changeAltitude(ID: number, altitudeChange: string) { - var command = {"ID": ID, "change": altitudeChange} - var data = {"changeAltitude": command} + var command = { "ID": ID, "change": altitudeChange } + var data = { "changeAltitude": command } + POST(data, () => { }); +} + +export function setAltitudeType(ID: number, altitudeType: string) { + var command = { "ID": ID, "altitudeType": altitudeType } + var data = { "setAltitudeType": command } POST(data, () => { }); } export function setAltitude(ID: number, altitude: number) { - var command = {"ID": ID, "altitude": altitude} - var data = {"setAltitude": command} + var command = { "ID": ID, "altitude": altitude } + var data = { "setAltitude": command } POST(data, () => { }); } export function createFormation(ID: number, isLeader: boolean, wingmenIDs: number[]) { - var command = {"ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader} - var data = {"setLeader": command} + var command = { "ID": ID, "wingmenIDs": wingmenIDs, "isLeader": isLeader } + var data = { "setLeader": command } POST(data, () => { }); } export function setROE(ID: number, ROE: string) { - var command = {"ID": ID, "ROE": ROE} - var data = {"setROE": command} + var command = { "ID": ID, "ROE": ROE } + var data = { "setROE": command } POST(data, () => { }); } export function setReactionToThreat(ID: number, reactionToThreat: string) { - var command = {"ID": ID, "reactionToThreat": reactionToThreat} - var data = {"setReactionToThreat": command} + var command = { "ID": ID, "reactionToThreat": reactionToThreat } + var data = { "setReactionToThreat": command } POST(data, () => { }); } export function setEmissionsCountermeasures(ID: number, emissionCountermeasure: string) { - var command = {"ID": ID, "emissionsCountermeasures": emissionCountermeasure} - var data = {"setEmissionsCountermeasures": command} + var command = { "ID": ID, "emissionsCountermeasures": emissionCountermeasure } + var data = { "setEmissionsCountermeasures": command } + POST(data, () => { }); +} + +export function setOnOff(ID: number, onOff: boolean) { + var command = { "ID": ID, "onOff": onOff } + var data = { "setOnOff": command } + POST(data, () => { }); +} + +export function setFollowRoads(ID: number, followRoads: boolean) { + var command = { "ID": ID, "followRoads": followRoads } + var data = { "setFollowRoads": command } POST(data, () => { }); } @@ -228,14 +251,14 @@ export function refuel(ID: number) { POST(data, () => { }); } -export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) -{ - var command = { "ID": ID, - "isTanker": isTanker, - "isAWACS": isAWACS, - "TACAN": TACAN, - "radio": radio, - "generalSettings": generalSettings +export function setAdvacedOptions(ID: number, isTanker: boolean, isAWACS: boolean, TACAN: TACAN, radio: Radio, generalSettings: GeneralSettings) { + var command = { + "ID": ID, + "isTanker": isTanker, + "isAWACS": isAWACS, + "TACAN": TACAN, + "radio": radio, + "generalSettings": generalSettings }; var data = { "setAdvancedOptions": command }; diff --git a/client/src/units/unit.ts b/client/src/units/unit.ts index eb4ef516..b9ce860c 100644 --- a/client/src/units/unit.ts +++ b/client/src/units/unit.ts @@ -1,7 +1,7 @@ import { Marker, LatLng, Polyline, Icon, DivIcon, CircleMarker, Map } from 'leaflet'; import { getMap, getUnitsManager } from '..'; import { rad2deg } from '../other/utils'; -import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures } from '../server/server'; +import { addDestination, attackUnit, changeAltitude, changeSpeed, createFormation as setLeader, deleteUnit, getUnits, landAt, setAltitude, setReactionToThreat, setROE, setSpeed, refuel, setAdvacedOptions, followUnit, setEmissionsCountermeasures, setSpeedType, setAltitudeType, setOnOff, setFollowRoads } from '../server/server'; import { aircraftDatabase } from './aircraftdatabase'; import { groundUnitsDatabase } from './groundunitsdatabase'; import { CustomMarker } from '../map/custommarker'; @@ -49,9 +49,13 @@ export class Unit extends CustomMarker { currentTask: "", activePath: {}, targetSpeed: 0, + targetSpeedType: "GS", targetAltitude: 0, + targetAltitudeType: "AGL", isTanker: false, isAWACS: false, + onOff: true, + followRoads: false }, optionsData: { ROE: "", @@ -116,8 +120,6 @@ export class Unit extends CustomMarker { /* Set the unit data */ this.setData(data); - - } getMarkerCategory() { @@ -203,48 +205,19 @@ export class Unit extends CustomMarker { const aliveChanged = (data.baseData != undefined && data.baseData.alive != undefined && this.getBaseData().alive != data.baseData.alive); var updateMarker = (positionChanged || headingChanged || aliveChanged || !getMap().hasLayer(this)); - if (data.baseData != undefined) { - for (let key in this.#data.baseData) - if (key in data.baseData) - //@ts-ignore - this.#data.baseData[key] = data.baseData[key]; - } - - if (data.flightData != undefined) { - for (let key in this.#data.flightData) - if (key in data.flightData) - //@ts-ignore - this.#data.flightData[key] = data.flightData[key]; - } - - if (data.missionData != undefined) { - for (let key in this.#data.missionData) - if (key in data.missionData) - //@ts-ignore - this.#data.missionData[key] = data.missionData[key]; - } - - if (data.formationData != undefined) { - for (let key in this.#data.formationData) - if (key in data.formationData) - //@ts-ignore - this.#data.formationData[key] = data.formationData[key]; - } - - if (data.taskData != undefined) { - for (let key in this.#data.taskData) - if (key in data.taskData) - //@ts-ignore - this.#data.taskData[key] = data.taskData[key]; - } - - if (data.optionsData != undefined) { - for (let key in this.#data.optionsData) - if (key in data.optionsData) - //@ts-ignore - this.#data.optionsData[key] = data.optionsData[key]; - } - + /* Load the data from the received json */ + Object.keys(this.#data).forEach((key1: string) => { + Object.keys(this.#data[key1 as keyof(UnitData)]).forEach((key2: string) => { + if (key1 in data && key2 in data[key1]) { + var value1 = this.#data[key1 as keyof(UnitData)]; + var value2 = value1[key2 as keyof typeof value1]; + if (typeof data[key1][key2] === typeof value2) + //@ts-ignore + this.#data[key1 as keyof(UnitData)][key2 as keyof typeof struct] = data[key1][key2]; + } + }); + }); + /* Fire an event when a unit dies */ if (aliveChanged && this.getBaseData().alive == false) document.dispatchEvent(new CustomEvent("unitDeath", { detail: this })); @@ -485,11 +458,21 @@ export class Unit extends CustomMarker { setSpeed(this.ID, speed); } + setSpeedType(speedType: string) { + if (!this.getMissionData().flags.Human) + setSpeedType(this.ID, speedType); + } + setAltitude(altitude: number) { if (!this.getMissionData().flags.Human) setAltitude(this.ID, altitude); } + setAltitudeType(altitudeType: string) { + if (!this.getMissionData().flags.Human) + setAltitudeType(this.ID, altitudeType); + } + setROE(ROE: string) { if (!this.getMissionData().flags.Human) setROE(this.ID, ROE); @@ -510,9 +493,19 @@ export class Unit extends CustomMarker { setLeader(this.ID, isLeader, wingmenIDs); } - delete() { + setOnOff(onOff: boolean) { + if (!this.getMissionData().flags.Human) + setOnOff(this.ID, onOff); + } + + setFollowRoads(followRoads: boolean) { + if (!this.getMissionData().flags.Human) + setFollowRoads(this.ID, followRoads); + } + + delete(explosion: boolean) { // TODO: add confirmation popup - deleteUnit(this.ID); + deleteUnit(this.ID, explosion); } refuel() { diff --git a/client/src/units/unitsmanager.ts b/client/src/units/unitsmanager.ts index adbe4b71..ba3004a5 100644 --- a/client/src/units/unitsmanager.ts +++ b/client/src/units/unitsmanager.ts @@ -21,7 +21,8 @@ export class UnitsManager { document.addEventListener('unitSelection', (e: CustomEvent) => this.#onUnitSelection(e.detail)); document.addEventListener('unitDeselection', (e: CustomEvent) => this.#onUnitDeselection(e.detail)); document.addEventListener('keydown', (event) => this.#onKeyDown(event)); - document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()) + document.addEventListener('deleteSelectedUnits', () => this.selectedUnitsDelete()); + document.addEventListener('explodeSelectedUnits', () => this.selectedUnitsDelete(true)); } getSelectableAircraft() { @@ -155,25 +156,16 @@ export class UnitsManager { }); }; - getSelectedUnitsTargetSpeed() { + getSelectedUnitsVariable(variableGetter: CallableFunction) { if (this.getSelectedUnits().length == 0) return undefined; return this.getSelectedUnits().map((unit: Unit) => { - return unit.getTaskData().targetSpeed + return variableGetter(unit); })?.reduce((a: any, b: any) => { return a == b ? a : undefined }); }; - getSelectedUnitsTargetAltitude() { - if (this.getSelectedUnits().length == 0) - return undefined; - return this.getSelectedUnits().map((unit: Unit) => { - return unit.getTaskData().targetAltitude - })?.reduce((a: any, b: any) => { - return a == b ? a : undefined - }); - }; getSelectedUnitsCoalition() { if (this.getSelectedUnits().length == 0) @@ -261,6 +253,14 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `setting speed to ${speed * 1.94384} kts`); } + selectedUnitsSetSpeedType(speedType: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setSpeedType(speedType); + } + this.#showActionMessage(selectedUnits, `setting speed type to ${speedType}`); + } + selectedUnitsSetAltitude(altitude: number) { var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); for (let idx in selectedUnits) { @@ -269,6 +269,14 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `setting altitude to ${altitude / 0.3048} ft`); } + selectedUnitsSetAltitudeType(altitudeType: string) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setAltitudeType(altitudeType); + } + this.#showActionMessage(selectedUnits, `setting altitude type to ${altitudeType}`); + } + selectedUnitsSetROE(ROE: string) { var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); for (let idx in selectedUnits) { @@ -290,7 +298,23 @@ export class UnitsManager { for (let idx in selectedUnits) { selectedUnits[idx].setEmissionsCountermeasures(emissionCountermeasure); } - this.#showActionMessage(selectedUnits, `reaction to threat set to ${emissionCountermeasure}`); + this.#showActionMessage(selectedUnits, `emissions & countermeasures set to ${emissionCountermeasure}`); + } + + selectedUnitsSetOnOff(onOff: boolean) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setOnOff(onOff); + } + this.#showActionMessage(selectedUnits, `unit acitve set to ${onOff}`); + } + + selectedUnitsSetFollowRoads(followRoads: boolean) { + var selectedUnits = this.getSelectedUnits({ excludeHumans: true }); + for (let idx in selectedUnits) { + selectedUnits[idx].setFollowRoads(followRoads); + } + this.#showActionMessage(selectedUnits, `follow roads set to ${followRoads}`); } @@ -302,10 +326,10 @@ export class UnitsManager { this.#showActionMessage(selectedUnits, `attacking unit ${this.getUnitByID(ID)?.getBaseData().unitName}`); } - selectedUnitsDelete() { + selectedUnitsDelete(explosion: boolean = false) { var selectedUnits = this.getSelectedUnits(); /* Can be applied to humans too */ for (let idx in selectedUnits) { - selectedUnits[idx].delete(); + selectedUnits[idx].delete(explosion); } this.#showActionMessage(selectedUnits, `deleted`); } diff --git a/client/views/other/contextmenus.ejs b/client/views/other/contextmenus.ejs index 4a535b9d..422177f4 100644 --- a/client/views/other/contextmenus.ejs +++ b/client/views/other/contextmenus.ejs @@ -1,9 +1,7 @@
- +
+
+
+
Spawn altitude +
+
+
+
+
+ + +
diff --git a/client/views/panels/unitcontrol.ejs b/client/views/panels/unitcontrol.ejs index c004bd59..22fffd52 100644 --- a/client/views/panels/unitcontrol.ejs +++ b/client/views/panels/unitcontrol.ejs @@ -14,12 +14,12 @@

Controls

-
+
Speed
-
+
@@ -61,12 +61,17 @@
-
+

Unit active

Toggling this disables unit AI completely. It will no longer move, react or emit radio waves.
+
+

Follow roads

+
+
+
diff --git a/client/views/panels/unitinfo.ejs b/client/views/panels/unitinfo.ejs index 603f4988..3c03ca9c 100644 --- a/client/views/panels/unitinfo.ejs +++ b/client/views/panels/unitinfo.ejs @@ -14,7 +14,7 @@
-
+
diff --git a/olympus.json b/olympus.json index 3618c236..45d703ff 100644 --- a/olympus.json +++ b/olympus.json @@ -1,6 +1,6 @@ { "server": { - "address": "localhost", + "address": "136.243.170.132", "port": 30000 }, "authentication": {