From 14679bd7d865c6fddc34a29528558f7fdb4c9c69 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Thu, 22 Feb 2024 08:01:13 +0100 Subject: [PATCH 01/10] Added code for map creation and camera control --- .gitignore | 1 + frontend/website/src/constants/constants.ts | 6 ++ frontend/website/src/map/map.ts | 26 ++++++- .../python/generateMaps/.vscode/launch.json | 16 ++++ scripts/python/generateMaps/generate_tiles.py | 41 ++++++++++ .../python/generateMaps/take_screenshots.py | 75 +++++++++++++++++++ 6 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 scripts/python/generateMaps/.vscode/launch.json create mode 100644 scripts/python/generateMaps/generate_tiles.py create mode 100644 scripts/python/generateMaps/take_screenshots.py diff --git a/.gitignore b/.gitignore index 0ce64a31..32f89517 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ package-lock.json /frontend/setup frontend/server/public/plugins/controltipsplugin/index.js frontend/website/plugins/controltips/index.js +/frontend/server/public/maps diff --git a/frontend/website/src/constants/constants.ts b/frontend/website/src/constants/constants.ts index 7ada419b..e544d31f 100644 --- a/frontend/website/src/constants/constants.ts +++ b/frontend/website/src/constants/constants.ts @@ -190,6 +190,12 @@ export const mapLayers = { minZoom: 1, maxZoom: 20, attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' + }, + "DCS": { + urlTemplate: 'http://localhost:3000/maps/dcs/{z}/{x}/{y}.png', + minZoom: 16, + maxZoom: 16, + attribution: 'Eagle Dynamics' } } diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index f2af5ce7..a10afd7d 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -7,7 +7,7 @@ import { AirbaseContextMenu } from "../contextmenus/airbasecontextmenu"; import { Dropdown } from "../controls/dropdown"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { bearing, createCheckboxOption, polyContains } from "../other/utils"; +import { bearing, createCheckboxOption, deg2rad, getGroundElevation, polyContains } from "../other/utils"; import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; @@ -157,6 +157,7 @@ export class Map extends L.Map { this.on('drag', (e: any) => this.#onMouseMove(e)); this.on('keydown', (e: any) => this.#onKeyDown(e)); this.on('keyup', (e: any) => this.#onKeyUp(e)); + this.on('move', (e: any) => this.#broadcastPosition(e)); /* Event listeners */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { @@ -704,6 +705,29 @@ export class Map extends L.Map { this.#isZooming = false; } + #broadcastPosition(e: any) { + getGroundElevation(this.getCenter(), (response: string) => { + var groundElevation: number | null = null; + try { + groundElevation = parseFloat(response); + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("PUT", "http://localhost:8080"); + xmlHttp.setRequestHeader("Content-Type", "application/json"); + + const C = 40075016.686; + let mpp = C * Math.cos(deg2rad(this.getCenter().lat)) / Math.pow(2, this.getZoom() + 8); + let d = mpp * 1920; + let alt = d / 2 * 1 / Math.tan(deg2rad(40)); + if (alt > 100000) + alt = 100000; + xmlHttp.send(JSON.stringify({lat: this.getCenter().lat, lng: this.getCenter().lng, alt: alt + groundElevation})); + } catch { + console.warn("broadcastPosition: could not retrieve ground elevation") + } + }); + + } + /* */ #panToUnit(unit: Unit) { var unitPosition = new L.LatLng(unit.getPosition().lat, unit.getPosition().lng); diff --git a/scripts/python/generateMaps/.vscode/launch.json b/scripts/python/generateMaps/.vscode/launch.json new file mode 100644 index 00000000..306f58eb --- /dev/null +++ b/scripts/python/generateMaps/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true + } + ] +} \ No newline at end of file diff --git a/scripts/python/generateMaps/generate_tiles.py b/scripts/python/generateMaps/generate_tiles.py new file mode 100644 index 00000000..a6663147 --- /dev/null +++ b/scripts/python/generateMaps/generate_tiles.py @@ -0,0 +1,41 @@ +import os +from PIL import Image +import concurrent.futures + +zoom = 16 +path = "output" + +def crop_image(filename): + img = Image.open(os.path.join(path, filename)) + center_X, center_Y = filename.removesuffix(".png").split("_") + center_X = int(center_X) + center_Y = int(center_Y) + w, h = img.size + + box_top_left = (w / 2 - 256, h / 2 - 256, w / 2, h / 2) + box_top_right = (w / 2, h / 2 - 256, w / 2 + 256, h / 2) + box_bottom_left = (w / 2 - 256, h / 2, w / 2, h / 2 + 256) + box_bottom_right = (w / 2, h / 2, w / 2 + 256, h / 2 + 256) + + if not os.path.exists(f"output/{zoom}/{center_X - 1}"): + os.mkdir(f"output/{zoom}/{center_X - 1}") + + if not os.path.exists(f"output/{zoom}/{center_X}"): + os.mkdir(f"output/{zoom}/{center_X}") + + img.crop(box_top_left).save(f"output/{zoom}/{center_X - 1}/{center_Y - 1}.png") + img.crop(box_top_right).save(f"output/{zoom}/{center_X}/{center_Y - 1}.png") + img.crop(box_bottom_left).save(f"output/{zoom}/{center_X - 1}/{center_Y}.png") + img.crop(box_bottom_right).save(f"output/{zoom}/{center_X}/{center_Y}.png") + + return True + +filenames = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] + +# Create output folder +if not os.path.exists(f"output/{zoom}"): + os.mkdir(f"output/{zoom}") + +with concurrent.futures.ThreadPoolExecutor() as executor: + futures = [executor.submit(crop_image, filename) for filename in filenames] + results = [future.result() for future in concurrent.futures.as_completed(futures)] \ No newline at end of file diff --git a/scripts/python/generateMaps/take_screenshots.py b/scripts/python/generateMaps/take_screenshots.py new file mode 100644 index 00000000..160f7e4b --- /dev/null +++ b/scripts/python/generateMaps/take_screenshots.py @@ -0,0 +1,75 @@ +import math +import requests +import json +import pyautogui +import time +import os + +# parameters +start_lat = 36.31669444 # degs +start_lng = -115.38336111 # degs + +end_lat = 35.93336111 # degs +end_lng = -114.95002778 # degs + +fov = 10 # deg +zoom = 16 + +# constants +C = 40075016.686 # meters + +def deg_to_num(lat_deg, lon_deg, zoom): + lat_rad = math.radians(lat_deg) + n = 1 << zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + return xtile, ytile + +def num_to_deg(xtile, ytile, zoom): + n = 1 << zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return lat_deg, lon_deg + +def camera_altitude(lat_deg): + mpp = C * math.cos(math.radians(lat_deg)) / math.pow(2, zoom + 8) + d = mpp * 1920 + alt = d / 2 * 1 / math.tan(math.radians(fov) / 2) + return alt + +# Find the starting and ending points +start_X, start_Y = deg_to_num(start_lat, start_lng, zoom) +end_X, end_Y = deg_to_num(end_lat, end_lng, zoom) + +time.sleep(2) + +# Create output folder +if not os.path.exists("output"): + os.mkdir("output") + +# Start looping +n = 1 +total = math.floor((end_X - start_X) / 2) * math.floor((end_Y - start_Y) / 2) +for X in range(start_X, end_X, 2): + for Y in range(start_Y, end_Y, 2): + # Find the center of the screen + center_lat, center_lng = num_to_deg(X + 1, Y + 1, zoom) + center_alt = camera_altitude(center_lat) + + # Making PUT request + data = json.dumps({'lat': center_lat, 'lng': center_lng, 'alt': center_alt}) + r = requests.put('http://localhost:8080', data = data) + + # Take and save screenshot + screenshot = pyautogui.screenshot() + screenshot.save(f"output/{X + 1}_{Y + 1}_{zoom}.png") + + time.sleep(0.5) + + print(f"Shot {n} of {total}") + n = n + 1 + + + + From acb55044d19d9a185b65e924a768c29683044490 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 22 Feb 2024 17:46:34 +0100 Subject: [PATCH 02/10] More work on automatic map creation --- .gitignore | 1 + frontend/server/package.json | 3 +- frontend/server/routes/resources.js | 125 ++++++++++++++++++ frontend/website/src/constants/constants.ts | 4 +- .../python/generateMaps/.vscode/launch.json | 2 +- scripts/python/generateMaps/capture_screen.py | 81 ++++++++++++ .../configs/LasVegas/LasVegas.kml | 84 ++++++++++++ .../configs/LasVegas/LasVegas.yml | 4 + .../configs/screen_properties.yml | 10 ++ scripts/python/generateMaps/generate_tiles.py | 10 +- scripts/python/generateMaps/main.py | 66 +++++++++ scripts/python/generateMaps/requirements.txt | 23 ++++ .../python/generateMaps/take_screenshots.py | 75 ----------- 13 files changed, 408 insertions(+), 80 deletions(-) create mode 100644 scripts/python/generateMaps/capture_screen.py create mode 100644 scripts/python/generateMaps/configs/LasVegas/LasVegas.kml create mode 100644 scripts/python/generateMaps/configs/LasVegas/LasVegas.yml create mode 100644 scripts/python/generateMaps/configs/screen_properties.yml create mode 100644 scripts/python/generateMaps/main.py create mode 100644 scripts/python/generateMaps/requirements.txt delete mode 100644 scripts/python/generateMaps/take_screenshots.py diff --git a/.gitignore b/.gitignore index 32f89517..088a483d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ package-lock.json frontend/server/public/plugins/controltipsplugin/index.js frontend/website/plugins/controltips/index.js /frontend/server/public/maps +*.pyc diff --git a/frontend/server/package.json b/frontend/server/package.json index 03a5346d..d612c932 100644 --- a/frontend/server/package.json +++ b/frontend/server/package.json @@ -23,9 +23,10 @@ "regedit": "^5.1.2", "save": "^2.9.0", "sha256": "^0.2.0", + "sharp": "^0.33.2", "srtm-elevation": "^2.1.2", "tcp-ping-port": "^1.0.1", "uuid": "^9.0.1", "yargs": "^17.7.2" } -} \ No newline at end of file +} diff --git a/frontend/server/routes/resources.js b/frontend/server/routes/resources.js index f3fd6198..ebb34ad7 100644 --- a/frontend/server/routes/resources.js +++ b/frontend/server/routes/resources.js @@ -1,4 +1,7 @@ const express = require('express'); +const sharp = require('sharp') +const fs = require('fs'); +const pfs = require('fs/promises') const router = express.Router(); router.get('/theme/*', function (req, res, next) { @@ -15,4 +18,126 @@ router.put('/theme/:newTheme', function (req, res, next) { res.end("Ok"); }); +router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) { + let map = req.params.map; + let x = req.params.x; + let y = req.params.y; + let z = req.params.z; + + if (fs.existsSync(`.\\public\\maps\\${map}`)) { + if (!await renderImage(map, z, x, y, res)) { + /* No image was found */ + res.sendStatus(404); + } + } else { + /* The requested map does not exist */ + res.sendStatus(404); + } +}); + +async function renderImage(map, z, x, y, res, recursionDepth = 0) { + if (recursionDepth == 20) { + console.log("Render image, maximum recursion depth reached") + /* We have reached limit recusion depth, something went wrong */ + return false; + } + /* If the requested image exists, send it straight away */ + if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { + res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + return true; + } else { + /* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */ + let sourceZoom = z - 1; + let sourceX = Math.floor(x / 2); + let sourceY = Math.floor(y / 2); + + /* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */ + while (sourceZoom >= 0) { + /* We have found a possible source image */ + if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) { + /* Split the image into four. We can retry up to 10 times to clear any race condition on the files */ + let retries = 10; + while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) { + await new Promise(r => setTimeout(r, 1000)); + retries--; + } + /* Check if we exited because we reached the maximum retry value */ + if (retries != 0) { + /* Recursively recall the function now that we have a new "source" image */ + return await renderImage(map, z, x, y, res, recursionDepth + 1); + } else { + return false; + } + } else { + /* Keep searching at a higher level */ + sourceZoom = sourceZoom - 1; + sourceX = Math.floor(sourceX / 2); + sourceY = Math.floor(sourceY / 2); + } + } + return false; + } +} + +async function splitTile(map, z, x, y) { + try { + /* Load the source image */ + let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + + /* Create the necessary folders */ + await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true }) + await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true }) + + /* Split the image into four parts */ + await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y); + img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y); + img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1); + img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1); + return true; + } catch (err) { + if (err.code !== 'EBUSY') { + console.error(err); + } + return false; + } +} + +/* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */ +function resizePromise(img, left, top, map, z, x, y) { + return new Promise((res, rej) => { + if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { + res(true); + } + img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) { + if (err) { + rej(err); + } else { + try { + fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + } catch (err) { + if (err.code === 'EBUSY') { + /* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */ + rej(err); + } else if (err.code === 'ENOENT') { + /* Someone else renamed the file already so this is not a real error */ + if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { + res(true); + } + /* Something odd happened, reject and try again */ + else { + rej(err); + } + } else { + rej(err); + } + } + res(true) + } + }); + }) +} + module.exports = router; diff --git a/frontend/website/src/constants/constants.ts b/frontend/website/src/constants/constants.ts index e544d31f..52e185cd 100644 --- a/frontend/website/src/constants/constants.ts +++ b/frontend/website/src/constants/constants.ts @@ -192,9 +192,9 @@ export const mapLayers = { attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' }, "DCS": { - urlTemplate: 'http://localhost:3000/maps/dcs/{z}/{x}/{y}.png', + urlTemplate: 'http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png', minZoom: 16, - maxZoom: 16, + maxZoom: 20, attribution: 'Eagle Dynamics' } } diff --git a/scripts/python/generateMaps/.vscode/launch.json b/scripts/python/generateMaps/.vscode/launch.json index 306f58eb..01d13453 100644 --- a/scripts/python/generateMaps/.vscode/launch.json +++ b/scripts/python/generateMaps/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "program": "${file}", "console": "integratedTerminal", - "justMyCode": true + "args": ["./configs/LasVegas/LasVegas.yml"] } ] } \ No newline at end of file diff --git a/scripts/python/generateMaps/capture_screen.py b/scripts/python/generateMaps/capture_screen.py new file mode 100644 index 00000000..6cc4ef1f --- /dev/null +++ b/scripts/python/generateMaps/capture_screen.py @@ -0,0 +1,81 @@ +import math +import requests +import json +import pyautogui +import time +import os + +from pyproj import Geod +from fastkml import kml +from shapely import wkt + +def deg_to_num(lat_deg, lon_deg, zoom): + lat_rad = math.radians(lat_deg) + n = 1 << zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + return xtile, ytile + +def num_to_deg(xtile, ytile, zoom): + n = 1 << zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return lat_deg, lon_deg + +def run(map_config): + with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp: + # Read the config file + doc = bp.read() + k = kml.KML() + k.from_string(doc) + + geod = Geod(ellps="WGS84") + features = [f for f in list(k.features()) if not f.isopen] + print(f"Found {len(features)} closed features in the provided kml file") + + area = 0 + for feature in features: + for sub_feature in list(feature.features()): + geo = sub_feature.geometry + + start_lat = geo.bounds[1] + start_lng = geo.bounds[0] + end_lat = geo.bounds[3] + end_lng = geo.bound[2] + + # Find the starting and ending points + start_X, start_Y = deg_to_num(start_lat, start_lng, zoom) + end_X, end_Y = deg_to_num(end_lat, end_lng, zoom) + + time.sleep(2) + + # Create output folder + if not os.path.exists("output"): + os.mkdir("output") + + # Start looping + n = 1 + total = math.floor((end_X - start_X) / 2) * math.floor((end_Y - start_Y) / 2) + for X in range(start_X, end_X, 2): + for Y in range(start_Y, end_Y, 2): + # Find the center of the screen + center_lat, center_lng = num_to_deg(X + 1, Y + 1, zoom) + center_alt = camera_altitude(center_lat) + + # Making PUT request + data = json.dumps({'lat': center_lat, 'lng': center_lng, 'alt': center_alt}) + r = requests.put('http://localhost:8080', data = data) + + # Take and save screenshot + screenshot = pyautogui.screenshot() + screenshot.save(f"output/{X + 1}_{Y + 1}_{zoom}.png") + + time.sleep(0.5) + + print(f"Shot {n} of {total}") + n = n + 1 + + + + diff --git a/scripts/python/generateMaps/configs/LasVegas/LasVegas.kml b/scripts/python/generateMaps/configs/LasVegas/LasVegas.kml new file mode 100644 index 00000000..a401dd8c --- /dev/null +++ b/scripts/python/generateMaps/configs/LasVegas/LasVegas.kml @@ -0,0 +1,84 @@ + + + + Senza titolo + + + + + + + + + normal + #__managed_style_1E6AF60F852F06CED14C + + + highlight + #__managed_style_29D7120C702F06CED14C + + + + Poligono senza titolo + + -115.7575513617584 + 36.45909683572987 + 1668.83938821393 + 0 + 0 + 35 + 382627.9679017514 + absolute + + #__managed_style_05AC9C65832F06CED14C + + + + + -115.5765603362792,35.92113103850846,0 -114.7667743850415,35.914903046417,0 -114.7800419428627,36.39467581209903,0 -115.6198023719551,36.39886214564519,0 -115.5765603362792,35.92113103850846,0 + + + + + + + diff --git a/scripts/python/generateMaps/configs/LasVegas/LasVegas.yml b/scripts/python/generateMaps/configs/LasVegas/LasVegas.yml new file mode 100644 index 00000000..b4f8eb93 --- /dev/null +++ b/scripts/python/generateMaps/configs/LasVegas/LasVegas.yml @@ -0,0 +1,4 @@ +{ + 'output_directory': './LasVegas', # Where to save the output files + 'boundary_file': './configs/LasVegas/LasVegas.kml' +} \ No newline at end of file diff --git a/scripts/python/generateMaps/configs/screen_properties.yml b/scripts/python/generateMaps/configs/screen_properties.yml new file mode 100644 index 00000000..6e7cd752 --- /dev/null +++ b/scripts/python/generateMaps/configs/screen_properties.yml @@ -0,0 +1,10 @@ +{ + 'width': 1920, # The width of your screen, in pixels + 'height': 1080, # The height of your screen, in pixels + 'geo_resolution': 1.0 # The resolution of the map on the screen, in meters per pixel. + # To measure this value, first set the F10 map at the desired zoom level. + # Then, use F10's map measure tool, and measure the width of the screen in meters. + # Finally, divide that value by the width in pixels. + # A good value would be around 1 meter per pixel, meaning a 1920px wide map would measure about 1 nautical mile across on the F10 map + # Lower values will produce higher resolution maps, but beware of space usage! +} \ No newline at end of file diff --git a/scripts/python/generateMaps/generate_tiles.py b/scripts/python/generateMaps/generate_tiles.py index a6663147..05302ec9 100644 --- a/scripts/python/generateMaps/generate_tiles.py +++ b/scripts/python/generateMaps/generate_tiles.py @@ -1,12 +1,20 @@ import os from PIL import Image import concurrent.futures +import math + +# correction parameters + +# NTTR +rotation = math.degrees(0.01895) +scale = 0.973384 zoom = 16 path = "output" def crop_image(filename): - img = Image.open(os.path.join(path, filename)) + img = Image.open(os.path.join(path, filename)).rotate(-rotation) + img = img.resize((math.floor(img.width * scale), math.floor(img.height * scale))) center_X, center_Y = filename.removesuffix(".png").split("_") center_X = int(center_X) center_Y = int(center_Y) diff --git a/scripts/python/generateMaps/main.py b/scripts/python/generateMaps/main.py new file mode 100644 index 00000000..d2f19ae7 --- /dev/null +++ b/scripts/python/generateMaps/main.py @@ -0,0 +1,66 @@ +import sys +import yaml +from pyproj import Geod +from fastkml import kml +from shapely import wkt +from datetime import timedelta + +import capture_screen + +if len(sys.argv) == 1: + print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.") +else: + config_file = sys.argv[1] + print(f"Using config file: {config_file}") + + with open('configs/screen_properties.yml', 'r') as sp: + with open(config_file, 'r') as cp: + screen_config = yaml.safe_load(sp) + map_config = yaml.safe_load(cp) + + print("#################################################################################################################################################") + print("# IMPORTANT NOTE: the screen properties must be configured according to your screen and desired zoom level. Make sure you set them accordingly. #") + print("#################################################################################################################################################") + + print("Screen parameters:") + print(f"-> Screen width: {screen_config["width"]}px") + print(f"-> Screen height: {screen_config["height"]}px") + print(f"-> Geographic resolution: {screen_config["geo_resolution"]} meters/pixel") + + print("Map parameters:") + print(f"-> Output directory: {map_config["output_directory"]}") + print(f"-> Boundary file: {map_config["boundary_file"]}") + + with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp: + # Read the config file and compute the total area of the covered map + doc = bp.read() + k = kml.KML() + k.from_string(doc) + + geod = Geod(ellps="WGS84") + features = [f for f in list(k.features()) if not f.isopen] + print(f"Found {len(features)} closed features in the provided kml file") + + area = 0 + for feature in features: + for sub_feature in list(feature.features()): + geo = sub_feature.geometry + area += abs(geod.geometry_area_perimeter(wkt.loads(geo.wkt))[0]) + + tile_size = 256 * screen_config["geo_resolution"] # meters + tiles_per_screenshot = int(screen_config["width"] / 256) * int(screen_config["height"] / 256) + tiles_num = int(area / (tile_size * tile_size)) + screenshots_num = int(tiles_num / tiles_per_screenshot) + total_time = int(screenshots_num / 1.0) + + print(f"Total area: {int(area / 1e6)} square kilometers") + print(f"Estimated number of tiles: {tiles_num}") + print(f"Estimated number of screenshots: {screenshots_num}") + print(f"Estimated time to complete: {timedelta(seconds=total_time)} (hh:mm:ss)") + print("The script is ready to go. After you press any key, it will wait for 5 seconds, and then it will start.") + + input("Press any key to continue...") + capture_screen.run(map_config) + + + diff --git a/scripts/python/generateMaps/requirements.txt b/scripts/python/generateMaps/requirements.txt new file mode 100644 index 00000000..43e52dfd --- /dev/null +++ b/scripts/python/generateMaps/requirements.txt @@ -0,0 +1,23 @@ +certifi==2024.2.2 +charset-normalizer==3.3.2 +fastkml==0.12 +idna==3.6 +MouseInfo==0.1.3 +numpy==1.26.4 +pillow==10.2.0 +PyAutoGUI==0.9.54 +pygeoif==0.7 +PyGetWindow==0.0.9 +PyMsgBox==1.0.9 +pyperclip==1.8.2 +pyproj==3.6.1 +PyRect==0.2.0 +PyScreeze==0.1.30 +python-dateutil==2.8.2 +pytweening==1.2.0 +PyYAML==6.0.1 +requests==2.31.0 +setuptools==69.1.0 +shapely==2.0.3 +six==1.16.0 +urllib3==2.2.1 diff --git a/scripts/python/generateMaps/take_screenshots.py b/scripts/python/generateMaps/take_screenshots.py deleted file mode 100644 index 160f7e4b..00000000 --- a/scripts/python/generateMaps/take_screenshots.py +++ /dev/null @@ -1,75 +0,0 @@ -import math -import requests -import json -import pyautogui -import time -import os - -# parameters -start_lat = 36.31669444 # degs -start_lng = -115.38336111 # degs - -end_lat = 35.93336111 # degs -end_lng = -114.95002778 # degs - -fov = 10 # deg -zoom = 16 - -# constants -C = 40075016.686 # meters - -def deg_to_num(lat_deg, lon_deg, zoom): - lat_rad = math.radians(lat_deg) - n = 1 << zoom - xtile = int((lon_deg + 180.0) / 360.0 * n) - ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) - return xtile, ytile - -def num_to_deg(xtile, ytile, zoom): - n = 1 << zoom - lon_deg = xtile / n * 360.0 - 180.0 - lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) - lat_deg = math.degrees(lat_rad) - return lat_deg, lon_deg - -def camera_altitude(lat_deg): - mpp = C * math.cos(math.radians(lat_deg)) / math.pow(2, zoom + 8) - d = mpp * 1920 - alt = d / 2 * 1 / math.tan(math.radians(fov) / 2) - return alt - -# Find the starting and ending points -start_X, start_Y = deg_to_num(start_lat, start_lng, zoom) -end_X, end_Y = deg_to_num(end_lat, end_lng, zoom) - -time.sleep(2) - -# Create output folder -if not os.path.exists("output"): - os.mkdir("output") - -# Start looping -n = 1 -total = math.floor((end_X - start_X) / 2) * math.floor((end_Y - start_Y) / 2) -for X in range(start_X, end_X, 2): - for Y in range(start_Y, end_Y, 2): - # Find the center of the screen - center_lat, center_lng = num_to_deg(X + 1, Y + 1, zoom) - center_alt = camera_altitude(center_lat) - - # Making PUT request - data = json.dumps({'lat': center_lat, 'lng': center_lng, 'alt': center_alt}) - r = requests.put('http://localhost:8080', data = data) - - # Take and save screenshot - screenshot = pyautogui.screenshot() - screenshot.save(f"output/{X + 1}_{Y + 1}_{zoom}.png") - - time.sleep(0.5) - - print(f"Shot {n} of {total}") - n = n + 1 - - - - From 9a571132c8226cdcf47de2feb3cc76d571c07a64 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 23 Feb 2024 15:54:49 +0100 Subject: [PATCH 03/10] Refined automatic map generation script --- .gitignore | 1 + scripts/python/generateMaps/capture_screen.py | 81 ------- .../configs/LasVegas/LasVegas.yml | 4 - scripts/python/generateMaps/generate_tiles.py | 49 ---- .../.vscode/launch.json | 2 +- .../configs/LasVegas/LasVegas.kml | 47 +++- .../configs/LasVegas/LasVegas.yml | 4 + .../configs/screen_properties.yml | 0 .../{generateMaps => map_generator}/main.py | 17 +- scripts/python/map_generator/map_generator.py | 224 ++++++++++++++++++ .../requirements.txt | 0 11 files changed, 274 insertions(+), 155 deletions(-) delete mode 100644 scripts/python/generateMaps/capture_screen.py delete mode 100644 scripts/python/generateMaps/configs/LasVegas/LasVegas.yml delete mode 100644 scripts/python/generateMaps/generate_tiles.py rename scripts/python/{generateMaps => map_generator}/.vscode/launch.json (93%) rename scripts/python/{generateMaps => map_generator}/configs/LasVegas/LasVegas.kml (52%) create mode 100644 scripts/python/map_generator/configs/LasVegas/LasVegas.yml rename scripts/python/{generateMaps => map_generator}/configs/screen_properties.yml (100%) rename scripts/python/{generateMaps => map_generator}/main.py (87%) create mode 100644 scripts/python/map_generator/map_generator.py rename scripts/python/{generateMaps => map_generator}/requirements.txt (100%) diff --git a/.gitignore b/.gitignore index 088a483d..c49b6812 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ frontend/server/public/plugins/controltipsplugin/index.js frontend/website/plugins/controltips/index.js /frontend/server/public/maps *.pyc +/scripts/**/*.jpg diff --git a/scripts/python/generateMaps/capture_screen.py b/scripts/python/generateMaps/capture_screen.py deleted file mode 100644 index 6cc4ef1f..00000000 --- a/scripts/python/generateMaps/capture_screen.py +++ /dev/null @@ -1,81 +0,0 @@ -import math -import requests -import json -import pyautogui -import time -import os - -from pyproj import Geod -from fastkml import kml -from shapely import wkt - -def deg_to_num(lat_deg, lon_deg, zoom): - lat_rad = math.radians(lat_deg) - n = 1 << zoom - xtile = int((lon_deg + 180.0) / 360.0 * n) - ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) - return xtile, ytile - -def num_to_deg(xtile, ytile, zoom): - n = 1 << zoom - lon_deg = xtile / n * 360.0 - 180.0 - lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) - lat_deg = math.degrees(lat_rad) - return lat_deg, lon_deg - -def run(map_config): - with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp: - # Read the config file - doc = bp.read() - k = kml.KML() - k.from_string(doc) - - geod = Geod(ellps="WGS84") - features = [f for f in list(k.features()) if not f.isopen] - print(f"Found {len(features)} closed features in the provided kml file") - - area = 0 - for feature in features: - for sub_feature in list(feature.features()): - geo = sub_feature.geometry - - start_lat = geo.bounds[1] - start_lng = geo.bounds[0] - end_lat = geo.bounds[3] - end_lng = geo.bound[2] - - # Find the starting and ending points - start_X, start_Y = deg_to_num(start_lat, start_lng, zoom) - end_X, end_Y = deg_to_num(end_lat, end_lng, zoom) - - time.sleep(2) - - # Create output folder - if not os.path.exists("output"): - os.mkdir("output") - - # Start looping - n = 1 - total = math.floor((end_X - start_X) / 2) * math.floor((end_Y - start_Y) / 2) - for X in range(start_X, end_X, 2): - for Y in range(start_Y, end_Y, 2): - # Find the center of the screen - center_lat, center_lng = num_to_deg(X + 1, Y + 1, zoom) - center_alt = camera_altitude(center_lat) - - # Making PUT request - data = json.dumps({'lat': center_lat, 'lng': center_lng, 'alt': center_alt}) - r = requests.put('http://localhost:8080', data = data) - - # Take and save screenshot - screenshot = pyautogui.screenshot() - screenshot.save(f"output/{X + 1}_{Y + 1}_{zoom}.png") - - time.sleep(0.5) - - print(f"Shot {n} of {total}") - n = n + 1 - - - - diff --git a/scripts/python/generateMaps/configs/LasVegas/LasVegas.yml b/scripts/python/generateMaps/configs/LasVegas/LasVegas.yml deleted file mode 100644 index b4f8eb93..00000000 --- a/scripts/python/generateMaps/configs/LasVegas/LasVegas.yml +++ /dev/null @@ -1,4 +0,0 @@ -{ - 'output_directory': './LasVegas', # Where to save the output files - 'boundary_file': './configs/LasVegas/LasVegas.kml' -} \ No newline at end of file diff --git a/scripts/python/generateMaps/generate_tiles.py b/scripts/python/generateMaps/generate_tiles.py deleted file mode 100644 index 05302ec9..00000000 --- a/scripts/python/generateMaps/generate_tiles.py +++ /dev/null @@ -1,49 +0,0 @@ -import os -from PIL import Image -import concurrent.futures -import math - -# correction parameters - -# NTTR -rotation = math.degrees(0.01895) -scale = 0.973384 - -zoom = 16 -path = "output" - -def crop_image(filename): - img = Image.open(os.path.join(path, filename)).rotate(-rotation) - img = img.resize((math.floor(img.width * scale), math.floor(img.height * scale))) - center_X, center_Y = filename.removesuffix(".png").split("_") - center_X = int(center_X) - center_Y = int(center_Y) - w, h = img.size - - box_top_left = (w / 2 - 256, h / 2 - 256, w / 2, h / 2) - box_top_right = (w / 2, h / 2 - 256, w / 2 + 256, h / 2) - box_bottom_left = (w / 2 - 256, h / 2, w / 2, h / 2 + 256) - box_bottom_right = (w / 2, h / 2, w / 2 + 256, h / 2 + 256) - - if not os.path.exists(f"output/{zoom}/{center_X - 1}"): - os.mkdir(f"output/{zoom}/{center_X - 1}") - - if not os.path.exists(f"output/{zoom}/{center_X}"): - os.mkdir(f"output/{zoom}/{center_X}") - - img.crop(box_top_left).save(f"output/{zoom}/{center_X - 1}/{center_Y - 1}.png") - img.crop(box_top_right).save(f"output/{zoom}/{center_X}/{center_Y - 1}.png") - img.crop(box_bottom_left).save(f"output/{zoom}/{center_X - 1}/{center_Y}.png") - img.crop(box_bottom_right).save(f"output/{zoom}/{center_X}/{center_Y}.png") - - return True - -filenames = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))] - -# Create output folder -if not os.path.exists(f"output/{zoom}"): - os.mkdir(f"output/{zoom}") - -with concurrent.futures.ThreadPoolExecutor() as executor: - futures = [executor.submit(crop_image, filename) for filename in filenames] - results = [future.result() for future in concurrent.futures.as_completed(futures)] \ No newline at end of file diff --git a/scripts/python/generateMaps/.vscode/launch.json b/scripts/python/map_generator/.vscode/launch.json similarity index 93% rename from scripts/python/generateMaps/.vscode/launch.json rename to scripts/python/map_generator/.vscode/launch.json index 01d13453..c5fdb6ca 100644 --- a/scripts/python/generateMaps/.vscode/launch.json +++ b/scripts/python/map_generator/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Python: Current File", "type": "python", "request": "launch", - "program": "${file}", + "program": "main.py", "console": "integratedTerminal", "args": ["./configs/LasVegas/LasVegas.yml"] } diff --git a/scripts/python/generateMaps/configs/LasVegas/LasVegas.kml b/scripts/python/map_generator/configs/LasVegas/LasVegas.kml similarity index 52% rename from scripts/python/generateMaps/configs/LasVegas/LasVegas.kml rename to scripts/python/map_generator/configs/LasVegas/LasVegas.kml index a401dd8c..b2da4f16 100644 --- a/scripts/python/generateMaps/configs/LasVegas/LasVegas.kml +++ b/scripts/python/map_generator/configs/LasVegas/LasVegas.kml @@ -2,7 +2,7 @@ Senza titolo - + - + - + normal - #__managed_style_1E6AF60F852F06CED14C + #__managed_style_1FB08D70372F0ACE3361 highlight - #__managed_style_29D7120C702F06CED14C + #__managed_style_2F9039BE1B2F0ACE3361 - + Poligono senza titolo - -115.7575513617584 - 36.45909683572987 - 1668.83938821393 + -115.0437621195802 + 36.2404454323581 + 568.1069300877758 0 0 35 - 382627.9679017514 + 21582.08160380367 absolute - #__managed_style_05AC9C65832F06CED14C + #__managed_style_0152B588AD2F0ACE3361 - -115.5765603362792,35.92113103850846,0 -114.7667743850415,35.914903046417,0 -114.7800419428627,36.39467581209903,0 -115.6198023719551,36.39886214564519,0 -115.5765603362792,35.92113103850846,0 + -115.0657741984423,36.20908708202413,0 -115.0064821223275,36.20969233542438,0 -115.0077003574054,36.25251471885595,0 -115.0604905801644,36.25236266770626,0 -115.0657741984423,36.20908708202413,0 + + + + + + + Poligono senza titolo + + -115.144039076036 + 36.07599823986274 + 636.6854074835677 + 0 + 0 + 35 + 14114.14487087633 + absolute + + #__managed_style_0152B588AD2F0ACE3361 + + + + + -115.1866576903507,36.06787728722109,0 -115.1107872218999,36.06765965614163,0 -115.1145241760975,36.10294242539007,0 -115.1779590799479,36.10179027153036,0 -115.1866576903507,36.06787728722109,0 diff --git a/scripts/python/map_generator/configs/LasVegas/LasVegas.yml b/scripts/python/map_generator/configs/LasVegas/LasVegas.yml new file mode 100644 index 00000000..404a661b --- /dev/null +++ b/scripts/python/map_generator/configs/LasVegas/LasVegas.yml @@ -0,0 +1,4 @@ +{ + 'output_directory': '.\LasVegas', # Where to save the output files + 'boundary_file': '.\configs\LasVegas\LasVegas.kml' +} \ No newline at end of file diff --git a/scripts/python/generateMaps/configs/screen_properties.yml b/scripts/python/map_generator/configs/screen_properties.yml similarity index 100% rename from scripts/python/generateMaps/configs/screen_properties.yml rename to scripts/python/map_generator/configs/screen_properties.yml diff --git a/scripts/python/generateMaps/main.py b/scripts/python/map_generator/main.py similarity index 87% rename from scripts/python/generateMaps/main.py rename to scripts/python/map_generator/main.py index d2f19ae7..79cac4a6 100644 --- a/scripts/python/generateMaps/main.py +++ b/scripts/python/map_generator/main.py @@ -5,7 +5,7 @@ from fastkml import kml from shapely import wkt from datetime import timedelta -import capture_screen +import map_generator if len(sys.argv) == 1: print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.") @@ -38,14 +38,15 @@ else: k.from_string(doc) geod = Geod(ellps="WGS84") - features = [f for f in list(k.features()) if not f.isopen] - print(f"Found {len(features)} closed features in the provided kml file") - + features = [] area = 0 - for feature in features: + for feature in k.features(): for sub_feature in list(feature.features()): geo = sub_feature.geometry area += abs(geod.geometry_area_perimeter(wkt.loads(geo.wkt))[0]) + features.append(sub_feature) + + print(f"Found {len(features)} features in the provided kml file") tile_size = 256 * screen_config["geo_resolution"] # meters tiles_per_screenshot = int(screen_config["width"] / 256) * int(screen_config["height"] / 256) @@ -57,10 +58,10 @@ else: print(f"Estimated number of tiles: {tiles_num}") print(f"Estimated number of screenshots: {screenshots_num}") print(f"Estimated time to complete: {timedelta(seconds=total_time)} (hh:mm:ss)") - print("The script is ready to go. After you press any key, it will wait for 5 seconds, and then it will start.") + print("The script is ready to go. After you press enter, it will wait for 5 seconds, then it will start.") - input("Press any key to continue...") - capture_screen.run(map_config) + input("Press enter to continue...") + map_generator.run(map_config) diff --git a/scripts/python/map_generator/map_generator.py b/scripts/python/map_generator/map_generator.py new file mode 100644 index 00000000..f5adaff2 --- /dev/null +++ b/scripts/python/map_generator/map_generator.py @@ -0,0 +1,224 @@ +import math +import requests +import pyautogui +import time +import os +import yaml + +from fastkml import kml +from shapely import wkt, Point +from PIL import Image +from concurrent import futures + +# global counters +fut_counter = 0 +tot_futs = 0 + +# constants +C = 40075016.686 # meters, Earth equatorial circumference + +def deg_to_num(lat_deg, lon_deg, zoom): + lat_rad = math.radians(lat_deg) + n = 1 << zoom + xtile = int((lon_deg + 180.0) / 360.0 * n) + ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n) + return xtile, ytile + +def num_to_deg(xtile, ytile, zoom): + n = 1 << zoom + lon_deg = xtile / n * 360.0 - 180.0 + lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n))) + lat_deg = math.degrees(lat_rad) + return lat_deg, lon_deg + +def compute_mpps(lat, z): + return C * math.cos(math.radians(lat)) / math.pow(2, z + 8) + +def printProgressBar(iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', printEnd = "\r"): + percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total))) + filledLength = int(length * iteration // total) + bar = fill * filledLength + '-' * (length - filledLength) + print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd) + # Print New Line on Complete + if iteration == total: + print() + +def done_callback(fut): + global fut_counter, tot_futs + fut_counter += 1 + printProgressBar(fut_counter, tot_futs) + +def extract_tiles(n, screenshots_coordinates, params): + f = params["f"] + zoom = params["zoom"] + output_directory = params["output_directory"] + n_width = params["n_width"] + n_height = params["n_height"] + screen_resolution = params["screen_resolution"] + mpps = params["mpps"] + + coords = screenshots_coordinates[n] + if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))): + # Open the source screenshot + img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")) + + # Scale the image so that tiles are 256x256 + scale = screen_resolution / mpps + w, h = img.size + img = img.resize((int(w * scale), int(h * scale))) + + # Compute the Web Mercator Projection position of the top left corner of the most centered tile + lat = coords[0] + lng = coords[1] + X_center, Y_center = deg_to_num(lat, lng, zoom) + + # Compute the position of the top left corner of the top left tile + start_x = w / 2 - n_width / 2 * 256 + start_y = h / 2 - n_height / 2 * 256 + + # Iterate on the grid + for column in range(0, n_width): + for row in range(0, n_height): + # Crop the tile and compute its Web Mercator Projection position + box = (start_x + column * 256, start_y + row * 256, start_x + (column + 1) * 256, start_y + (row + 1) * 256) + X = X_center - math.floor(n_width / 2) + column + Y = Y_center - math.floor(n_height / 2) + row + + # Save the tile + if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom), str(X))): + try: + os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X))) + except FileExistsError: + continue + except Exception as e: + raise e + img.crop(box).save(os.path.join(output_directory, "tiles", str(zoom), str(X), f"{Y}.jpg")) + n += 1 + + else: + raise Exception(f"{os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")} missing") + +def run(map_config): + with open('configs/screen_properties.yml', 'r') as sp: + screen_config = yaml.safe_load(sp) + + # Create output folders + output_directory = map_config["output_directory"] + if not os.path.exists(output_directory): + os.mkdir(output_directory) + + if not os.path.exists(os.path.join(output_directory, "screenshots")): + os.mkdir(os.path.join(output_directory, "screenshots")) + else: + skip_screenshots = (input("Raw screenshots already found for this config, do you want to skip directly to tiles extraction? Enter y to skip: ") == "y") + + if not os.path.exists(os.path.join(output_directory, "tiles")): + os.mkdir(os.path.join(output_directory, "tiles")) + + # Compute the optimal zoom level + usable_width = screen_config["width"] - 200 # Keep a margin around the center + usable_height = screen_config["height"] - 200 # Keep a margin around the center + + with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp: + # Read the config file + doc = bp.read() + k = kml.KML() + k.from_string(doc) + + # Extract the features + features = [] + for feature in k.features(): + for sub_feature in list(feature.features()): + features.append(sub_feature) + + # Iterate over all the closed features in the kml file + f = 1 + for feature in features: + geo = sub_feature.geometry + + # Define the boundary rect around the area + start_lat = geo.bounds[3] + start_lng = geo.bounds[0] + end_lat = geo.bounds[1] + end_lng = geo.bounds[2] + + # Find the zoom level that better approximates the provided resolution + screen_resolution = screen_config['geo_resolution'] + mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - screen_resolution) for z in range(0, 21)] + zoom = mpps_delta.index(min(mpps_delta)) + + print(f"Feature {f} of {len(features)}, using zoom level {zoom}") + + # Find the maximum dimension of the tiles at the given resolution + mpps = compute_mpps(end_lat, zoom) + d = 256 * mpps / screen_resolution + + n_height = math.floor(usable_height / d) + n_width = math.floor(usable_width / d) + + print(f"Feature {f} of {len(features)}, each screenshot will provide {n_height} tiles in height and {n_width} tiles in width") + + # Find the starting and ending points + start_X, start_Y = deg_to_num(start_lat, start_lng, zoom) + end_X, end_Y = deg_to_num(end_lat, end_lng, zoom) + + # Find all the X, Y coordinates inside of the provided area + screenshots_coordinates = [] + for X in range(start_X, end_X, n_width): + for Y in range(start_Y, end_Y, n_height): + lat, lng = num_to_deg(X, Y, zoom) + p = Point(lng, lat) + if p.within(wkt.loads(geo.wkt)): + screenshots_coordinates.append((lat, lng)) + + print(f"Feature {f} of {len(features)}, {len(screenshots_coordinates)} screenshots will be taken") + + # Start looping + if not skip_screenshots: + print(f"Feature {f} of {len(features)}, taking screenshots...") + n = 0 + for coords in screenshots_coordinates: + # Making PUT request + #data = json.dumps({'lat': coords[0], 'lng': coords[1]}) + #r = requests.put('http://localhost:8080', data = data) + + time.sleep(0.1) + + ## Take and save screenshot + screenshot = pyautogui.screenshot() + screenshot.save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")) + + printProgressBar(n + 1, len(screenshots_coordinates)) + n += 1 + + # Extract the tiles + if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom))): + os.mkdir(os.path.join(output_directory, "tiles", str(zoom))) + + params = { + "f": f, + "zoom": zoom, + "output_directory": output_directory, + "n_width": n_width, + "n_height": n_height, + "screen_resolution": screen_resolution, + "mpps": mpps + } + + # Extract the tiles with parallel thread execution + with futures.ThreadPoolExecutor() as executor: + print(f"Feature {f} of {len(features)}, extracting tiles...") + global tot_futs, fut_counter + futs = [executor.submit(extract_tiles, n, screenshots_coordinates, params) for n in range(0, len(screenshots_coordinates))] + tot_futs = len(futs) + fut_counter = 0 + [fut.add_done_callback(done_callback) for fut in futs] + [fut.result() for fut in futures.as_completed(futs)] + + # Increase the feature counter + print(f"Feature {f} of {len(features)} completed!") + f += 1 + + + + diff --git a/scripts/python/generateMaps/requirements.txt b/scripts/python/map_generator/requirements.txt similarity index 100% rename from scripts/python/generateMaps/requirements.txt rename to scripts/python/map_generator/requirements.txt From 2e1c3ec4b9c9f118ce18bd73299ff1e2afa016dc Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Fri, 23 Feb 2024 16:25:19 +0100 Subject: [PATCH 04/10] Added ability to add more map sources --- frontend/server/app.js | 2 +- frontend/server/routes/resources.js | 252 ++++++++++---------- frontend/website/src/constants/constants.ts | 8 +- frontend/website/src/map/map.ts | 25 +- frontend/website/src/olympusapp.ts | 18 ++ olympus.json | 10 +- 6 files changed, 180 insertions(+), 135 deletions(-) diff --git a/frontend/server/app.js b/frontend/server/app.js index 1f43110f..be9c829c 100644 --- a/frontend/server/app.js +++ b/frontend/server/app.js @@ -15,7 +15,7 @@ module.exports = function (configLocation) { var indexRouter = require('./routes/index'); var uikitRouter = require('./routes/uikit'); var usersRouter = require('./routes/users'); - var resourcesRouter = require('./routes/resources'); + var resourcesRouter = require('./routes/resources')(configLocation); var pluginsRouter = require('./routes/plugins'); /* Load the config and create the express app */ diff --git a/frontend/server/routes/resources.js b/frontend/server/routes/resources.js index ebb34ad7..e2460f42 100644 --- a/frontend/server/routes/resources.js +++ b/frontend/server/routes/resources.js @@ -4,140 +4,152 @@ const fs = require('fs'); const pfs = require('fs/promises') const router = express.Router(); -router.get('/theme/*', function (req, res, next) { - var reqTheme = "olympus"; - - /* Yes, this in an easter egg! :D Feel free to ignore it, or activate the parrot theme to check what it does. Why parrots? The story is a bit long, come to the Discord and ask :D */ - if (reqTheme === "parrot" && !req.url.includes(".css")) - res.redirect('/themes/parrot/images/parrot.svg'); - else - res.redirect(req.url.replace("theme", "themes/" + reqTheme)); -}); +module.exports = function (configLocation) { + router.get('/theme/*', function (req, res, next) { + var reqTheme = "olympus"; + + /* Yes, this in an easter egg! :D Feel free to ignore it, or activate the parrot theme to check what it does. Why parrots? The story is a bit long, come to the Discord and ask :D */ + if (reqTheme === "parrot" && !req.url.includes(".css")) + res.redirect('/themes/parrot/images/parrot.svg'); + else + res.redirect(req.url.replace("theme", "themes/" + reqTheme)); + }); -router.put('/theme/:newTheme', function (req, res, next) { - res.end("Ok"); -}); + router.put('/theme/:newTheme', function (req, res, next) { + res.end("Ok"); + }); -router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) { - let map = req.params.map; - let x = req.params.x; - let y = req.params.y; - let z = req.params.z; - - if (fs.existsSync(`.\\public\\maps\\${map}`)) { - if (!await renderImage(map, z, x, y, res)) { - /* No image was found */ + router.get('/config', function (req, res, next) { + if (fs.existsSync(configLocation)) { + let rawdata = fs.readFileSync(configLocation); + config = JSON.parse(rawdata); + res.send(JSON.stringify(config.frontend)); + res.end() + } else { res.sendStatus(404); } - } else { - /* The requested map does not exist */ - res.sendStatus(404); - } -}); + }); -async function renderImage(map, z, x, y, res, recursionDepth = 0) { - if (recursionDepth == 20) { - console.log("Render image, maximum recursion depth reached") - /* We have reached limit recusion depth, something went wrong */ - return false; - } - /* If the requested image exists, send it straight away */ - if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { - res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - return true; - } else { - /* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */ - let sourceZoom = z - 1; - let sourceX = Math.floor(x / 2); - let sourceY = Math.floor(y / 2); + router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) { + let map = req.params.map; + let x = req.params.x; + let y = req.params.y; + let z = req.params.z; - /* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */ - while (sourceZoom >= 0) { - /* We have found a possible source image */ - if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) { - /* Split the image into four. We can retry up to 10 times to clear any race condition on the files */ - let retries = 10; - while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) { - await new Promise(r => setTimeout(r, 1000)); - retries--; - } - /* Check if we exited because we reached the maximum retry value */ - if (retries != 0) { - /* Recursively recall the function now that we have a new "source" image */ - return await renderImage(map, z, x, y, res, recursionDepth + 1); - } else { - return false; - } - } else { - /* Keep searching at a higher level */ - sourceZoom = sourceZoom - 1; - sourceX = Math.floor(sourceX / 2); - sourceY = Math.floor(sourceY / 2); + if (fs.existsSync(`.\\public\\maps\\${map}`)) { + if (!await renderImage(map, z, x, y, res)) { + /* No image was found */ + res.sendStatus(404); } + } else { + /* The requested map does not exist */ + res.sendStatus(404); + } + }); + + async function renderImage(map, z, x, y, res, recursionDepth = 0) { + if (recursionDepth == 20) { + console.log("Render image, maximum recursion depth reached") + /* We have reached limit recusion depth, something went wrong */ + return false; } - return false; - } -} + /* If the requested image exists, send it straight away */ + if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { + res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + return true; + } else { + /* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */ + let sourceZoom = z - 1; + let sourceX = Math.floor(x / 2); + let sourceY = Math.floor(y / 2); -async function splitTile(map, z, x, y) { - try { - /* Load the source image */ - let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - - /* Create the necessary folders */ - await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true }) - await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true }) - - /* Split the image into four parts */ - await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y); - img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y); - img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1); - img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1); - return true; - } catch (err) { - if (err.code !== 'EBUSY') { - console.error(err); + /* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */ + while (sourceZoom >= 0) { + /* We have found a possible source image */ + if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) { + /* Split the image into four. We can retry up to 10 times to clear any race condition on the files */ + let retries = 10; + while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) { + await new Promise(r => setTimeout(r, 1000)); + retries--; + } + /* Check if we exited because we reached the maximum retry value */ + if (retries != 0) { + /* Recursively recall the function now that we have a new "source" image */ + return await renderImage(map, z, x, y, res, recursionDepth + 1); + } else { + return false; + } + } else { + /* Keep searching at a higher level */ + sourceZoom = sourceZoom - 1; + sourceX = Math.floor(sourceX / 2); + sourceY = Math.floor(sourceY / 2); + } + } + return false; } - return false; } -} -/* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */ -function resizePromise(img, left, top, map, z, x, y) { - return new Promise((res, rej) => { - if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { - res(true); - } - img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) { - if (err) { - rej(err); - } else { - try { - fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - } catch (err) { - if (err.code === 'EBUSY') { - /* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */ - rej(err); - } else if (err.code === 'ENOENT') { - /* Someone else renamed the file already so this is not a real error */ - if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { - res(true); - } - /* Something odd happened, reject and try again */ - else { + async function splitTile(map, z, x, y) { + try { + /* Load the source image */ + let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + + /* Create the necessary folders */ + await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true }) + await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true }) + + /* Split the image into four parts */ + await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y); + img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y); + img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1); + img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1); + return true; + } catch (err) { + if (err.code !== 'EBUSY') { + console.error(err); + } + return false; + } + } + + /* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */ + function resizePromise(img, left, top, map, z, x, y) { + return new Promise((res, rej) => { + if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { + res(true); + } + img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) { + if (err) { + rej(err); + } else { + try { + fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); + } catch (err) { + if (err.code === 'EBUSY') { + /* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */ + rej(err); + } else if (err.code === 'ENOENT') { + /* Someone else renamed the file already so this is not a real error */ + if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { + res(true); + } + /* Something odd happened, reject and try again */ + else { + rej(err); + } + } else { rej(err); } - } else { - rej(err); } + res(true) } - res(true) - } - }); - }) + }); + }) + } + return router; } - -module.exports = router; diff --git a/frontend/website/src/constants/constants.ts b/frontend/website/src/constants/constants.ts index 52e185cd..fc96f47a 100644 --- a/frontend/website/src/constants/constants.ts +++ b/frontend/website/src/constants/constants.ts @@ -154,7 +154,7 @@ export const mapBounds = { "SinaiMap": { bounds: new LatLngBounds([34.312222, 28.523333], [25.946944, 36.897778]), zoom: 4 }, } -export const mapLayers = { +export const defaultMapLayers = { "ArcGIS Satellite": { urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", minZoom: 1, @@ -190,12 +190,6 @@ export const mapLayers = { minZoom: 1, maxZoom: 20, attribution: 'CyclOSM | Map data: © OpenStreetMap contributors' - }, - "DCS": { - urlTemplate: 'http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png', - minZoom: 16, - maxZoom: 20, - attribution: 'Eagle Dynamics' } } diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index a10afd7d..32eccb44 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -12,7 +12,7 @@ import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' -import { mapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants"; +import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants"; import { CoalitionArea } from "./coalitionarea/coalitionarea"; import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { DrawingCursor } from "./coalitionarea/drawingcursor"; @@ -90,6 +90,7 @@ export class Map extends L.Map { #coalitionAreaContextMenu: CoalitionAreaContextMenu = new CoalitionAreaContextMenu("coalition-area-contextmenu"); #mapSourceDropdown: Dropdown; + #mapLayers: any = defaultMapLayers; #mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS; #mapVisibilityOptionsDropdown: Dropdown; #optionButtons: { [key: string]: HTMLButtonElement[] } = {} @@ -120,10 +121,10 @@ export class Map extends L.Map { this.#ID = ID; - this.setLayer(Object.keys(mapLayers)[0]); + this.setLayer(Object.keys(this.#mapLayers)[0]); /* Minimap */ - var minimapLayer = new L.TileLayer(mapLayers[Object.keys(mapLayers)[0] as keyof typeof mapLayers].urlTemplate, { minZoom: 0, maxZoom: 13 }); + var minimapLayer = new L.TileLayer(this.#mapLayers[Object.keys(this.#mapLayers)[0]].urlTemplate, { minZoom: 0, maxZoom: 13 }); this.#miniMapLayerGroup = new L.LayerGroup([minimapLayer]); this.#miniMapPolyline = new L.Polyline([], { color: '#202831' }); this.#miniMapPolyline.addTo(this.#miniMapLayerGroup); @@ -203,6 +204,18 @@ export class Map extends L.Map { this.getContainer().toggleAttribute("data-hide-labels", !this.getVisibilityOptions()[SHOW_UNIT_LABELS]); }); + document.addEventListener("configLoaded", () => { + let config = getApp().getConfig(); + if (config.additionalMaps) { + let additionalMaps = config.additionalMaps; + this.#mapLayers = { + ...this.#mapLayers, + ...additionalMaps + } + this.#mapSourceDropdown.setOptions(this.getLayers()); + } + }) + /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) @@ -234,8 +247,8 @@ export class Map extends L.Map { if (this.#layer != null) this.removeLayer(this.#layer) - if (layerName in mapLayers) { - const layerData = mapLayers[layerName as keyof typeof mapLayers]; + if (layerName in this.#mapLayers) { + const layerData = this.#mapLayers[layerName]; var options: L.TileLayerOptions = { attribution: layerData.attribution, minZoom: layerData.minZoom, @@ -248,7 +261,7 @@ export class Map extends L.Map { } getLayers() { - return Object.keys(mapLayers); + return Object.keys(this.#mapLayers); } /* State machine */ diff --git a/frontend/website/src/olympusapp.ts b/frontend/website/src/olympusapp.ts index 4d916d11..e2c5dc09 100644 --- a/frontend/website/src/olympusapp.ts +++ b/frontend/website/src/olympusapp.ts @@ -33,6 +33,7 @@ export class OlympusApp { /* Global data */ #activeCoalition: string = "blue"; #latestVersion: string|undefined = undefined; + #config: any = {}; /* Main leaflet map, extended by custom methods */ #map: Map | null = null; @@ -251,6 +252,19 @@ export class OlympusApp { latestVersionSpan.classList.toggle("new-version", this.#latestVersion !== VERSION); } }) + + /* Load the config file from the server */ + const configRequest = new Request(location.href + "resources/config"); + fetch(configRequest).then((response) => { + if (response.status === 200) { + return response.json(); + } else { + throw new Error("Error retrieving config file"); + } + }).then((res) => { + this.#config = res; + document.dispatchEvent(new CustomEvent("configLoaded")); + }) } #setupEvents() { @@ -446,4 +460,8 @@ export class OlympusApp { img.addEventListener("load", () => { SVGInjector(img); }); }) } + + getConfig() { + return this.#config; + } } \ No newline at end of file diff --git a/olympus.json b/olympus.json index e40a4843..54c8916f 100644 --- a/olympus.json +++ b/olympus.json @@ -14,6 +14,14 @@ "provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip", "username": null, "password": null - } + }, + "additionalMaps": { + "DCS": { + "urlTemplate": "http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png", + "minZoom": 8, + "maxZoom": 20, + "attribution": "Eagle Dynamics" + } + } } } \ No newline at end of file From c74258e3ad9ab8e371426acb2c834794a9563558 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Mon, 26 Feb 2024 08:55:03 +0100 Subject: [PATCH 05/10] Completed automatic algorithm --- frontend/server/routes/resources.js | 123 +------------- frontend/website/src/map/map.ts | 4 +- olympus.json | 3 +- .../python/map_generator/.vscode/launch.json | 2 +- .../configs/LasVegas/LasVegas.yml | 4 - .../LasVegas/{LasVegas.kml => boundary.kml} | 0 .../map_generator/configs/LasVegas/config.yml | 6 + .../map_generator/configs/NTTR/boundary.kml | 84 ++++++++++ .../map_generator/configs/NTTR/config.yml | 5 + .../configs/screen_properties.yml | 10 +- scripts/python/map_generator/main.py | 42 +++-- scripts/python/map_generator/map_generator.py | 158 +++++++++++++----- 12 files changed, 247 insertions(+), 194 deletions(-) delete mode 100644 scripts/python/map_generator/configs/LasVegas/LasVegas.yml rename scripts/python/map_generator/configs/LasVegas/{LasVegas.kml => boundary.kml} (100%) create mode 100644 scripts/python/map_generator/configs/LasVegas/config.yml create mode 100644 scripts/python/map_generator/configs/NTTR/boundary.kml create mode 100644 scripts/python/map_generator/configs/NTTR/config.yml diff --git a/frontend/server/routes/resources.js b/frontend/server/routes/resources.js index e2460f42..391a1550 100644 --- a/frontend/server/routes/resources.js +++ b/frontend/server/routes/resources.js @@ -29,127 +29,6 @@ module.exports = function (configLocation) { res.sendStatus(404); } }); - - router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) { - let map = req.params.map; - let x = req.params.x; - let y = req.params.y; - let z = req.params.z; - - if (fs.existsSync(`.\\public\\maps\\${map}`)) { - if (!await renderImage(map, z, x, y, res)) { - /* No image was found */ - res.sendStatus(404); - } - } else { - /* The requested map does not exist */ - res.sendStatus(404); - } - }); - - async function renderImage(map, z, x, y, res, recursionDepth = 0) { - if (recursionDepth == 20) { - console.log("Render image, maximum recursion depth reached") - /* We have reached limit recusion depth, something went wrong */ - return false; - } - /* If the requested image exists, send it straight away */ - if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { - res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - return true; - } else { - /* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */ - let sourceZoom = z - 1; - let sourceX = Math.floor(x / 2); - let sourceY = Math.floor(y / 2); - - /* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */ - while (sourceZoom >= 0) { - /* We have found a possible source image */ - if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) { - /* Split the image into four. We can retry up to 10 times to clear any race condition on the files */ - let retries = 10; - while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) { - await new Promise(r => setTimeout(r, 1000)); - retries--; - } - /* Check if we exited because we reached the maximum retry value */ - if (retries != 0) { - /* Recursively recall the function now that we have a new "source" image */ - return await renderImage(map, z, x, y, res, recursionDepth + 1); - } else { - return false; - } - } else { - /* Keep searching at a higher level */ - sourceZoom = sourceZoom - 1; - sourceX = Math.floor(sourceX / 2); - sourceY = Math.floor(sourceY / 2); - } - } - return false; - } - } - - async function splitTile(map, z, x, y) { - try { - /* Load the source image */ - let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - - /* Create the necessary folders */ - await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true }) - await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true }) - - /* Split the image into four parts */ - await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y); - img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y); - img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1); - img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1); - return true; - } catch (err) { - if (err.code !== 'EBUSY') { - console.error(err); - } - return false; - } - } - - /* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */ - function resizePromise(img, left, top, map, z, x, y) { - return new Promise((res, rej) => { - if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { - res(true); - } - img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) { - if (err) { - rej(err); - } else { - try { - fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`); - } catch (err) { - if (err.code === 'EBUSY') { - /* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */ - rej(err); - } else if (err.code === 'ENOENT') { - /* Someone else renamed the file already so this is not a real error */ - if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) { - res(true); - } - /* Something odd happened, reject and try again */ - else { - rej(err); - } - } else { - rej(err); - } - } - res(true) - } - }); - }) - } + return router; } diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index 32eccb44..4c82614c 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -252,7 +252,9 @@ export class Map extends L.Map { var options: L.TileLayerOptions = { attribution: layerData.attribution, minZoom: layerData.minZoom, - maxZoom: layerData.maxZoom + maxZoom: layerData.maxZoom, + minNativeZoom: layerData.minNativeZoom, + maxNativeZoom: layerData.maxNativeZoom }; this.#layer = new L.TileLayer(layerData.urlTemplate, options); } diff --git a/olympus.json b/olympus.json index 54c8916f..ccac4692 100644 --- a/olympus.json +++ b/olympus.json @@ -17,9 +17,10 @@ }, "additionalMaps": { "DCS": { - "urlTemplate": "http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png", + "urlTemplate": "http://localhost:3000/maps/dcs/{z}/{x}/{y}.jpg", "minZoom": 8, "maxZoom": 20, + "maxNativeZoom": 17, "attribution": "Eagle Dynamics" } } diff --git a/scripts/python/map_generator/.vscode/launch.json b/scripts/python/map_generator/.vscode/launch.json index c5fdb6ca..acdc83b5 100644 --- a/scripts/python/map_generator/.vscode/launch.json +++ b/scripts/python/map_generator/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "program": "main.py", "console": "integratedTerminal", - "args": ["./configs/LasVegas/LasVegas.yml"] + "args": ["./configs/NTTR/config.yml"] } ] } \ No newline at end of file diff --git a/scripts/python/map_generator/configs/LasVegas/LasVegas.yml b/scripts/python/map_generator/configs/LasVegas/LasVegas.yml deleted file mode 100644 index 404a661b..00000000 --- a/scripts/python/map_generator/configs/LasVegas/LasVegas.yml +++ /dev/null @@ -1,4 +0,0 @@ -{ - 'output_directory': '.\LasVegas', # Where to save the output files - 'boundary_file': '.\configs\LasVegas\LasVegas.kml' -} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/LasVegas/LasVegas.kml b/scripts/python/map_generator/configs/LasVegas/boundary.kml similarity index 100% rename from scripts/python/map_generator/configs/LasVegas/LasVegas.kml rename to scripts/python/map_generator/configs/LasVegas/boundary.kml diff --git a/scripts/python/map_generator/configs/LasVegas/config.yml b/scripts/python/map_generator/configs/LasVegas/config.yml new file mode 100644 index 00000000..c7b54066 --- /dev/null +++ b/scripts/python/map_generator/configs/LasVegas/config.yml @@ -0,0 +1,6 @@ +{ + 'output_directory': '.\LasVegas', # Where to save the output files + 'boundary_file': '.\configs\LasVegas\boundary.kml', # Input kml file setting the boundary of the map to create + 'zoom_factor': 0.02, # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)] + 'geo_width': 1.14 +} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/NTTR/boundary.kml b/scripts/python/map_generator/configs/NTTR/boundary.kml new file mode 100644 index 00000000..486eb401 --- /dev/null +++ b/scripts/python/map_generator/configs/NTTR/boundary.kml @@ -0,0 +1,84 @@ + + + + Senza titolo + + + + + + + + + normal + #__managed_style_1847AF2A832F1651A60F + + + highlight + #__managed_style_2C7F63B5A12F1651A60F + + + + NTTR + + -117.2703145690532 + 37.39557832822189 + 1754.517427470683 + 359.4706465490362 + 0 + 35 + 1393300.815671235 + absolute + + #__managed_style_043F3D3A202F1651A60F + + + + + -119.7864240113604,34.44074394422174,0 -112.42342379541,34.34217218687283,0 -112.1179107081757,39.75928290264283,0 -120.0041004413372,39.79698539473655,0 -119.7864240113604,34.44074394422174,0 + + + + + + + diff --git a/scripts/python/map_generator/configs/NTTR/config.yml b/scripts/python/map_generator/configs/NTTR/config.yml new file mode 100644 index 00000000..7583cafe --- /dev/null +++ b/scripts/python/map_generator/configs/NTTR/config.yml @@ -0,0 +1,5 @@ +{ + 'output_directory': '.\NTTR', # Where to save the output files + 'boundary_file': '.\configs\NTTR\boundary.kml', # Input kml file setting the boundary of the map to create + 'zoom_factor': 0.5 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)] +} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/screen_properties.yml b/scripts/python/map_generator/configs/screen_properties.yml index 6e7cd752..d578b910 100644 --- a/scripts/python/map_generator/configs/screen_properties.yml +++ b/scripts/python/map_generator/configs/screen_properties.yml @@ -1,10 +1,4 @@ { - 'width': 1920, # The width of your screen, in pixels - 'height': 1080, # The height of your screen, in pixels - 'geo_resolution': 1.0 # The resolution of the map on the screen, in meters per pixel. - # To measure this value, first set the F10 map at the desired zoom level. - # Then, use F10's map measure tool, and measure the width of the screen in meters. - # Finally, divide that value by the width in pixels. - # A good value would be around 1 meter per pixel, meaning a 1920px wide map would measure about 1 nautical mile across on the F10 map - # Lower values will produce higher resolution maps, but beware of space usage! + 'width': 1920, # The width of your screen, in pixels + 'height': 1080 # The height of your screen, in pixels } \ No newline at end of file diff --git a/scripts/python/map_generator/main.py b/scripts/python/map_generator/main.py index 79cac4a6..baa269d8 100644 --- a/scripts/python/map_generator/main.py +++ b/scripts/python/map_generator/main.py @@ -1,5 +1,9 @@ import sys import yaml +import json +import requests +import time + from pyproj import Geod from fastkml import kml from shapely import wkt @@ -18,20 +22,19 @@ else: screen_config = yaml.safe_load(sp) map_config = yaml.safe_load(cp) - print("#################################################################################################################################################") - print("# IMPORTANT NOTE: the screen properties must be configured according to your screen and desired zoom level. Make sure you set them accordingly. #") - print("#################################################################################################################################################") - print("Screen parameters:") - print(f"-> Screen width: {screen_config["width"]}px") - print(f"-> Screen height: {screen_config["height"]}px") - print(f"-> Geographic resolution: {screen_config["geo_resolution"]} meters/pixel") + print(f"-> Screen width: {screen_config['width']}px") + print(f"-> Screen height: {screen_config['height']}px") print("Map parameters:") - print(f"-> Output directory: {map_config["output_directory"]}") - print(f"-> Boundary file: {map_config["boundary_file"]}") + print(f"-> Output directory: {map_config['output_directory']}") + print(f"-> Boundary file: {map_config['boundary_file']}") + print(f"-> Zoom factor: {map_config['zoom_factor']}") - with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp: + if 'geo_width' in map_config: + print(f"-> Geo width: {map_config['geo_width']}NM") + + with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp: # Read the config file and compute the total area of the covered map doc = bp.read() k = kml.KML() @@ -48,8 +51,17 @@ else: print(f"Found {len(features)} features in the provided kml file") - tile_size = 256 * screen_config["geo_resolution"] # meters - tiles_per_screenshot = int(screen_config["width"] / 256) * int(screen_config["height"] / 256) + if 'geo_width' not in map_config: + # Let the user input the size of the screen to compute resolution + data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)}) + r = requests.put('http://localhost:8080', data = data) + print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles") + map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ") + + map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width'] + + tile_size = 256 * map_config['mpps'] # meters + tiles_per_screenshot = int(screen_config['width'] / 256) * int(screen_config['height'] / 256) tiles_num = int(area / (tile_size * tile_size)) screenshots_num = int(tiles_num / tiles_per_screenshot) total_time = int(screenshots_num / 1.0) @@ -57,11 +69,11 @@ else: print(f"Total area: {int(area / 1e6)} square kilometers") print(f"Estimated number of tiles: {tiles_num}") print(f"Estimated number of screenshots: {screenshots_num}") - print(f"Estimated time to complete: {timedelta(seconds=total_time)} (hh:mm:ss)") - print("The script is ready to go. After you press enter, it will wait for 5 seconds, then it will start.") - + print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)") input("Press enter to continue...") + map_generator.run(map_config) + diff --git a/scripts/python/map_generator/map_generator.py b/scripts/python/map_generator/map_generator.py index f5adaff2..27726b27 100644 --- a/scripts/python/map_generator/map_generator.py +++ b/scripts/python/map_generator/map_generator.py @@ -4,11 +4,14 @@ import pyautogui import time import os import yaml +import json from fastkml import kml from shapely import wkt, Point from PIL import Image from concurrent import futures +from os import listdir +from os.path import isfile, isdir, join # global counters fut_counter = 0 @@ -16,6 +19,7 @@ tot_futs = 0 # constants C = 40075016.686 # meters, Earth equatorial circumference +R = C / (2 * math.pi) def deg_to_num(lat_deg, lon_deg, zoom): lat_rad = math.radians(lat_deg) @@ -48,33 +52,24 @@ def done_callback(fut): fut_counter += 1 printProgressBar(fut_counter, tot_futs) -def extract_tiles(n, screenshots_coordinates, params): - f = params["f"] - zoom = params["zoom"] - output_directory = params["output_directory"] - n_width = params["n_width"] - n_height = params["n_height"] - screen_resolution = params["screen_resolution"] - mpps = params["mpps"] +def extract_tiles(n, screenshots_XY, params): + f = params['f'] + zoom = params['zoom'] + output_directory = params['output_directory'] + n_width = params['n_width'] + n_height = params['n_height'] - coords = screenshots_coordinates[n] + XY = screenshots_XY[n] if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))): # Open the source screenshot img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")) - # Scale the image so that tiles are 256x256 - scale = screen_resolution / mpps - w, h = img.size - img = img.resize((int(w * scale), int(h * scale))) - # Compute the Web Mercator Projection position of the top left corner of the most centered tile - lat = coords[0] - lng = coords[1] - X_center, Y_center = deg_to_num(lat, lng, zoom) + X_center, Y_center = XY[0], XY[1] # Compute the position of the top left corner of the top left tile - start_x = w / 2 - n_width / 2 * 256 - start_y = h / 2 - n_height / 2 * 256 + start_x = img.width / 2 - n_width / 2 * 256 + start_y = img.height / 2 - n_height / 2 * 256 # Iterate on the grid for column in range(0, n_width): @@ -96,17 +91,44 @@ def extract_tiles(n, screenshots_coordinates, params): n += 1 else: - raise Exception(f"{os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")} missing") + raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}.jpg')} missing") + +def merge_tiles(base_path, zoom, tile): + X = tile[0] + Y = tile[1] + positions = [(0, 0), (0, 1), (1, 0), (1, 1)] + + dst = Image.new('RGB', (256, 256), (0, 0, 0, 0)) + for i in range(0, 4): + if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")): + im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128)) + else: + im = Image.new('RGB', (128, 128), (0, 0, 0, 0)) + dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128)) + + if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))): + try: + os.mkdir(os.path.join(base_path, str(zoom - 1), str(X))) + except FileExistsError: + pass + except Exception as e: + raise e + + dst.save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95) + def run(map_config): + global tot_futs, fut_counter + with open('configs/screen_properties.yml', 'r') as sp: screen_config = yaml.safe_load(sp) # Create output folders - output_directory = map_config["output_directory"] + output_directory = map_config['output_directory'] if not os.path.exists(output_directory): os.mkdir(output_directory) + skip_screenshots = False if not os.path.exists(os.path.join(output_directory, "screenshots")): os.mkdir(os.path.join(output_directory, "screenshots")) else: @@ -116,10 +138,10 @@ def run(map_config): os.mkdir(os.path.join(output_directory, "tiles")) # Compute the optimal zoom level - usable_width = screen_config["width"] - 200 # Keep a margin around the center - usable_height = screen_config["height"] - 200 # Keep a margin around the center + usable_width = screen_config['width'] - 400 # Keep a margin around the center + usable_height = screen_config['height'] - 400 # Keep a margin around the center - with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp: + with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp: # Read the config file doc = bp.read() k = kml.KML() @@ -134,7 +156,8 @@ def run(map_config): # Iterate over all the closed features in the kml file f = 1 for feature in features: - geo = sub_feature.geometry + ########### Take screenshots + geo = feature.geometry # Define the boundary rect around the area start_lat = geo.bounds[3] @@ -143,15 +166,14 @@ def run(map_config): end_lng = geo.bounds[2] # Find the zoom level that better approximates the provided resolution - screen_resolution = screen_config['geo_resolution'] - mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - screen_resolution) for z in range(0, 21)] + mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - map_config['mpps']) for z in range(0, 21)] zoom = mpps_delta.index(min(mpps_delta)) print(f"Feature {f} of {len(features)}, using zoom level {zoom}") # Find the maximum dimension of the tiles at the given resolution mpps = compute_mpps(end_lat, zoom) - d = 256 * mpps / screen_resolution + d = 256 * mpps / map_config['mpps'] n_height = math.floor(usable_height / d) n_width = math.floor(usable_width / d) @@ -163,35 +185,60 @@ def run(map_config): end_X, end_Y = deg_to_num(end_lat, end_lng, zoom) # Find all the X, Y coordinates inside of the provided area - screenshots_coordinates = [] + screenshots_XY = [] for X in range(start_X, end_X, n_width): for Y in range(start_Y, end_Y, n_height): lat, lng = num_to_deg(X, Y, zoom) p = Point(lng, lat) if p.within(wkt.loads(geo.wkt)): - screenshots_coordinates.append((lat, lng)) + screenshots_XY.append((X, Y)) - print(f"Feature {f} of {len(features)}, {len(screenshots_coordinates)} screenshots will be taken") + print(f"Feature {f} of {len(features)}, {len(screenshots_XY)} screenshots will be taken") # Start looping if not skip_screenshots: print(f"Feature {f} of {len(features)}, taking screenshots...") n = 0 - for coords in screenshots_coordinates: + for XY in screenshots_XY: # Making PUT request - #data = json.dumps({'lat': coords[0], 'lng': coords[1]}) - #r = requests.put('http://localhost:8080', data = data) + # If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile! + lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom) + data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)}) + r = requests.put('http://localhost:8080', data = data) + + geo_data = json.loads(r.text) time.sleep(0.1) - ## Take and save screenshot + # Take and save screenshot. The response to the put request contains data, among which there is the north rotation at that point. screenshot = pyautogui.screenshot() - screenshot.save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")) - printProgressBar(n + 1, len(screenshots_coordinates)) + # Scale the screenshot to account for Mercator Map Deformation + lat1, lng1 = num_to_deg(XY[0], XY[1], zoom) + lat2, lng2 = num_to_deg(XY[0] + 1, XY[1] + 1, zoom) + + deltaLat = abs(lat2 - lat1) + deltaLng = abs(lng2 - lng1) + + # Compute the height and width the screenshot should have + m_height = math.radians(deltaLat) * R * n_height + m_width = math.radians(deltaLng) * R * math.cos(math.radians(lat1)) * n_width + + # Compute the height and width the screenshot has + s_height = map_config['mpps'] * 256 * n_height + s_width = map_config['mpps'] * 256 * n_width + + # Compute the scaling required to achieve that + sx = s_width / m_width + sy = s_height / m_height + + # Resize, rotate and save the screenshot + screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"), quality=95) + + printProgressBar(n + 1, len(screenshots_XY)) n += 1 - # Extract the tiles + ########### Extract the tiles if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom))): os.mkdir(os.path.join(output_directory, "tiles", str(zoom))) @@ -201,15 +248,12 @@ def run(map_config): "output_directory": output_directory, "n_width": n_width, "n_height": n_height, - "screen_resolution": screen_resolution, - "mpps": mpps } # Extract the tiles with parallel thread execution with futures.ThreadPoolExecutor() as executor: print(f"Feature {f} of {len(features)}, extracting tiles...") - global tot_futs, fut_counter - futs = [executor.submit(extract_tiles, n, screenshots_coordinates, params) for n in range(0, len(screenshots_coordinates))] + futs = [executor.submit(extract_tiles, n, screenshots_XY, params) for n in range(0, len(screenshots_XY))] tot_futs = len(futs) fut_counter = 0 [fut.add_done_callback(done_callback) for fut in futs] @@ -219,6 +263,36 @@ def run(map_config): print(f"Feature {f} of {len(features)} completed!") f += 1 + ########### Assemble tiles to get lower zoom levels + for current_zoom in range(zoom, 8, -1): + Xs = [int(d) for d in listdir(os.path.join(output_directory, "tiles", str(current_zoom))) if isdir(join(output_directory, "tiles", str(current_zoom), d))] + existing_tiles = [] + for X in Xs: + Ys = [int(f.removesuffix(".jpg")) for f in listdir(os.path.join(output_directory, "tiles", str(current_zoom), str(X))) if isfile(join(output_directory, "tiles", str(current_zoom), str(X), f))] + for Y in Ys: + existing_tiles.append((X, Y)) + + tiles_to_produce = [] + for tile in existing_tiles: + if (int(tile[0] / 2), int(tile[1] / 2)) not in tiles_to_produce: + tiles_to_produce.append((int(tile[0] / 2), int(tile[1] / 2))) + + # Merge the tiles with parallel thread execution + with futures.ThreadPoolExecutor() as executor: + print(f"Merging tiles for zoom level {current_zoom - 1}...") + + if not os.path.exists(os.path.join(output_directory, "tiles", str(current_zoom - 1))): + os.mkdir(os.path.join(output_directory, "tiles", str(current_zoom - 1))) + + futs = [executor.submit(merge_tiles, os.path.join(output_directory, "tiles"), current_zoom, tile) for tile in tiles_to_produce] + tot_futs = len(futs) + fut_counter = 0 + [fut.add_done_callback(done_callback) for fut in futs] + [fut.result() for fut in futures.as_completed(futs)] + + + + From 05e0cc393a826f0d0f823e6532333d05525afd2a Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Tue, 27 Feb 2024 16:46:11 +0100 Subject: [PATCH 06/10] Complete camera control UI --- .../public/stylesheets/other/toolbar.css | 2 +- .../public/stylesheets/panels/unitcontrol.css | 2 +- .../server/public/stylesheets/style/style.css | 189 ++++++++++-------- .../olympus/images/buttons/camera/linked.svg | 36 ++++ .../images/buttons/visibility/airbase.svg | 11 +- .../images/buttons/visibility/aircraft.svg | 2 +- .../buttons/visibility/groundunit-sam.svg | 6 +- .../images/buttons/visibility/groundunit.svg | 4 +- .../visibility/head-side-virus-solid.svg | 1 - .../images/buttons/visibility/helicopter.svg | 3 +- .../images/buttons/visibility/navyunit.svg | 2 +- .../images/buttons/visibility/olympus.svg | 1 + .../themes/olympus/images/icons/camera.svg | 34 ++++ .../server/views/toolbars/commandmode.ejs | 3 +- frontend/server/views/toolbars/primary.ejs | 49 ++++- frontend/website/src/constants/constants.ts | 3 +- frontend/website/src/controls/dropdown.ts | 7 + frontend/website/src/map/map.ts | 109 ++++++++-- .../website/src/mission/missionmanager.ts | 4 +- frontend/website/src/other/utils.ts | 46 ++++- .../website/src/toolbars/primarytoolbar.ts | 7 + frontend/website/src/unit/unit.ts | 2 +- scripts/python/http_example.py | 23 +++ 23 files changed, 423 insertions(+), 123 deletions(-) create mode 100644 frontend/server/public/themes/olympus/images/buttons/camera/linked.svg delete mode 100644 frontend/server/public/themes/olympus/images/buttons/visibility/head-side-virus-solid.svg create mode 100644 frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg create mode 100644 frontend/server/public/themes/olympus/images/icons/camera.svg create mode 100644 scripts/python/http_example.py diff --git a/frontend/server/public/stylesheets/other/toolbar.css b/frontend/server/public/stylesheets/other/toolbar.css index 8c3ec441..5fdf1490 100644 --- a/frontend/server/public/stylesheets/other/toolbar.css +++ b/frontend/server/public/stylesheets/other/toolbar.css @@ -64,7 +64,7 @@ width: 20px; } -#toolbar-container>*:nth-child(3)>svg { +#command-mode-toolbar>svg { display: none; } diff --git a/frontend/server/public/stylesheets/panels/unitcontrol.css b/frontend/server/public/stylesheets/panels/unitcontrol.css index 1040ac3b..a5f41c02 100644 --- a/frontend/server/public/stylesheets/panels/unitcontrol.css +++ b/frontend/server/public/stylesheets/panels/unitcontrol.css @@ -330,7 +330,7 @@ body.feature-forceShowUnitControlPanel #unit-control-panel { #advanced-settings-div>button { background-color: var(--background-grey); - box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.25); + /*box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.25);*/ font-size: 13px; height: 40px; padding: 0 20px; diff --git a/frontend/server/public/stylesheets/style/style.css b/frontend/server/public/stylesheets/style/style.css index b1bb100b..3ce629ca 100644 --- a/frontend/server/public/stylesheets/style/style.css +++ b/frontend/server/public/stylesheets/style/style.css @@ -177,7 +177,7 @@ button svg.fill-coalition[data-coalition="red"] * { .ol-select>.ol-select-value { align-content: center; - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + /*box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);*/ cursor: pointer; display: flex; justify-content: left; @@ -279,7 +279,7 @@ button svg.fill-coalition[data-coalition="red"] * { } .ol-select>.ol-select-options div hr { - background-color: white; + background-color: var(--background-hover); height: 1px; width: 100%; } @@ -408,10 +408,8 @@ nav.ol-panel { display: flex; flex-direction: row; height: 58px; -} - -nav.ol-panel> :last-child { - margin-right: 5px; + padding-left: 15px; + padding-right: 15px; } .ol-panel .ol-group { @@ -654,9 +652,9 @@ nav.ol-panel> :last-child { align-items: center; } -.ol-navbar-buttons-group > div { +.ol-navbar-buttons-group>div { align-items: center; - display:flex; + display: flex; flex-direction: row; } @@ -683,9 +681,16 @@ nav.ol-panel> :last-child { border: 1px solid white; } -.ol-navbar-buttons-group button.off svg * { - fill: white !important; /* Higher price than the Soul Stone but inline styling is causing issues. */ - stroke: white !important; /* I'm sorry, daughter. */ +.ol-navbar-buttons-group button.off svg *[fill="black"] { + fill: white !important; +} + +.ol-navbar-buttons-group button.off svg *[stroke="black"] { + stroke: white !important; +} + +.ol-navbar-buttons-group button.off.red svg *[fill="black"] { + fill: red !important; } .ol-navbar-buttons-group button svg * { @@ -696,53 +701,54 @@ nav.ol-panel> :last-child { .ol-navbar-buttons-group .protectable button:first-of-type { border-bottom-right-radius: 0; border-top-right-radius: 0; - width:28px; + width: 28px; } -.ol-navbar-buttons-group > .protectable > button.lock { +.ol-navbar-buttons-group>.protectable>button.lock { align-items: center; background-color: var(--primary-red); border-bottom-left-radius: 0; border-top-left-radius: 0; - display:flex; + display: flex; justify-content: center; - width:18px; + width: 18px; } -.ol-navbar-buttons-group > .protectable > button[data-protected].lock { +.ol-navbar-buttons-group>.protectable>button[data-protected].lock { background-color: var(--background-grey); } -.ol-navbar-buttons-group > .protectable > button.lock svg { - height:10px; - width:10px; +.ol-navbar-buttons-group>.protectable>button.lock svg { + height: 10px; + width: 10px; } @keyframes lock-prompt { 100% { opacity: 1; } + 0% { opacity: 0; } } -.ol-navbar-buttons-group > .protectable > button[data-protected].lock.prompt svg { +.ol-navbar-buttons-group>.protectable>button[data-protected].lock.prompt svg { animation: lock-prompt .25s alternate infinite; } -.ol-navbar-buttons-group > .protectable > button.lock svg.locked * { - fill:white !important; +.ol-navbar-buttons-group>.protectable>button.lock svg.locked * { + fill: white !important; } -.ol-navbar-buttons-group > .protectable > button:not([data-protected]).lock svg.unlocked, -.ol-navbar-buttons-group > .protectable > button[data-protected].lock svg.locked { - display:flex; +.ol-navbar-buttons-group>.protectable>button:not([data-protected]).lock svg.unlocked, +.ol-navbar-buttons-group>.protectable>button[data-protected].lock svg.locked { + display: flex; } -.ol-navbar-buttons-group > .protectable > button[data-protected].lock svg.unlocked, -.ol-navbar-buttons-group > .protectable > button:not([data-protected]).lock svg.locked { - display:none; +.ol-navbar-buttons-group>.protectable>button[data-protected].lock svg.unlocked, +.ol-navbar-buttons-group>.protectable>button:not([data-protected]).lock svg.locked { + display: none; } @@ -750,8 +756,7 @@ nav.ol-panel> :last-child { #roe-buttons-container button, #reaction-to-threat-buttons-container button, #emissions-countermeasures-buttons-container button, -#shots-scatter-buttons-container button -#shots-intensity-buttons-container button { +#shots-scatter-buttons-container button #shots-intensity-buttons-container button { align-items: center; background-color: transparent; border: 1px solid var(--accent-light-blue); @@ -834,7 +839,7 @@ nav.ol-panel> :last-child { width: auto; } } - + #splash-content::after { background-color: var(--background-steel); content: ""; @@ -1030,7 +1035,7 @@ nav.ol-panel> :last-child { font-size: 14px; font-weight: bolder; padding-left: 10px; - margin-left: -11px; + margin-left: -16px; margin-top: -0px; margin-bottom: -0px; height: 58px; @@ -1068,15 +1073,8 @@ nav.ol-panel> :last-child { #spawn-points-container { height: 100%; - border-right: 1px solid gray; display: flex; align-items: center; - padding-right: 20px; -} - -#command-mode-phase::before { - content: "Time to start"; - font-size: 14px; } #command-mode-phase.setup-phase::after { @@ -1085,7 +1083,6 @@ nav.ol-panel> :last-child { border-radius: 999px; padding: 5px 10px; background-color: var(--background-grey); - margin-left: 15px; content: attr(data-remaining-time); font-size: 14px; } @@ -1099,20 +1096,17 @@ nav.ol-panel> :last-child { display: flex; flex-direction: column; align-items: center; -} - -#command-mode-phase.game-commenced::before { - content: "Game commenced"; - font-weight: bold; + justify-content: center; + height: 100%; } #command-mode-phase.game-commenced::after { - content: "Spawn restrictions are being enforced"; - font-size: 10px; + content: "Spawn restrictions on"; + font-size: 12px; } #command-mode-phase.no-restrictions::after { - content: "No spawn restrictions"; + content: "Spawn restrictions on"; font-size: 10px; } @@ -1327,8 +1321,8 @@ dl.ol-data-grid dd { .ol-dialog-content table th { background-color: var(--background-grey); - color:white; - font-size:14px; + color: white; + font-size: 14px; font-weight: normal; } @@ -1349,7 +1343,8 @@ dl.ol-data-grid dd { overflow-y: auto; } -.ol-checkbox label { +.ol-checkbox label, +.ol-text-input label { align-items: center; cursor: pointer; display: flex; @@ -1357,6 +1352,11 @@ dl.ol-data-grid dd { white-space: nowrap; } +.ol-text-input label { + justify-content: space-between; + width: 100%; +} + .ol-checkbox input[type="checkbox"] { appearance: none; background-color: transparent; @@ -1388,16 +1388,19 @@ dl.ol-data-grid dd { .ol-text-input input { background-color: var(--background-grey); border: 1px solid var(--background-grey); - border-radius: 5px; border-radius: var(--border-radius-sm); - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); + /*box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);*/ color: var(--background-offwhite); height: 32px; text-align: center; } +.ol-text-input.border input { + border: 1px solid var(--background-offwhite); +} + .ol-text-input input[disabled] { - color:var(--ol-dialog-disabled-text-color); + color: var(--ol-dialog-disabled-text-color); } input[type=number] { @@ -1441,7 +1444,7 @@ input[type=number]::-webkit-outer-spin-button { .ol-button-apply[disabled] { border-color: var(--ol-dialog-disabled-text-color); - color:var(--ol-dialog-disabled-text-color); + color: var(--ol-dialog-disabled-text-color); } .ol-button-apply::before { @@ -1525,49 +1528,49 @@ input[type=number]::-webkit-outer-spin-button { } .switch-control.yes-no .ol-switch[data-value="false"] .ol-switch-fill { - background-color: var(--ol-switch-off); + background-color: var(--ol-switch-off); } .switch-control.yes-no .ol-switch[data-value="undefined"] .ol-switch-fill { - background-color: var(--ol-switch-undefined); + background-color: var(--ol-switch-undefined); } .switch-control.coalition .ol-switch>.ol-switch-fill::before, .switch-control.yes-no .ol-switch>.ol-switch-fill::before { - translate:-100% 0; + translate: -100% 0; transform: none; } .switch-control.yes-no .ol-switch[data-value="true"]>.ol-switch-fill::before { - content: "YES"; + content: "YES"; } .switch-control.yes-no .ol-switch[data-value="false"]>.ol-switch-fill::before { - content: "NO"; + content: "NO"; } .switch-control.coalition [data-value="true"] .ol-switch-fill { - background-color: var(--primary-blue); + background-color: var(--primary-blue); } .switch-control.coalition [data-value="false"] .ol-switch-fill { - background-color: var(--primary-red); + background-color: var(--primary-red); } .switch-control.coalition [data-value="undefined"] .ol-switch-fill { - background-color: var(--primary-neutral); + background-color: var(--primary-neutral); } .switch-control.coalition [data-value="true"] .ol-switch-fill::before { - content: "BLUE"; + content: "BLUE"; } .switch-control.coalition [data-value="false"] .ol-switch-fill::before { - content: "RED"; + content: "RED"; } .switch-control.no-label [data-value] .ol-switch-fill::before { - content:""; + content: ""; } .ol-context-menu>ul { @@ -1628,17 +1631,17 @@ input[type=number]::-webkit-outer-spin-button { } #map-visibility-options .ol-select-options .ol-checkbox { - font-size:13px; - font-weight:400; - padding:6px 15px; + font-size: 13px; + font-weight: 400; + padding: 6px 15px; } #map-visibility-options .ol-select-options .ol-checkbox:first-of-type { - padding-top:12px; + padding-top: 12px; } #map-visibility-options .ol-select-options .ol-checkbox:last-of-type { - padding-bottom:12px; + padding-bottom: 18px; } #map-visibility-options .ol-select-options .ol-checkbox label:hover span { @@ -1658,19 +1661,19 @@ input[type=number]::-webkit-outer-spin-button { } .file-import-export .ol-dialog-content { - display:flex; + display: flex; flex-direction: column; justify-content: center; } .file-import-export p { background-color: var(--background-grey); - border-left:6px solid var(--secondary-blue-text); - padding:12px; + border-left: 6px solid var(--secondary-blue-text); + padding: 12px; } .file-import-export th { - padding:4px 6px; + padding: 4px 6px; } .file-import-export tr td:first-child { @@ -1678,12 +1681,12 @@ input[type=number]::-webkit-outer-spin-button { } .file-import-export td { - color:white; + color: white; text-align: center; } .file-import-export .ol-checkbox { - display:flex; + display: flex; justify-content: center; } @@ -1692,7 +1695,7 @@ input[type=number]::-webkit-outer-spin-button { } .file-import-export .ol-checkbox span { - display:none; + display: none; } .file-import-export button.start-transfer { @@ -1732,7 +1735,7 @@ input[type=number]::-webkit-outer-spin-button { pointer-events: none; } -.file-import-export .ol-dialog-footer button:first-of-type{ +.file-import-export .ol-dialog-footer button:first-of-type { margin-left: auto; } @@ -1742,3 +1745,31 @@ input[type=number]::-webkit-outer-spin-button { } } +#camera-link-type-switch { + width: 60px; + height: 25px; +} + +#camera-link-type-switch[data-value="true"]>.ol-switch-fill::before { + content: "MAP"; +} + +#camera-link-type-switch[data-value="false"]>.ol-switch-fill::before { + content: "LIVE"; +} + +#camera-link-type-switch[data-value="true"]>.ol-switch-fill { + background-color: var(--background-grey); +} + +#camera-link-type-switch[data-value="false"]>.ol-switch-fill { + background-color: var(--background-offwhite); +} + +#camera-link-type-switch[data-value="false"]>.ol-switch-fill::before { + color: var(--background-steel); +} + +#camera-link-type-switch[data-value="false"]>.ol-switch-fill::after { + background-color: var(--background-steel); +} diff --git a/frontend/server/public/themes/olympus/images/buttons/camera/linked.svg b/frontend/server/public/themes/olympus/images/buttons/camera/linked.svg new file mode 100644 index 00000000..86e90e32 --- /dev/null +++ b/frontend/server/public/themes/olympus/images/buttons/camera/linked.svg @@ -0,0 +1,36 @@ + + diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg index 28ad42a4..af684050 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/airbase.svg @@ -50,27 +50,26 @@ diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg index 09a9e322..593e7643 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/aircraft.svg @@ -36,6 +36,6 @@ diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg index 2a568fbb..3198968b 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/groundunit-sam.svg @@ -38,12 +38,12 @@ \ No newline at end of file diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg index d6eebd8a..c333457b 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/helicopter.svg @@ -52,5 +52,6 @@ inkscape:connector-curvature="0" d="m 3.173155,1.0409694 c 0,-0.46651814 0.3476271,-0.84342412 0.7779063,-0.84342412 h 9.3348367 c 0.430278,0 0.777906,0.37690598 0.777906,0.84342412 0,0.4665187 -0.347628,0.8434202 -0.777906,0.8434202 H 9.3963807 V 3.571233 h 0.7779053 c 2.148956,0 3.889518,1.8871557 3.889518,4.2171087 v 1.686841 c 0,0.4665172 -0.347628,0.8434233 -0.777906,0.8434233 H 9.3963807 7.8405774 c -0.4886243,0 -0.9505011,-0.250391 -1.2446479,-0.6747365 L 4.8602324,7.1346873 C 4.7751488,7.0108098 4.6584662,6.9159274 4.5271922,6.8579429 L 1.2089503,5.4188529 C 0.97800953,5.318696 0.80298167,5.1025686 0.7422081,4.8389987 L 0.18309237,2.4088928 C 0.11988251,2.1426888 0.30706869,1.8843896 0.55988607,1.8843896 H 1.2283992 c 0.245524,0 0.4764649,0.1238773 0.6223221,0.3373706 L 2.7842066,3.571233 H 7.8405774 V 1.8843896 H 3.9510613 c -0.4302792,0 -0.7779063,-0.3769015 -0.7779063,-0.8434202 z m 6.2232257,7.5907923 h 3.1116153 v -0.84342 c 0,-1.3969171 -1.045307,-2.5302639 -2.33371,-2.5302639 H 9.3963807 Z m 5.9947173,2.7780193 c 0.303871,0.329462 0.303871,0.864511 0,1.193968 l -0.09481,0.102794 c -0.583429,0.632565 -1.375917,0.988385 -2.200006,0.988385 H 6.2847694 c -0.4302775,0 -0.7779054,-0.376906 -0.7779054,-0.843421 0,-0.466518 0.3476279,-0.843424 0.7779054,-0.843424 h 6.8115166 c 0.413259,0 0.809506,-0.176591 1.101221,-0.492874 l 0.09481,-0.102793 c 0.30387,-0.329461 0.79735,-0.329461 1.101219,0 z" id="path1174-3" - style="fill:#202831;fill-opacity:1;stroke-width:0.02531246" /> + style="fill-opacity:1;stroke-width:0.02531246" + fill="black"/> diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg index d4ff6fec..87f20c12 100644 --- a/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/navyunit.svg @@ -36,6 +36,6 @@ diff --git a/frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg b/frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg new file mode 100644 index 00000000..c13a36a3 --- /dev/null +++ b/frontend/server/public/themes/olympus/images/buttons/visibility/olympus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/server/public/themes/olympus/images/icons/camera.svg b/frontend/server/public/themes/olympus/images/icons/camera.svg new file mode 100644 index 00000000..71f65572 --- /dev/null +++ b/frontend/server/public/themes/olympus/images/icons/camera.svg @@ -0,0 +1,34 @@ + + diff --git a/frontend/server/views/toolbars/commandmode.ejs b/frontend/server/views/toolbars/commandmode.ejs index 1954bc66..69000053 100644 --- a/frontend/server/views/toolbars/commandmode.ejs +++ b/frontend/server/views/toolbars/commandmode.ejs @@ -3,5 +3,6 @@
Spawn points
- + \ No newline at end of file diff --git a/frontend/server/views/toolbars/primary.ejs b/frontend/server/views/toolbars/primary.ejs index 70e130b8..7b1fbe3f 100644 --- a/frontend/server/views/toolbars/primary.ejs +++ b/frontend/server/views/toolbars/primary.ejs @@ -6,7 +6,8 @@

DCS Olympus

-
version {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}}
+
version {{OLYMPUS_VERSION_NUMBER}}.{{OLYMPUS_COMMIT_HASH}} +
Discord @@ -31,14 +32,16 @@
-
ArcGIS Satellite
+
ArcGIS Satellite
-
Options
+
Options +
@@ -57,17 +60,51 @@
+ data-on-click-params='{ "coalition": "blue" }' title="Toggle Blue coalition visibility">
+ data-on-click-params='{ "coalition": "red" }' title="Toggle Red coalition visibility">
+ data-on-click-params='{ "coalition": "neutral" }' title="Toggle Neutral coalition visibility">
+ + \ No newline at end of file diff --git a/frontend/website/src/constants/constants.ts b/frontend/website/src/constants/constants.ts index fc96f47a..a0fa35cf 100644 --- a/frontend/website/src/constants/constants.ts +++ b/frontend/website/src/constants/constants.ts @@ -207,7 +207,7 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{ "toggles": ["human"], "tooltip": "Toggle human players' visibility" }, { - "image": "visibility/head-side-virus-solid.svg", + "image": "visibility/olympus.svg", "isProtected": false, "name": "Olympus", "protectable": false, @@ -268,6 +268,7 @@ export const FILL_SELECTED_RING = "Fill the threat range rings of selected units export const SHOW_UNIT_CONTACTS = "Show selected units contact lines"; export const SHOW_UNIT_PATHS = "Show selected unit paths"; export const SHOW_UNIT_TARGETS = "Show selected unit targets"; +export const DCS_LINK_PORT = "DCS Camera link port"; export enum DataIndexes { startOfData = 0, diff --git a/frontend/website/src/controls/dropdown.ts b/frontend/website/src/controls/dropdown.ts index 38014dc9..99694154 100644 --- a/frontend/website/src/controls/dropdown.ts +++ b/frontend/website/src/controls/dropdown.ts @@ -117,6 +117,13 @@ export class Dropdown { this.#options.appendChild(optionElement); } + addHorizontalDivider() { + let div = document.createElement("div"); + let hr = document.createElement('hr'); + div.appendChild(hr); + this.#options.appendChild(div); + } + /** Select the active value of the dropdown * * @param idx The index of the element to select diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index 4c82614c..6629c59c 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -7,12 +7,12 @@ import { AirbaseContextMenu } from "../contextmenus/airbasecontextmenu"; import { Dropdown } from "../controls/dropdown"; import { Airbase } from "../mission/airbase"; import { Unit } from "../unit/unit"; -import { bearing, createCheckboxOption, deg2rad, getGroundElevation, polyContains } from "../other/utils"; +import { bearing, createCheckboxOption, createTextInputOption, deg2rad, getGroundElevation, polyContains } from "../other/utils"; import { DestinationPreviewMarker } from "./markers/destinationpreviewmarker"; import { TemporaryUnitMarker } from "./markers/temporaryunitmarker"; import { ClickableMiniMap } from "./clickableminimap"; import { SVGInjector } from '@tanem/svg-injector' -import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS } from "../constants/constants"; +import { defaultMapLayers, mapBounds, minimapBoundaries, IDLE, COALITIONAREA_DRAW_POLYGON, MOVE_UNIT, SHOW_UNIT_CONTACTS, HIDE_GROUP_MEMBERS, SHOW_UNIT_PATHS, SHOW_UNIT_TARGETS, SHOW_UNIT_LABELS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNITS_ACQUISITION_RINGS, HIDE_UNITS_SHORT_RANGE_RINGS, FILL_SELECTED_RING, MAP_MARKER_CONTROLS, DCS_LINK_PORT } from "../constants/constants"; import { CoalitionArea } from "./coalitionarea/coalitionarea"; import { CoalitionAreaContextMenu } from "../contextmenus/coalitionareacontextmenu"; import { DrawingCursor } from "./coalitionarea/drawingcursor"; @@ -70,6 +70,11 @@ export class Map extends L.Map { #selecting: boolean = false; #isZooming: boolean = false; #previousZoom: number = 0; + #slaveDCSCamera: boolean = false; + #slaveDCSCameraAvailable: boolean = false; + #cameraControlTimer: number = 0; + #cameraControlPort: number = 3003; + #cameraControlMode: string = 'map'; #destinationGroupRotation: number = 0; #computeDestinationRotation: boolean = false; @@ -94,7 +99,7 @@ export class Map extends L.Map { #mapMarkerVisibilityControls: MapMarkerVisibilityControl[] = MAP_MARKER_CONTROLS; #mapVisibilityOptionsDropdown: Dropdown; #optionButtons: { [key: string]: HTMLButtonElement[] } = {} - #visibilityOptions: { [key: string]: boolean } = {} + #visibilityOptions: { [key: string]: boolean | string | number } = {} #hiddenTypes: string[] = []; /** @@ -158,7 +163,7 @@ export class Map extends L.Map { this.on('drag', (e: any) => this.#onMouseMove(e)); this.on('keydown', (e: any) => this.#onKeyDown(e)); this.on('keyup', (e: any) => this.#onKeyUp(e)); - this.on('move', (e: any) => this.#broadcastPosition(e)); + this.on('move', (e: any) => { if (this.#slaveDCSCamera) this.#broadcastPosition() }); /* Event listeners */ document.addEventListener("toggleCoalitionVisibility", (ev: CustomEventInit) => { @@ -202,6 +207,7 @@ export class Map extends L.Map { document.addEventListener("mapOptionsChanged", () => { this.getContainer().toggleAttribute("data-hide-labels", !this.getVisibilityOptions()[SHOW_UNIT_LABELS]); + this.#cameraControlPort = this.getVisibilityOptions()[DCS_LINK_PORT] as number; }); document.addEventListener("configLoaded", () => { @@ -216,6 +222,18 @@ export class Map extends L.Map { } }) + document.addEventListener("toggleCameraLinkStatus", () => { + if (this.#slaveDCSCameraAvailable) { + this.setSlaveDCSCamera(!this.#slaveDCSCamera); + } + }) + + document.addEventListener("slewCameraToPosition", () => { + if (this.#slaveDCSCameraAvailable) { + this.#broadcastPosition(); + } + }) + /* Pan interval */ this.#panInterval = window.setInterval(() => { if (this.#panUp || this.#panDown || this.#panRight || this.#panLeft) @@ -223,10 +241,19 @@ export class Map extends L.Map { ((this.#panUp ? -1 : 0) + (this.#panDown ? 1 : 0)) * this.#deafultPanDelta * (this.#shiftKey ? 3 : 1))); }, 20); + /* Periodically check if the camera control endpoint is available */ + this.#cameraControlTimer = window.setInterval(() => { + this.#checkCameraPort(); + }, 1000) + /* Option buttons */ this.#createUnitMarkerControlButtons(); /* Create the checkboxes to select the advanced visibility options */ + this.addVisibilityOption(DCS_LINK_PORT, 3003, { min: 1024, max: 65535 }); + + this.#mapVisibilityOptionsDropdown.addHorizontalDivider(); + this.addVisibilityOption(SHOW_UNIT_CONTACTS, false); this.addVisibilityOption(HIDE_GROUP_MEMBERS, true); this.addVisibilityOption(SHOW_UNIT_PATHS, true); @@ -238,9 +265,14 @@ export class Map extends L.Map { /* this.addVisibilityOption(FILL_SELECTED_RING, false); Removed since currently broken: TODO fix!*/ } - addVisibilityOption(option: string, defaultValue: boolean) { + addVisibilityOption(option: string, defaultValue: boolean | number | string, options?: { [key: string]: any }) { this.#visibilityOptions[option] = defaultValue; - this.#mapVisibilityOptionsDropdown.addOptionElement(createCheckboxOption(option, option, defaultValue, (ev: any) => { this.#setVisibilityOption(option, ev); })); + if (typeof defaultValue === 'boolean') + this.#mapVisibilityOptionsDropdown.addOptionElement(createCheckboxOption(option, option, defaultValue as boolean, (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); + else if (typeof defaultValue === 'number') + this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue.toString(), 'number', (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); + else + this.#mapVisibilityOptionsDropdown.addOptionElement(createTextInputOption(option, option, defaultValue, 'text', (ev: any) => { this.#setVisibilityOption(option, ev); }, options)); } setLayer(layerName: string) { @@ -538,6 +570,21 @@ export class Map extends L.Map { return this.#mapMarkerVisibilityControls; } + setSlaveDCSCamera(newSlaveDCSCamera: boolean) { + if (this.#slaveDCSCameraAvailable || !newSlaveDCSCamera) { + this.#slaveDCSCamera = newSlaveDCSCamera; + let button = document.getElementById("camera-link-control"); + button?.classList.toggle("off", !newSlaveDCSCamera); + if (newSlaveDCSCamera) + this.#broadcastPosition(); + } + } + + setCameraControlMode(newCameraControlMode: string) { + this.#cameraControlMode = newCameraControlMode; + this.#broadcastPosition(); + } + /* Event handlers */ #onClick(e: any) { if (!this.#preventLeftClick) { @@ -720,27 +767,26 @@ export class Map extends L.Map { this.#isZooming = false; } - #broadcastPosition(e: any) { + #broadcastPosition() { getGroundElevation(this.getCenter(), (response: string) => { var groundElevation: number | null = null; try { groundElevation = parseFloat(response); var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("PUT", "http://localhost:8080"); + xmlHttp.open("PUT", `http://localhost:${this.#cameraControlPort}`); xmlHttp.setRequestHeader("Content-Type", "application/json"); - const C = 40075016.686; + const C = 40075016.686; let mpp = C * Math.cos(deg2rad(this.getCenter().lat)) / Math.pow(2, this.getZoom() + 8); let d = mpp * 1920; let alt = d / 2 * 1 / Math.tan(deg2rad(40)); if (alt > 100000) alt = 100000; - xmlHttp.send(JSON.stringify({lat: this.getCenter().lat, lng: this.getCenter().lng, alt: alt + groundElevation})); + xmlHttp.send(JSON.stringify({ lat: this.getCenter().lat, lng: this.getCenter().lng, alt: alt + groundElevation, mode: this.#cameraControlMode })); } catch { console.warn("broadcastPosition: could not retrieve ground elevation") } }); - } /* */ @@ -909,8 +955,47 @@ export class Map extends L.Map { } #setVisibilityOption(option: string, ev: any) { - this.#visibilityOptions[option] = ev.currentTarget.checked; + if (typeof this.#visibilityOptions[option] === 'boolean') + this.#visibilityOptions[option] = ev.currentTarget.checked; + else if (typeof this.#visibilityOptions[option] === 'number') + this.#visibilityOptions[option] = Number(ev.currentTarget.value); + else + this.#visibilityOptions[option] = ev.currentTarget.value; document.dispatchEvent(new CustomEvent("mapOptionsChanged")); } + + #setSlaveDCSCameraAvailable(newSlaveDCSCameraAvailable: boolean) { + this.#slaveDCSCameraAvailable = newSlaveDCSCameraAvailable; + let linkButton = document.getElementById("camera-link-control"); + if (linkButton) { + if (!newSlaveDCSCameraAvailable) { + this.setSlaveDCSCamera(false); + linkButton.classList.add("red"); + linkButton.title = "Camera link to DCS is not available"; + } else { + linkButton.classList.remove("red"); + linkButton.title = "Link/Unlink DCS camera with Olympus position"; + } + } + } + + #checkCameraPort(){ + var xmlHttp = new XMLHttpRequest(); + xmlHttp.open("OPTIONS", `http://localhost:${this.#cameraControlPort}`); + xmlHttp.onload = (res: any) => { + if (xmlHttp.status == 200) + this.#setSlaveDCSCameraAvailable(true); + else + this.#setSlaveDCSCameraAvailable(false); + }; + xmlHttp.onerror = (res: any) => { + this.#setSlaveDCSCameraAvailable(false); + } + xmlHttp.ontimeout = (res: any) => { + this.#setSlaveDCSCameraAvailable(false); + } + xmlHttp.timeout = 500; + xmlHttp.send(""); + } } diff --git a/frontend/website/src/mission/missionmanager.ts b/frontend/website/src/mission/missionmanager.ts index fb557e33..0cbe8232 100644 --- a/frontend/website/src/mission/missionmanager.ts +++ b/frontend/website/src/mission/missionmanager.ts @@ -118,8 +118,8 @@ export class MissionManager { } commandModePhaseEl.classList.toggle("setup-phase", this.#remainingSetupTime > 0 && this.getCommandModeOptions().restrictSpawns); - commandModePhaseEl.classList.toggle("game-commenced", this.#remainingSetupTime <= 0 || !this.getCommandModeOptions().restrictSpawns); - commandModePhaseEl.classList.toggle("no-restrictions", !this.getCommandModeOptions().restrictSpawns); + //commandModePhaseEl.classList.toggle("game-commenced", this.#remainingSetupTime <= 0 || !this.getCommandModeOptions().restrictSpawns); + //commandModePhaseEl.classList.toggle("no-restrictions", !this.getCommandModeOptions().restrictSpawns); } } } diff --git a/frontend/website/src/other/utils.ts b/frontend/website/src/other/utils.ts index 0476a610..f8e6d5ff 100644 --- a/frontend/website/src/other/utils.ts +++ b/frontend/website/src/other/utils.ts @@ -463,7 +463,7 @@ export function convertDateAndTimeToDate(dateAndTime: DateAndTime) { return new Date(year, month, date.Day, time.h, time.m, time.s); } -export function createCheckboxOption(value: string, text: string, checked: boolean = true, callback: CallableFunction = (ev: any) => {}, options?:any) { +export function createCheckboxOption(text: string, description: string, checked: boolean = true, callback: CallableFunction = (ev: any) => {}, options?:any) { options = { "disabled": false, "name": "", @@ -473,16 +473,15 @@ export function createCheckboxOption(value: string, text: string, checked: boole var div = document.createElement("div"); div.classList.add("ol-checkbox"); var label = document.createElement("label"); - label.title = text; + label.title = description; var input = document.createElement("input"); input.type = "checkbox"; input.checked = checked; input.name = options.name; input.disabled = options.disabled; input.readOnly = options.readOnly; - input.value = value; var span = document.createElement("span"); - span.innerText = value; + span.innerText = text; label.appendChild(input); label.appendChild(span); div.appendChild(label); @@ -503,6 +502,45 @@ export function getCheckboxOptions(dropdown: Dropdown) { return values; } +export function createTextInputOption(text: string, description: string, initialValue: string, type: string, callback: CallableFunction = (ev: any) => {}, options?:any) { + options = { + "disabled": false, + "name": "", + "readOnly": false, + ...options + }; + var div = document.createElement("div"); + div.classList.add("ol-text-input", "border"); + var label = document.createElement("label"); + label.title = description; + var input = document.createElement("input"); + input.type = type; + input.name = options.name; + input.disabled = options.disabled; + input.readOnly = options.readOnly; + if (options.min) + input.min = options.min; + if (options.max) + input.max = options.max; + input.value = initialValue; + input.style.width = "80px"; + var span = document.createElement("span"); + span.innerText = text; + label.appendChild(span); + label.appendChild(input); + div.appendChild(label); + input.onchange = (ev: any) => { + if (type === 'number') { + if (Number(input.max) && Number(ev.srcElement.value) > Number(input.max)) + input.value = input.max; + else if (Number(input.min) && Number(ev.srcElement.value) < Number(input.min)) + input.value = input.min; + } + callback(ev); + } + return div as HTMLElement; +} + export function getGroundElevation(latlng: LatLng, callback: CallableFunction) { /* Get the ground elevation from the server endpoint */ const xhr = new XMLHttpRequest(); diff --git a/frontend/website/src/toolbars/primarytoolbar.ts b/frontend/website/src/toolbars/primarytoolbar.ts index ad0aa625..a9d477c1 100644 --- a/frontend/website/src/toolbars/primarytoolbar.ts +++ b/frontend/website/src/toolbars/primarytoolbar.ts @@ -1,14 +1,21 @@ +import { getApp } from ".."; import { Dropdown } from "../controls/dropdown"; +import { Switch } from "../controls/switch"; import { Toolbar } from "./toolbar"; export class PrimaryToolbar extends Toolbar { #mainDropdown: Dropdown; + #cameraLinkTypeSwitch: Switch; constructor(ID: string) { super(ID); /* The content of the dropdown is entirely defined in the .ejs file */ this.#mainDropdown = new Dropdown("app-icon", () => { }); + + this.#cameraLinkTypeSwitch = new Switch("camera-link-type-switch", (value: boolean) => { + getApp().getMap().setCameraControlMode(value? 'map': 'live'); + }) } getMainDropdown() { diff --git a/frontend/website/src/unit/unit.ts b/frontend/website/src/unit/unit.ts index 6166ce41..ab8f8b9a 100644 --- a/frontend/website/src/unit/unit.ts +++ b/frontend/website/src/unit/unit.ts @@ -1654,7 +1654,7 @@ export class GroundUnit extends Unit { /* When we zoom past the grouping limit, grouping is enabled and the unit is a leader, we redraw the unit to apply any possible grouped marker */ checkZoomRedraw(): boolean { - return (this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] && + return (this.getIsLeader() && getApp().getMap().getVisibilityOptions()[HIDE_GROUP_MEMBERS] as boolean && (getApp().getMap().getZoom() >= GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() < GROUPING_ZOOM_TRANSITION || getApp().getMap().getZoom() < GROUPING_ZOOM_TRANSITION && getApp().getMap().getPreviousZoom() >= GROUPING_ZOOM_TRANSITION)) } diff --git a/scripts/python/http_example.py b/scripts/python/http_example.py new file mode 100644 index 00000000..5b857886 --- /dev/null +++ b/scripts/python/http_example.py @@ -0,0 +1,23 @@ +import socket +from email.utils import formatdate + +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.bind(('127.0.0.1', 3003)) +sock.listen(5) + +count = 0 +while True: + connection, address = sock.accept() + buf = connection.recv(1024) + print(buf.decode("utf-8")) + if "OPTIONS" in buf.decode("utf-8"): + resp = (f"""HTTP/1.1 200 OK\r\nDate: {formatdate(timeval=None, localtime=False, usegmt=True)}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n""".encode("utf-8")) + connection.send(resp) + if not "PUT" in buf.decode("utf-8"): + connection.close() + else: + resp = (f"""HTTP/1.1 200 OK\r\nDate: {formatdate(timeval=None, localtime=False, usegmt=True)}\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, GET, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n\r\n{{"Hi": "Wirts!"}}\r\n""".encode("utf-8")) + connection.send(resp) + connection.close() + + count += 1 \ No newline at end of file From 832568aa0044d063aa8450c2a080b0d8768c7e69 Mon Sep 17 00:00:00 2001 From: Pax1601 Date: Wed, 28 Feb 2024 18:58:40 +0100 Subject: [PATCH 07/10] Completed camera control server in lua and more work on merging algorithm --- frontend/website/src/map/map.ts | 7 +- scripts/lua/camera/OlympusCameraControl.lua | 214 ++++++++++++++++++ .../python/map_generator/.vscode/launch.json | 2 +- .../python/map_generator/airbases_to_kml.py | 37 +++ .../configs/Caucasus/HighResolution.yml | 5 + .../LowResolution.kml} | 47 +--- .../configs/Caucasus/LowResolution.yml | 5 + .../configs/Caucasus/MediumResolution.kml | 71 ++++++ .../configs/Caucasus/MediumResolution.yml | 5 + .../configs/Caucasus/airbases.json | 1 + .../configs/Caucasus/airbases.kml | 1 + .../map_generator/configs/LasVegas/config.yml | 6 - scripts/python/map_generator/main.py | 15 +- scripts/python/map_generator/map_generator.py | 52 +++-- 14 files changed, 403 insertions(+), 65 deletions(-) create mode 100644 scripts/lua/camera/OlympusCameraControl.lua create mode 100644 scripts/python/map_generator/airbases_to_kml.py create mode 100644 scripts/python/map_generator/configs/Caucasus/HighResolution.yml rename scripts/python/map_generator/configs/{LasVegas/boundary.kml => Caucasus/LowResolution.kml} (52%) create mode 100644 scripts/python/map_generator/configs/Caucasus/LowResolution.yml create mode 100644 scripts/python/map_generator/configs/Caucasus/MediumResolution.kml create mode 100644 scripts/python/map_generator/configs/Caucasus/MediumResolution.yml create mode 100644 scripts/python/map_generator/configs/Caucasus/airbases.json create mode 100644 scripts/python/map_generator/configs/Caucasus/airbases.kml delete mode 100644 scripts/python/map_generator/configs/LasVegas/config.yml diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index 6629c59c..33daa13f 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -582,7 +582,8 @@ export class Map extends L.Map { setCameraControlMode(newCameraControlMode: string) { this.#cameraControlMode = newCameraControlMode; - this.#broadcastPosition(); + if (this.#slaveDCSCamera) + this.#broadcastPosition(); } /* Event handlers */ @@ -773,7 +774,7 @@ export class Map extends L.Map { try { groundElevation = parseFloat(response); var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("PUT", `http://localhost:${this.#cameraControlPort}`); + xmlHttp.open("PUT", `http://127.0.0.1:${this.#cameraControlPort}`); xmlHttp.setRequestHeader("Content-Type", "application/json"); const C = 40075016.686; @@ -981,7 +982,7 @@ export class Map extends L.Map { #checkCameraPort(){ var xmlHttp = new XMLHttpRequest(); - xmlHttp.open("OPTIONS", `http://localhost:${this.#cameraControlPort}`); + xmlHttp.open("OPTIONS", `http://127.0.0.1:${this.#cameraControlPort}`); xmlHttp.onload = (res: any) => { if (xmlHttp.status == 200) this.#setSlaveDCSCameraAvailable(true); diff --git a/scripts/lua/camera/OlympusCameraControl.lua b/scripts/lua/camera/OlympusCameraControl.lua new file mode 100644 index 00000000..5fa4894c --- /dev/null +++ b/scripts/lua/camera/OlympusCameraControl.lua @@ -0,0 +1,214 @@ +local _prevLuaExportStart = LuaExportStart +local _prevLuaExportBeforeNextFrame = LuaExportBeforeNextFrame +local _prevLuaExportStop = LuaExportStop + +local server = nil +local port = 3003 +local headers = "Access-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: PUT, OPTIONS\r\nAccess-Control-Allow-Headers: *\r\nAccess-Control-Max-Age: 86400\r\nVary: Accept-Encoding, Origin\r\nKeep-Alive: timeout=2, max=100\r\nConnection: Keep-Alive\r\n\r\n" + +function startTCPServer() + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'Starting TCP Server') + package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua" + package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll" + + socket = require("socket") + + server = assert(socket.bind("127.0.0.1", port)) + if server then + server:setoption("tcp-nodelay", true) + server:settimeout(0) + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'TCP Server listening on port ' .. port) + else + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'TCP Server did not start successfully') + end +end + +function receiveTCP() + if server then + -- Accept a new connection without blocking + local client = server:accept() + + if client then + -- Set the timeout of the connection to 5ms + client:settimeout(0) + client:setoption("tcp-nodelay", true) + + local acc = "" + local data = "" + + -- Start receiving data, accumulate it in acc + while data ~= nil do + -- Receive a new line + data, err, partial = client:receive('*l') + if data then + -- If we receive an empty string it means the header section of the message is over + if data == "" then + -- Is this an OPTIONS request? + if string.find(acc, "OPTIONS") ~= nil then + client:send("HTTP/1.1 200 OK\r\n" .. headers) + client:close() + + -- Is this a PUT request? + elseif string.find(acc, "PUT") ~= nil then + -- Extract the length of the body + local contentLength = string.match(acc, "Content%-Length: (%d+)") + if contentLength ~= nil then + -- Receive the body + body, err, partial = client:receive(tonumber(contentLength)) + if body ~= nil then + local lat = string.match(body, '"lat":%s*([%+%-]?[%d%.]+)%s*[},]') + local lng = string.match(body, '"lng":%s*([%+%-]?[%d%.]+)%s*[},]') + local alt = string.match(body, '"alt":%s*([%+%-]?[%d%.]+)%s*[},]') + local mode = string.match(body, '"mode":%s*"(%a+)"%s*[},]') + + if lat ~= nil and lng ~= nil then + client:send("HTTP/1.1 200 OK\r\n" .. headers) + + local position = {} + position["lat"] = tonumber(lat) + position["lng"] = tonumber(lng) + if alt ~= nil then + position["alt"] = tonumber(alt) + end + + -- F11 view + if mode == "live" or mode == nil then + LoSetCommand(158) + -- F10 view + elseif mode == "map" then + LoSetCommand(15) + end + + client:send(setCameraPosition(position)) + client:close() + else + client:send("HTTP/1.1 500 ERROR\r\n" .. headers) + client:close() + end + else + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, err) + end + end + client:close() + break + end + else + -- Keep accumulating the incoming data + acc = acc .. " " .. data + end + end + end + end + end +end + +function stopTCPServer() + if server then + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.INFO, 'Stopping TCP Server') + server:close() + end + server = nil +end + +function setCameraPosition(position) + -- Get the old camera position + local oldPos = LoGetCameraPosition() + + -- Extract the commanded position + local point = LoGeoCoordinatesToLoCoordinates(position.lng, position.lat) + local pointNorth = LoGeoCoordinatesToLoCoordinates(position.lng, position.lat + 0.1) + + -- Compute the local map rotation and scale and send it back to the server + local rotation = math.atan2(pointNorth.z - point.z, pointNorth.x - point.x) + + -- If no altitude is provided, preserve the current camera altitude + local altitude = nil + if position.alt == nil then + altitude = oldPos.p.y + else + altitude = position.alt + end + + -- Set the camera position + local pos = + { + x = {x = 0, y = -1, z = 0}, + y = {x = 1, y = 0, z = 0}, + z = {x = 0, y = 0, z = 1}, + p = {x = point.x, y = altitude, z = point.z} + } + LoSetCameraPosition(pos) + + return '{"northRotation": ' .. rotation .. '}' +end + +LuaExportStart = function() + package.path = package.path..";"..lfs.currentdir().."/LuaSocket/?.lua" + package.cpath = package.cpath..";"..lfs.currentdir().."/LuaSocket/?.dll" + + startTCPServer() + + -- call original + if _prevLuaExportStart then + _status, _result = pcall(_prevLuaExportStart) + if not _status then + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportStart from another script', _result) + end + end +end + +LuaExportBeforeNextFrame = function() + receiveTCP() + + -- call original + if _prevLuaExportBeforeNextFrame then + _status, _result = pcall(_prevLuaExportBeforeNextFrame) + if not _status then + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportBeforeNextFrame from another script', _result) + end + end +end + +LuaExportStop = function() + stopTCPServer() + + -- call original + if _prevLuaExportStop then + _status, _result = pcall(_prevLuaExportStop) + if not _status then + log.write('OLYMPUSCAMERACONTROL.EXPORT.LUA', log.ERROR, 'ERROR Calling other LuaExportStop from another script', _result) + end + end +end + +function serializeTable(val, name, skipnewlines, depth) + skipnewlines = skipnewlines or false + depth = depth or 0 + + local tmp = string.rep(" ", depth) + if name then + if type(name) == "number" then + tmp = tmp .. "[" .. name .. "]" .. " = " + else + tmp = tmp .. name .. " = " + end + end + + if type(val) == "table" then + tmp = tmp .. "{" .. (not skipnewlines and "\n" or "") + for k, v in pairs(val) do + tmp = tmp .. serializeTable(v, k, skipnewlines, depth + 1) .. "," .. (not skipnewlines and "\n" or "") + end + tmp = tmp .. string.rep(" ", depth) .. "}" + elseif type(val) == "number" then + tmp = tmp .. tostring(val) + elseif type(val) == "string" then + tmp = tmp .. string.format("%q", val) + elseif type(val) == "boolean" then + tmp = tmp .. (val and "true" or "false") + else + tmp = tmp .. "\"[inserializeable datatype:" .. type(val) .. "]\"" + end + + return tmp +end \ No newline at end of file diff --git a/scripts/python/map_generator/.vscode/launch.json b/scripts/python/map_generator/.vscode/launch.json index acdc83b5..40a86611 100644 --- a/scripts/python/map_generator/.vscode/launch.json +++ b/scripts/python/map_generator/.vscode/launch.json @@ -10,7 +10,7 @@ "request": "launch", "program": "main.py", "console": "integratedTerminal", - "args": ["./configs/NTTR/config.yml"] + "args": ["./configs/Caucasus/HighResolution.yml"] } ] } \ No newline at end of file diff --git a/scripts/python/map_generator/airbases_to_kml.py b/scripts/python/map_generator/airbases_to_kml.py new file mode 100644 index 00000000..69e23881 --- /dev/null +++ b/scripts/python/map_generator/airbases_to_kml.py @@ -0,0 +1,37 @@ +import sys +from fastkml import kml +from pygeoif.geometry import Polygon +import json +import math + +# constants +C = 40075016.686 # meters, Earth equatorial circumference +R = C / (2 * math.pi) # meters, Earth equatorial radius +W = 10000 # meters, size of the square around the airbase + +if len(sys.argv) == 1: + print("Please provide a json file as first argument. You can also drop the json file on this script to run it.") +else: + input_file = sys.argv[1] + k = kml.KML() + ns = '{http://www.opengis.net/kml/2.2}' + + d = kml.Document(ns, 'docid', 'doc name', 'doc description') + k.append(d) + + with open(input_file) as jp: + j = json.load(jp) + + for point in j['airbases'].values(): + p = kml.Placemark(ns, 'id', 'name', 'description') + lat = point['latitude'] + lng = point['longitude'] + + latDelta = math.degrees(W / R) + lngDelta = math.degrees(W / (R * math.cos(math.radians(lat)))) + + p.geometry = Polygon([(lng - lngDelta, lat - latDelta), (lng - lngDelta, lat + latDelta), (lng + lngDelta, lat + latDelta), (lng + lngDelta, lat - latDelta)]) + d.append(p) + + with open(input_file.removesuffix('.json')+'.kml', 'w') as kp: + kp.writelines(k.to_string(prettyprint=True)) \ No newline at end of file diff --git a/scripts/python/map_generator/configs/Caucasus/HighResolution.yml b/scripts/python/map_generator/configs/Caucasus/HighResolution.yml new file mode 100644 index 00000000..135d85c9 --- /dev/null +++ b/scripts/python/map_generator/configs/Caucasus/HighResolution.yml @@ -0,0 +1,5 @@ +{ + 'output_directory': '.\Caucasus', # Where to save the output files + 'boundary_file': '.\configs\Caucasus\airbases.kml', # Input kml file setting the boundary of the map to create + 'zoom_factor': 0.1 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)] +} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/LasVegas/boundary.kml b/scripts/python/map_generator/configs/Caucasus/LowResolution.kml similarity index 52% rename from scripts/python/map_generator/configs/LasVegas/boundary.kml rename to scripts/python/map_generator/configs/Caucasus/LowResolution.kml index b2da4f16..e56c3210 100644 --- a/scripts/python/map_generator/configs/LasVegas/boundary.kml +++ b/scripts/python/map_generator/configs/Caucasus/LowResolution.kml @@ -2,7 +2,7 @@ Senza titolo - + - + - + normal - #__managed_style_1FB08D70372F0ACE3361 + #__managed_style_1EB9027B622F24E92C22 highlight - #__managed_style_2F9039BE1B2F0ACE3361 + #__managed_style_280E5494AE2F24E92C22 - + Poligono senza titolo - -115.0437621195802 - 36.2404454323581 - 568.1069300877758 + 37.25019544589698 + 44.41771380726969 + -138.6844933247498 0 0 35 - 21582.08160380367 + 3831683.119853139 absolute - #__managed_style_0152B588AD2F0ACE3361 + #__managed_style_0F57E9B9782F24E92C22 - -115.0657741984423,36.20908708202413,0 -115.0064821223275,36.20969233542438,0 -115.0077003574054,36.25251471885595,0 -115.0604905801644,36.25236266770626,0 -115.0657741984423,36.20908708202413,0 - - - - - - - Poligono senza titolo - - -115.144039076036 - 36.07599823986274 - 636.6854074835677 - 0 - 0 - 35 - 14114.14487087633 - absolute - - #__managed_style_0152B588AD2F0ACE3361 - - - - - -115.1866576903507,36.06787728722109,0 -115.1107872218999,36.06765965614163,0 -115.1145241760975,36.10294242539007,0 -115.1779590799479,36.10179027153036,0 -115.1866576903507,36.06787728722109,0 + 32.46459319237173,45.67416695848307,0 32.2740650283415,45.2221541106433,0 33.22174616520244,44.4837859435444,0 34.05427109764131,44.2149221586376,0 34.96485577272431,44.60230684639296,0 35.50552864748745,44.8069362633187,0 36.446105774871,44.84425518198143,0 36.76914203317659,44.70347050722764,0 38.22313992004164,44.3163345847565,0 39.43106567523965,43.72064977016311,0 40.23832274382622,43.06831352526857,0 41.01327578994438,42.67925159935859,0 41.34464189582403,42.34329512558789,0 41.16749495371268,41.74956946999534,0 40.80780496107725,41.39360013128164,0 39.98364177441992,41.27272565351572,0 39.42209428526464,41.27830763089842,0 38.82136897872954,41.2291415593637,0 38.78900701766597,39.59331113999448,0 46.4826445997655,39.11657164682355,0 46.83937081793388,45.04996086829865,0 46.88987497227086,47.59122144470205,0 32.29992865035658,47.73230965442627,0 32.46459319237173,45.67416695848307,0 diff --git a/scripts/python/map_generator/configs/Caucasus/LowResolution.yml b/scripts/python/map_generator/configs/Caucasus/LowResolution.yml new file mode 100644 index 00000000..1190bb55 --- /dev/null +++ b/scripts/python/map_generator/configs/Caucasus/LowResolution.yml @@ -0,0 +1,5 @@ +{ + 'output_directory': '.\Caucasus', # Where to save the output files + 'boundary_file': '.\configs\Caucasus\LowResolution.kml', # Input kml file setting the boundary of the map to create + 'zoom_factor': 0.5 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)] +} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/Caucasus/MediumResolution.kml b/scripts/python/map_generator/configs/Caucasus/MediumResolution.kml new file mode 100644 index 00000000..0e0af764 --- /dev/null +++ b/scripts/python/map_generator/configs/Caucasus/MediumResolution.kml @@ -0,0 +1,71 @@ + + + + MediumResolution.kml + + + normal + #s_ylw-pushpin + + + highlight + #s_ylw-pushpin_hl + + + + + + Untitled Polygon + + #m_ylw-pushpin + + + 1 + + + + 38.01314831290035,44.57640274670221,0 38.02944798685579,44.60276751290017,0 38.03701060313551,44.62238817868698,0 38.04339313778571,44.65487861016632,0 38.05976583140756,44.68123948836164,0 38.07725859098449,44.69472890923019,0 38.11291945378741,44.71522618625848,0 38.15737096473566,44.74243269722512,0 38.18485778769824,44.74972736437217,0 38.21177330355324,44.7634527979471,0 38.25852533281954,44.76484292008565,0 38.30469494342632,44.77265044108603,0 38.36049540323182,44.77428878950431,0 38.37905726470693,44.77483376304828,0 38.44346421266869,44.78314952437485,0 38.48003274711677,44.79063943163983,0 38.53510271283257,44.79863898344369,0 38.58131400320148,44.79993344687284,0 38.62750840123165,44.80120844370346,0 38.67367469674257,44.80246384096735,0 38.71956774499493,44.80373436400996,0 38.75627384860339,44.8047373881825,0 38.81181562123516,44.79979181843787,0 38.86732496532647,44.79482114270701,0 38.9418190787013,44.78384375272389,0 38.97905705406094,44.778337408217,0 39.01583176131165,44.77923974267458,0 39.0900956709648,44.76819193321122,0 39.15465996521237,44.76330766478745,0 39.21909989645339,44.75839879172977,0 39.2560739657125,44.75283091433155,0 39.2926368410373,44.75366325960852,0 39.32996917677399,44.74166417489695,0 39.36650552718523,44.74247384431043,0 39.41255496817574,44.73705800868364,0 39.48634199995926,44.72579712136418,0 39.52357053143431,44.71374682534395,0 39.56077169452355,44.70168758531198,0 39.6165192307209,44.6835853772921,0 39.65364919828341,44.67150732324989,0 39.72813801276361,44.64093207562432,0 39.78387993140787,44.61639917038017,0 39.83048040841024,44.591681936124,0 39.85859564812953,44.57301236646423,0 39.90509751878192,44.5482824053089,0 39.94249273127954,44.52338201672428,0 39.99853360189767,44.49238758170668,0 40.06418699118991,44.44876421256014,0 40.10122416737037,44.43022041947975,0 40.17476642160124,44.39952250587424,0 40.22062991958054,44.38112056606379,0 40.26616925085745,44.36908408773458,0 40.32066183181299,44.3571725102825,0 40.35687691238554,44.35134432568741,0 40.44767908225873,44.32717767100264,0 40.48389607700778,44.31496053921349,0 40.53788603259777,44.30297929770608,0 40.57403888305044,44.29073121970499,0 40.66420062160682,44.26643647458618,0 40.70947171289453,44.24790062395415,0 40.79051702894591,44.22341319737066,0 40.86240131168533,44.1987807551584,0 40.90727896521627,44.18655120898199,0 40.9971751422766,44.16204307598525,0 41.06896675719418,44.14366887573048,0 41.11381283684614,44.13137235210612,0 41.19430400444456,44.10665219373025,0 41.29215003328483,44.09472243976229,0 41.37240316383965,44.07625022448003,0 41.4257513549977,44.07025961185919,0 41.47917541902734,44.05789539392341,0 41.5325417114993,44.04550988906617,0 41.58597268867194,44.02675551936349,0 41.63036154961326,44.02063436986167,0 41.66587473645689,44.01445730447657,0 41.70144949563171,44.00192621497413,0 41.73692804647133,43.99573038959692,0 41.80790527475083,43.9769682848858,0 41.86104680287534,43.96445030267287,0 41.92294354557144,43.95192640497554,0 41.97593555038175,43.94569501888316,0 42.0289322801191,43.92675381975371,0 42.06422510564282,43.92045601376109,0 42.11715008872746,43.9078193193867,0 42.16126121663363,43.89517408007984,0 42.20531402728533,43.88883194377493,0 42.24050364188762,43.8761279474099,0 42.27567975880997,43.86974938516271,0 42.33721825822707,43.85697758262555,0 42.36356945565706,43.84425223576203,0 42.4162746612844,43.83145551598434,0 42.46900614324979,43.81867159050743,0 42.52171597341432,43.80587848838739,0 42.57435452102627,43.79303476282518,0 42.66192800537871,43.76098283129826,0 42.68820542361732,43.75451828141484,0 42.71453056569531,43.75437460048707,0 42.78443169369056,43.7223423950142,0 42.82815553677322,43.70943149863041,0 42.88931539666483,43.69006393459333,0 42.93306263868907,43.68341230544033,0 42.97662716861461,43.66407134484366,0 43.01149955281491,43.65107862194047,0 43.0724574902484,43.62514140546799,0 43.13327577066558,43.59283128307425,0 43.17650639536976,43.56075387900956,0 43.19390304524044,43.5542383074794,0 43.2458228621912,43.52203453680247,0 43.28917624236355,43.50260877389707,0 43.33249712773732,43.48316752413342,0 43.39341545401486,43.46982012002935,0 43.41934387933852,43.45687177914835,0 43.45417023053265,43.45011914908257,0 43.54116031694757,43.42340105939786,0 43.60215378404467,43.40973324821089,0 43.62805859606298,43.39668681257595,0 43.66269445936168,43.3835061348825,0 43.71445259635788,43.35737376950652,0 43.75781252139677,43.34400401633088,0 43.79224026980383,43.32441967291912,0 43.84412456960271,43.30452957746165,0 43.86977478309283,43.28506457890185,0 43.90414446493419,43.26544051056777,0 43.98147797545967,43.22607034290247,0 44.05860864936233,43.18675114853992,0 44.06709119023878,43.18027612980465,0 44.12715980003196,43.15386409427541,0 44.18743101444286,43.13372968109404,0 44.23897569124552,43.11371864078352,0 44.27358790844354,43.1066733857835,0 44.28203952535908,43.100179325582,0 44.33401294247151,43.09279825752913,0 44.37696649117978,43.07928586699428,0 44.41989828391778,43.06575823137832,0 44.45469479565277,43.06501638277474,0 44.51539689936585,43.05076982497799,0 44.55018766980033,43.04346891719553,0 44.58498367893472,43.0361552674791,0 44.63705094972993,43.02198827645994,0 44.68065391179591,43.02090890830104,0 44.73259892455201,43.01331904594272,0 44.7761165148564,43.01226353824702,0 44.83675394474802,43.0044120900956,0 44.87127327746453,42.9971914930625,0 44.92355181162227,42.99582138878485,0 44.97588472069648,42.99439652359818,0 45.02831258656551,42.99289450840476,0 45.07207630821009,42.99158817485907,0 45.0895848496222,42.99106070586604,0 45.14117239315028,42.97694647097289,0 45.17601296980932,42.97596816214897,0 45.21047622192639,42.96865722217818,0 45.28886690434192,42.96639262525014,0 45.34984222945756,42.96459406473044,0 45.37646173654968,42.9700880705898,0 45.43810803374696,42.97443956886776,0 45.49185301851502,42.9851743808321,0 45.52759443680108,42.99020242274158,0 45.5633579633161,42.99522156765907,0 45.62609326432623,43.00557429659632,0 45.64419236937005,43.01125368180472,0 45.71617312145327,43.0276362621879,0 45.76976054364151,43.03847417679859,0 45.83259009413669,43.05533668327431,0 45.88664835623321,43.0725031755841,0 45.90460015950028,43.0782529571539,0 45.96724185845013,43.09518163430661,0 46.02071293056959,43.10603674537837,0 46.08363829327891,43.12283810442124,0 46.12919552359966,43.1402092420311,0 46.17527504653226,43.16392574078856,0 46.21256602738341,43.18797952343618,0 46.25038909630553,43.21839048242535,0 46.28777139407883,43.2424397908981,0 46.30774824868674,43.27361498596171,0 46.32865779774432,43.31754539001604,0 46.33935284365015,43.34270001952713,0 46.34117604912893,43.36820735506829,0 46.34482783840426,43.41922482374558,0 46.33913196765758,43.46421581299708,0 46.32316096412696,43.49041999939335,0 46.30715550915618,43.51662177371173,0 46.28265395288795,43.54954566985638,0 46.26660295085507,43.57574025590465,0 46.2416351531661,43.60227192587565,0 46.21623782737858,43.62240514869328,0 46.1908274697064,43.64252979654582,0 46.14844641588729,43.67605152819741,0 46.09754206819095,43.71624568019379,0 46.04573101337119,43.7436522532048,0 45.99352467916157,43.76465044035011,0 45.93237929007829,43.78592394921217,0 45.89713232779982,43.79351371317505,0 45.86228431553677,43.80746104708328,0 45.77472350124277,43.83590173707711,0 45.71348831641006,43.85703490795119,0 45.65191189201103,43.87174753144312,0 45.61667402158427,43.87922934910029,0 45.54656786024061,43.90052206107319,0 45.49350967028987,43.90850130248388,0 45.45862584676529,43.92229280860784,0 45.42337035734356,43.92970702709297,0 45.38811307759509,43.937108252938,0 45.35322471819191,43.95085957802508,0 45.3005649870871,43.96510331400013,0 45.23011659328166,43.97979736377869,0 45.17709837174289,43.98760694827055,0 45.15110443452078,44.00104251565386,0 45.08951712248168,44.01540345578517,0 45.06316511801389,44.02245559755142,0 44.99267086992596,44.03698583304246,0 44.92219608741352,44.05146730436935,0 44.89615719435373,44.06483501784728,0 44.8170642195509,44.08581884549502,0 44.79988885185894,44.09895260738689,0 44.76462446697382,44.10613318475036,0 44.70326861737696,44.1266187753704,0 44.65106302010911,44.15323163062371,0 44.58076324644311,44.17383446229861,0 44.53684318558219,44.18748404076401,0 44.50180424794954,44.20093302105131,0 44.46701746015775,44.22072797580508,0 44.42304975061145,44.23432878609736,0 44.38822310457682,44.25409621098623,0 44.33556878516728,44.27418909606034,0 44.30069641113407,44.29392636141888,0 44.28339349889821,44.30696393893064,0 44.23097562347358,44.33336747822818,0 44.17851176458034,44.35974675030393,0 44.15226167318346,44.37292711611524,0 44.11709253571063,44.3862520301023,0 44.06408773145306,44.3998586550974,0 44.02020359643429,44.41966316702641,0 43.97607632487119,44.43308694714862,0 43.95846260821069,44.43972355886389,0 43.90559771591774,44.45962297435154,0 43.87925266250725,44.47274148280075,0 43.83525174753024,44.49247153785101,0 43.76445414868348,44.51255406389881,0 43.72038648542567,44.53223910895665,0 43.64934486886363,44.54587768081366,0 43.61397852496307,44.55904554271575,0 43.56057970837746,44.56605714778042,0 43.49855667429589,44.58587055533157,0 43.42755914078424,44.60574627274126,0 43.32971303148774,44.62584438952917,0 43.28529868203722,44.63900032613098,0 43.24967933417494,44.64569501002083,0 43.18749309794997,44.66535857889014,0 43.16090537909993,44.67832317668243,0 43.11641904880359,44.69143181586064,0 43.08075073451171,44.69808695876216,0 43.03622697338457,44.7111647564733,0 42.98273333893267,44.72429025542684,0 42.94711324326666,44.73727121278075,0 42.91139377096744,44.74387478102076,0 42.86678779842207,44.75689566297324,0 42.82222718981076,44.77627663268911,0 42.78646057264498,44.78285467849167,0 42.75068361087877,44.78942153624351,0 42.71489664982047,44.79597749357526,0 42.67018293714294,44.8089434503947,0 42.63436265526764,44.81549043973975,0 42.58062882971889,44.82847708911186,0 42.53577121010387,44.82865760553419,0 42.4999208693445,44.83515250024195,0 42.46405955597668,44.84163606739416,0 42.41921437347892,44.84813226946913,0 42.34743402687941,44.8546589311031,0 42.28461265904593,44.86112695267551,0 42.23075235677478,44.8675535271605,0 42.14989565225897,44.87399084137638,0 42.11390246653278,44.88676612195948,0 42.05993854846654,44.89314814654652,0 42.02396195908283,44.89950167231509,0 41.9789497694081,44.9122091851487,0 41.93395441207461,44.91852527096788,0 41.8709082213281,44.93116423274844,0 41.83487300285083,44.93746204450073,0 41.78979408028204,44.94374575060081,0 41.73564639729818,44.95001729713729,0 41.69040551164322,44.96267366923298,0 41.64521725871576,44.96893289651593,0 41.60000973222633,44.975175757325,0 41.53672612986542,44.98133438865409,0 41.48244250948969,44.98749676965726,0 41.41904790587971,44.99362631059221,0 41.36465478024421,44.9997667282959,0 41.30118394243455,45.0058328814192,0 41.21035696372313,45.01810001242023,0 41.16475687358584,45.03060404802721,0 41.08305809003318,45.03643156262545,0 41.01022120757838,45.04866108931765,0 40.95564788310254,45.05460618921672,0 40.91918811076351,45.06068938106755,0 40.85529919457088,45.07290925496478,0 40.77294208672041,45.09133585558963,0 40.69975096549803,45.10342659498956,0 40.67223599748748,45.10954762666954,0 40.60826715231116,45.11528463578665,0 40.57135928605479,45.12770045512377,0 40.52555110464314,45.1335962585174,0 40.47035151607203,45.14577591815908,0 40.43361189969977,45.15174699931359,0 40.38743867936167,45.16402180769061,0 40.31380771819658,45.17592665584618,0 40.25839825627587,45.18803032898685,0 40.20297601134237,45.20010650582828,0 40.13835718316099,45.21202917610439,0 40.07397839683421,45.21751425339473,0 40.00883672469774,45.23582880740994,0 39.93507023862615,45.24112145045733,0 39.87931155020412,45.2530948415529,0 39.81464896123029,45.25847075344878,0 39.73120153969697,45.26992195087966,0 39.6663551131453,45.27525219860404,0 39.60108916496383,45.28696596304335,0 39.54499678429895,45.29882416872194,0 39.48923652215399,45.30422816521241,0 39.45191407720477,45.30996135331282,0 39.33011463236952,45.33337217162322,0 39.28379180440938,45.33245751303431,0 39.21765428078088,45.35046296264309,0 39.15227166561398,45.3555619831826,0 39.10540939634441,45.3610267026658,0 39.0301811039084,45.37231545305005,0 38.93615795673987,45.38317755359031,0 38.87977490498771,45.38837104664595,0 38.8611362654265,45.38794485212644,0 38.76739945004297,45.39222452308083,0 38.70209151973316,45.39065440109443,0 38.66422219910767,45.39620787040619,0 38.57948224848323,45.40060912002444,0 38.53212686453689,45.40590283250339,0 38.47592254071214,45.40447123138531,0 38.41969247627424,45.40301206561257,0 38.37281475292825,45.40177431165958,0 38.31652805906793,45.4002684092433,0 38.26953977469677,45.39899860039247,0 38.2225204175456,45.39771723895213,0 38.17548244051565,45.39641595431553,0 38.13783892001045,45.395360610056,0 38.09015720206296,45.40050051490334,0 38.04306020447405,45.39914471108719,0 37.94877077629969,45.39637607763545,0 37.88266036855689,45.39440863283985,0 37.8265581564524,45.386217910841,0 37.77043567566488,45.37799535308437,0 37.75150814308252,45.37740831723679,0 37.68523701121197,45.37532863532221,0 37.62907056001207,45.3670276412096,0 37.58167406687781,45.36547984955829,0 37.56272067532693,45.36485974724874,0 37.49624372816054,45.36270913594099,0 37.42976711206789,45.3604558515046,0 37.36300988528929,45.35826594092745,0 37.29631903836574,45.35598736154873,0 37.2574563269086,45.36116116878497,0 37.20010906702014,45.3592220898275,0 37.13254208285088,45.36290330815135,0 37.0655525681113,45.36091256593174,0 36.99876905778555,45.35842937608302,0 36.93202970910508,45.35589845692478,0 36.87419344573569,45.35378013197294,0 36.84528518883048,45.35271304946658,0 36.7985371039233,45.33791738963414,0 36.77194931640519,45.31731416288704,0 36.74544608511216,45.29666640798376,0 36.71978022459398,45.26949861492521,0 36.70380156937625,45.24271188567592,0 36.6877688993328,45.21591864758674,0 36.68220030795592,45.18298920764466,0 36.67644135182661,45.1501054833135,0 36.68108172872844,45.11111815847261,0 36.68413932080762,45.08519148093472,0 36.6982416402701,45.04652422941314,0 36.71895425088757,45.03431199144838,0 36.76176466252009,44.99690251128398,0 36.78237252013942,44.98469651020781,0 36.81400605981582,44.9598997677145,0 36.85432758358725,44.94198483400054,0 36.89463171023441,44.92406228333582,0 36.95607078108547,44.88744866471525,0 37.0056915338367,44.86989329050147,0 37.05590681577546,44.84586084391723,0 37.13577795837795,44.81003106420523,0 37.1661492932199,44.79170052673156,0 37.2840283303223,44.7507927272321,0 37.333699169088,44.72674043281787,0 37.39266858152133,44.70306913557176,0 37.44156188721184,44.68544143936163,0 37.48966540451932,44.67428275061241,0 37.52834674166351,44.66276488989296,0 37.57756480521245,44.63867678850659,0 37.6167789701361,44.6206512126293,0 37.64658130793807,44.60237958368824,0 37.68563249181236,44.58439603164166,0 37.72350647070319,44.57929352624546,0 37.76135764103405,44.57418080106547,0 37.79971359709912,44.56261018810848,0 37.83749478428704,44.55749069061759,0 37.86596991602401,44.55202519279939,0 37.91353842880665,44.5407661107667,0 37.95069554428976,44.54202127928167,0 37.96901833162684,44.54908041138402,0 38.01314831290035,44.57640274670221,0 + + + + + + 1 + + + + 41.53775554078113,41.54284169087985,0 41.5557362933533,41.54308773135198,0 41.59168899881018,41.54349741438154,0 41.62753474418238,41.55053498182795,0 41.67243157856005,41.55100515184548,0 41.70823459948813,41.55800681789061,0 41.7440345191207,41.56499465604114,0 41.7796477946567,41.58523610619877,0 41.81540029084289,41.59885565759765,0 41.85100111634027,41.62573535054879,0 41.87778979736473,41.63924597870949,0 41.90444289204574,41.66602302187243,0 41.92206151548901,41.6993597342634,0 42.04693067641833,41.80646201228668,0 42.13604393961469,41.89982122942289,0 42.17183308027819,41.92652169606252,0 42.19852871789745,41.95977352212215,0 42.2252628968215,41.97970842725169,0 42.26994872187819,41.99965020443318,0 42.29673577361682,42.01957606699964,0 42.33254608454833,42.03285823420884,0 42.35044607253829,42.04613765715574,0 42.38629271712682,42.06606236809689,0 42.4311496768144,42.0859710512487,0 42.46706331372805,42.09922911957894,0 42.502993728219,42.11247688800623,0 42.53894283546896,42.12571437916819,0 42.57489926553479,42.13894623322069,0 42.61985198507322,42.14548637159785,0 42.6648140035679,42.15200897086535,0 42.70975920132265,42.15186531039845,0 42.7457140791612,42.15173772830666,0 42.79065500174401,42.15156200799349,0 42.81761723751103,42.15144834777791,0 42.87154381883792,42.15120691211129,0 42.88948251478816,42.14449105785713,0 42.93431916747353,42.13759144636472,0 42.99713969807707,42.13060671855428,0 43.06883659142119,42.1168946332286,0 43.09568772190185,42.11008092547894,0 43.1315020603319,42.10320307300102,0 43.17627327311938,42.09625352467222,0 43.22989271585855,42.08257743750935,0 43.28347321227141,42.06885444601161,0 43.32817202801907,42.06183494226872,0 43.3818131650025,42.05471755909613,0 43.40868719679401,42.05446991900791,0 43.44464426678762,42.06077093451521,0 43.48983383300899,42.08025470959098,0 43.53518971128506,42.1063651468701,0 43.57162117444575,42.13255664457429,0 43.60840652610747,42.17201492544496,0 43.63593726230538,42.19829858984674,0 43.64561976620663,42.23147075519826,0 43.64631468213079,42.26474898795221,0 43.64700960042722,42.29802384606256,0 43.62941954714505,42.31820153960343,0 43.59380776363254,42.33858319170501,0 43.54024893401661,42.35912253707791,0 43.52238473691688,42.36596393345501,0 43.46863720819255,42.37981940818332,0 43.44162786499893,42.38008338950666,0 43.3697973887418,42.3940728486736,0 43.33373894928955,42.39440171561582,0 43.27985652940934,42.40818858744005,0 43.2437832018161,42.4084895392751,0 43.18985791220844,42.42223429039895,0 43.15376662807179,42.42250590127029,0 43.08165249776953,42.4296721637964,0 43.0365970877093,42.43662902324724,0 42.97339409950316,42.43701594915608,0 42.92837507808357,42.45059311942708,0 42.87418834415907,42.45087682837404,0 42.82912205079744,42.45775792484824,0 42.79307650237364,42.46458182687464,0 42.74800211233436,42.47142959219867,0 42.72101715155875,42.48485361330731,0 42.65795502138735,42.50504793003268,0 42.62188306119439,42.51181337142309,0 42.57680918934006,42.52524805411103,0 42.53171552103682,42.5453236739107,0 42.4773939052392,42.56541494626055,0 42.45020055709857,42.57212405074564,0 42.39581384265429,42.59884830001353,0 42.35949810336288,42.60555296409036,0 42.29590990712439,42.62559176168327,0 42.25954382216485,42.63893498303896,0 42.20494699261081,42.65893454593285,0 42.16856108827348,42.67225181411676,0 42.12310928360301,42.68553934625314,0 42.06853937910092,42.69879588322272,0 42.02301234779619,42.71871322359522,0 41.98658482768704,42.73197012891247,0 41.93193663650399,42.74516556255131,0 41.8863275391389,42.76503248686282,0 41.82242507396163,42.79149171599865,0 41.79500299216333,42.80472444625348,0 41.73988234604107,42.83122223678754,0 41.69368796281208,42.85778621032522,0 41.66597438741361,42.87105054289982,0 41.62905250953446,42.88427960424815,0 41.57367590476706,42.90409614079181,0 41.52737897401412,42.93060790088431,0 41.49030785595106,42.9504710178689,0 41.43516448825834,42.97014538786008,0 41.39841148287921,42.98323536689215,0 41.33370461438196,43.02283575676092,0 41.29674821910837,43.03593609563478,0 41.25970706568651,43.05569418075022,0 41.22270411169512,43.0687753648313,0 41.18567865624801,43.08184718692608,0 41.14855821584256,43.10157709004786,0 41.08354843051588,43.13441412574268,0 41.05568777197013,43.14088102660241,0 40.99972072685822,43.16714104809289,0 40.97161520577396,43.18693500869321,0 40.93430640248746,43.19997851502518,0 40.90615392097179,43.21976308849529,0 40.87808475586904,43.23287166021948,0 40.81244462881681,43.25903898424126,0 40.74653268526342,43.29187236784487,0 40.72758076919474,43.30507544992877,0 40.67100707738557,43.33129576545086,0 40.63336888345545,43.34430487580448,0 40.59551776613485,43.36400131053726,0 40.56697491011295,43.38379690364339,0 40.52929238947076,43.39677187567171,0 40.49158229972748,43.40973842205175,0 40.46318575886772,43.42280613228139,0 40.42520135412486,43.44246253179541,0 40.35851841607079,43.46859967230878,0 40.2920545002291,43.48798320432996,0 40.29186693076454,43.49467606366818,0 40.22485660673597,43.51417407539959,0 40.21481235678878,43.52753350534147,0 40.15672909129018,43.55389118804134,0 40.09883298603469,43.57351259881662,0 40.06004175537093,43.59325592294303,0 40.00245439033321,43.61935254888024,0 39.9833242227229,43.62580479212573,0 39.94476341368929,43.64542880184169,0 39.89700122671705,43.65817839613955,0 39.86838204498354,43.66447582070867,0 39.83029802350944,43.67062102850877,0 39.77298359656623,43.68318431178038,0 39.71561588042994,43.69569978988519,0 39.67706529680439,43.70185169199368,0 39.6295382941978,43.70122550352337,0 39.59236697914641,43.68042623868242,0 39.56605357924803,43.64606219379195,0 39.5680943256674,43.61227393564905,0 39.56933450184233,43.59198717577144,0 39.57138689975699,43.55816983965688,0 39.59225023718366,43.52475242713699,0 39.62168558290202,43.50502984680832,0 39.66091151318264,43.47866256184555,0 39.70942807994742,43.45262153219434,0 39.75862473346064,43.41300280243701,0 39.78785020872552,43.39328905300923,0 39.82677471399703,43.36698906613079,0 39.84693702219752,43.34037904684922,0 39.89543417295292,43.30753036807191,0 39.96285281113042,43.26827568651616,0 40.00117377725787,43.24872331473418,0 40.03012486622227,43.22901364276324,0 40.05871639761818,43.21602938254538,0 40.11612240784784,43.18332213420401,0 40.1353343850455,43.17018093074569,0 40.19259980331036,43.13747560172189,0 40.24978348352383,43.10478282219141,0 40.25934837429926,43.09820972028794,0 40.3160800643051,43.07219370716132,0 40.3539478839585,43.05260526031623,0 40.41052991283914,43.02659535458304,0 40.46678080177634,43.00727222906239,0 40.49500740972938,42.99423189364647,0 40.5419270147596,42.97474033208791,0 40.60742162080103,42.94880418863689,0 40.67279963510654,42.9228378057344,0 40.72912837383883,42.89003553322812,0 40.76657456827925,42.8703943730393,0 40.81293104737578,42.85755905855336,0 40.85044054112145,42.8312065939578,0 40.87836129697151,42.8181357379184,0 40.93396511184284,42.7986735566373,0 40.97118396529061,42.77899668912436,0 41.01770393338185,42.75271927536402,0 41.07330108293286,42.72651965302641,0 41.10123341197125,42.70673087941755,0 41.12913879068348,42.68694676806709,0 41.15717452916644,42.66046729786308,0 41.21266587733444,42.62752013054661,0 41.27753680000098,42.58133347938784,0 41.31449162942334,42.55492161154923,0 41.34218069105316,42.53517337956085,0 41.3699521159656,42.50864535973016,0 41.3977337373972,42.48216315570383,0 41.42580420794186,42.43564706263653,0 41.44442509268509,42.40910366742161,0 41.46330987180127,42.36252566285938,0 41.47300777847832,42.3225579626207,0 41.49205605734203,42.26266839510883,0 41.51091812899023,42.20945509965943,0 41.51124736400526,42.18940417181055,0 41.51184899375748,42.14940704737933,0 41.51257301985768,42.09604680192994,0 41.51304361340566,42.06270781873246,0 41.51351460595959,42.02937155010276,0 41.51389103853685,42.00270439552118,0 41.51426753630366,41.97603845148638,0 41.51454886823195,41.9560408205822,0 41.51529969022265,41.90271680024225,0 41.51601958509476,41.84939179639044,0 41.50738364776096,41.82268764844807,0 41.50794314458717,41.78265830783323,0 41.49956653289131,41.73593204200335,0 41.49094379732394,41.7091836430297,0 41.48241637344626,41.67576839642926,0 41.48277005855586,41.64910177468716,0 41.46538962600514,41.60894168315224,0 41.46583961205656,41.57560963453324,0 41.53775554078113,41.54284169087985,0 + + + + + + 1 + + + + 46.29136056526277,41.70642736508859,0 46.2655374598496,41.72746512034644,0 46.24798959676117,41.73478228033488,0 46.19564432800482,41.76341874451683,0 46.16115209622109,41.79143028820085,0 46.11736962289969,41.81302249013741,0 46.08220077071437,41.82760279762712,0 46.07393530816669,41.84128122420302,0 46.02953907168371,41.86296653864726,0 45.97592576115243,41.88496605089954,0 45.96712042768141,41.89198115153647,0 45.91376114310117,41.920632764118,0 45.86013833031562,41.94254110596773,0 45.80676845790597,41.97110399371204,0 45.77101722026075,41.98565920730643,0 45.725955564429,41.99379516697699,0 45.68099840062693,42.00862816602489,0 45.63565270913485,42.01677973576709,0 45.5812152974966,42.0251941414858,0 45.52690343264386,42.02683115346582,0 45.48201492865787,42.02137122038625,0 45.45493178565337,42.01542542452609,0 45.40967572857903,42.00328169139971,0 45.35552095218777,41.99134464290105,0 45.31840845667836,41.97238166781943,0 45.29099863128936,41.96648146570077,0 45.23621071348215,41.95466470778074,0 45.19969202127459,41.94228297108083,0 45.15423561361719,41.93011569448655,0 45.1090988089524,41.92458878469146,0 45.05471454575017,41.91260804562862,0 45.0006433083413,41.90725980387611,0 44.95588715117321,41.90832075394844,0 44.90190046564182,41.90290500703968,0 44.84792260312036,41.89746351754019,0 44.80317141083541,41.8984582160928,0 44.75815683274477,41.89277719190957,0 44.71340818825777,41.89373744246018,0 44.6860244148217,41.8809885369469,0 44.63145468005109,41.8621918212395,0 44.59502689459989,41.84968114723631,0 44.55862312027542,41.83716212732757,0 44.51302072169235,41.81817931718435,0 44.48518326606419,41.79213691670284,0 44.45715170864211,41.75943595332741,0 44.44729881207198,41.73300160978207,0 44.42827616640895,41.70010583641513,0 44.41822805812167,41.66701663104404,0 44.40819056272188,41.63392764776611,0 44.4073228431249,41.60729951896295,0 44.40578327122952,41.56072419136571,0 44.41337823615215,41.52061272268416,0 44.43879679781359,41.48011372054618,0 44.48146939519803,41.42597045923384,0 44.49856796833567,41.40563622444879,0 44.54189197339844,41.37139214028922,0 44.59382879531736,41.33027103698075,0 44.65486748320358,41.29554587105938,0 44.71607012701851,41.26747049424475,0 44.75945136971854,41.23980950145585,0 44.80386201218432,41.23873212776564,0 44.83939508710957,41.23785862285534,0 44.89270076787832,41.23652617709271,0 44.93772737194195,41.24867818391856,0 44.97328205520126,41.24776308881047,0 45.01907030278738,41.27309403085943,0 45.07369949925368,41.29141017020593,0 45.10983278395497,41.29696806464043,0 45.15527303267147,41.30888709188234,0 45.19128061237639,41.31453219506862,0 45.23602909997862,41.32002510612885,0 45.29857768985443,41.3250143950737,0 45.33416190651862,41.32403215347762,0 45.39671697915616,41.32213231990524,0 45.45940221481742,41.32014120641703,0 45.50419250763679,41.31869709118127,0 45.54003268838061,41.31753670697918,0 45.57551084597447,41.30970076629319,0 45.62888963209738,41.30126446312586,0 45.68227301560751,41.2928005994364,0 45.72667875602514,41.2846220366463,0 45.78000434070471,41.27613899870758,0 45.83298573021339,41.26094177619265,0 45.85052958090154,41.25364113518242,0 45.91244252847301,41.23809291562738,0 45.98361453558361,41.22889643865703,0 46.01900165480877,41.22094340493064,0 46.08099732081731,41.20524529321233,0 46.12596191925329,41.20349831104546,0 46.16194497038691,41.20208815960403,0 46.23318663834206,41.1858368946756,0 46.26918939591037,41.18438942888334,0 46.35924251833756,41.18071667516919,0 46.41346204771126,41.17841788224271,0 46.44127361113204,41.19070266410098,0 46.46052644083118,41.21004447149864,0 46.48921841312685,41.23570883496545,0 46.49992571005606,41.26211184915207,0 46.51106905429771,41.29520785111438,0 46.52093812397818,41.30822447349239,0 46.52351243433359,41.34838352045616,0 46.5252309386499,41.37515650858474,0 46.52738102506081,41.40862114563205,0 46.52953279269449,41.44208467456748,0 46.53168678136583,41.47554737894669,0 46.52450690141258,41.50946657468598,0 46.4984229782033,41.53070656409578,0 46.47267856199456,41.55864564162263,0 46.45527834611592,41.57278820142519,0 46.4117624801078,41.60811801287936,0 46.37733789777619,41.63623784726956,0 46.34296583161303,41.66432492867152,0 46.3353640999735,41.69146384505699,0 46.29136056526277,41.70642736508859,0 + + + + + + + + diff --git a/scripts/python/map_generator/configs/Caucasus/MediumResolution.yml b/scripts/python/map_generator/configs/Caucasus/MediumResolution.yml new file mode 100644 index 00000000..d8ab84d4 --- /dev/null +++ b/scripts/python/map_generator/configs/Caucasus/MediumResolution.yml @@ -0,0 +1,5 @@ +{ + 'output_directory': '.\Caucasus', # Where to save the output files + 'boundary_file': '.\configs\Caucasus\MediumResolution.kml', # Input kml file setting the boundary of the map to create + 'zoom_factor': 0.25 # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)] +} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/Caucasus/airbases.json b/scripts/python/map_generator/configs/Caucasus/airbases.json new file mode 100644 index 00000000..12626ffd --- /dev/null +++ b/scripts/python/map_generator/configs/Caucasus/airbases.json @@ -0,0 +1 @@ +{"airbases":{"1":{"callsign":"Anapa-Vityazevo","coalition":"neutral","latitude":45.013174733771677,"longitude":37.359783477555922},"10":{"callsign":"Gudauta","coalition":"neutral","latitude":43.124233340197144,"longitude":40.564175768400638},"11":{"callsign":"Batumi","coalition":"neutral","latitude":41.603279859649049,"longitude":41.609275483509791},"12":{"callsign":"Senaki-Kolkhi","coalition":"neutral","latitude":42.238728081573278,"longitude":42.061021312855914},"13":{"callsign":"Kobuleti","coalition":"neutral","latitude":41.93210535345338,"longitude":41.876483823101026},"14":{"callsign":"Kutaisi","coalition":"neutral","latitude":42.179153937689627,"longitude":42.495684077400142},"15":{"callsign":"Mineralnye Vody","coalition":"neutral","latitude":44.218646823806807,"longitude":43.100679733081456},"16":{"callsign":"Nalchik","coalition":"neutral","latitude":43.510071438529849,"longitude":43.625108736097914},"17":{"callsign":"Mozdok","coalition":"neutral","latitude":43.791303250938249,"longitude":44.620327262102009},"18":{"callsign":"Tbilisi-Lochini","coalition":"neutral","latitude":41.674720064437075,"longitude":44.946875226153338},"19":{"callsign":"Soganlug","coalition":"neutral","latitude":41.641163266786613,"longitude":44.947183065316693},"2":{"callsign":"Krasnodar-Center","coalition":"neutral","latitude":45.087429883845076,"longitude":38.925202300775062},"20":{"callsign":"Vaziani","coalition":"neutral","latitude":41.637735936261556,"longitude":45.019090938460067},"21":{"callsign":"Beslan","coalition":"neutral","latitude":43.208500987380937,"longitude":44.588922553542936},"3":{"callsign":"Novorossiysk","coalition":"neutral","latitude":44.673329604126899,"longitude":37.786226060479564},"4":{"callsign":"Krymsk","coalition":"neutral","latitude":44.961383022734175,"longitude":37.985886938697085},"5":{"callsign":"Maykop-Khanskaya","coalition":"neutral","latitude":44.67144025735508,"longitude":40.021427482235985},"6":{"callsign":"Gelendzhik","coalition":"neutral","latitude":44.56767458600406,"longitude":38.004146350528103},"7":{"callsign":"Sochi-Adler","coalition":"neutral","latitude":43.439378434050852,"longitude":39.924231880466095},"8":{"callsign":"Krasnodar-Pashkovsky","coalition":"neutral","latitude":45.0460996415433,"longitude":39.203066906324537},"9":{"callsign":"Sukhumi-Babushara","coalition":"neutral","latitude":42.852741071634995,"longitude":41.142447588488196}},"frameRate":60,"load":0,"sessionHash":"K2n7kpGE9yOaYE4G","time":"1709136685634"} \ No newline at end of file diff --git a/scripts/python/map_generator/configs/Caucasus/airbases.kml b/scripts/python/map_generator/configs/Caucasus/airbases.kml new file mode 100644 index 00000000..983ec63d --- /dev/null +++ b/scripts/python/map_generator/configs/Caucasus/airbases.kml @@ -0,0 +1 @@ +doc namedoc description1namedescription137.232713,44.923343 37.232713,45.103006 37.486854,45.103006 37.486854,44.923343 37.232713,44.923343namedescription140.441098,43.034402 40.441098,43.214065 40.687254,43.214065 40.687254,43.034402 40.441098,43.034402namedescription141.489141,41.513448 41.489141,41.693111 41.729410,41.693111 41.729410,41.513448 41.489141,41.513448namedescription141.939685,42.148897 41.939685,42.328560 42.182358,42.328560 42.182358,42.148897 41.939685,42.148897namedescription141.755732,41.842274 41.755732,42.021937 41.997235,42.021937 41.997235,41.842274 41.755732,41.842274namedescription142.374462,42.089322 42.374462,42.268985 42.616906,42.268985 42.616906,42.089322 42.374462,42.089322namedescription142.975336,44.128815 42.975336,44.308478 43.226023,44.308478 43.226023,44.128815 42.975336,44.128815namedescription143.501246,43.420240 43.501246,43.599903 43.748971,43.599903 43.748971,43.420240 43.501246,43.420240namedescription144.495884,43.701472 44.495884,43.881135 44.744771,43.881135 44.744771,43.701472 44.495884,43.701472namedescription144.826608,41.584889 44.826608,41.764552 45.067143,41.764552 45.067143,41.584889 44.826608,41.584889namedescription144.826978,41.551332 44.826978,41.730995 45.067388,41.730995 45.067388,41.551332 44.826978,41.551332namedescription138.797967,44.997598 38.797967,45.177261 39.052438,45.177261 39.052438,44.997598 38.797967,44.997598namedescription144.898893,41.547904 44.898893,41.727567 45.139289,41.727567 45.139289,41.547904 44.898893,41.547904namedescription144.465674,43.118669 44.465674,43.298333 44.712171,43.298333 44.712171,43.118669 44.465674,43.118669namedescription137.659903,44.583498 37.659903,44.763161 37.912549,44.763161 37.912549,44.583498 37.659903,44.583498namedescription137.858932,44.871551 37.858932,45.051215 38.112842,45.051215 38.112842,44.871551 37.858932,44.871551namedescription139.895109,44.581609 39.895109,44.761272 40.147746,44.761272 40.147746,44.581609 39.895109,44.581609namedescription137.878053,44.477843 37.878053,44.657506 38.130239,44.657506 38.130239,44.477843 37.878053,44.477843namedescription139.800514,43.349547 39.800514,43.529210 40.047949,43.529210 40.047949,43.349547 39.800514,43.349547namedescription139.075924,44.956268 39.075924,45.135931 39.330210,45.135931 39.330210,44.956268 39.075924,44.956268namedescription141.019912,42.762910 41.019912,42.942573 41.264983,42.942573 41.264983,42.762910 41.019912,42.762910 \ No newline at end of file diff --git a/scripts/python/map_generator/configs/LasVegas/config.yml b/scripts/python/map_generator/configs/LasVegas/config.yml deleted file mode 100644 index c7b54066..00000000 --- a/scripts/python/map_generator/configs/LasVegas/config.yml +++ /dev/null @@ -1,6 +0,0 @@ -{ - 'output_directory': '.\LasVegas', # Where to save the output files - 'boundary_file': '.\configs\LasVegas\boundary.kml', # Input kml file setting the boundary of the map to create - 'zoom_factor': 0.02, # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)] - 'geo_width': 1.14 -} \ No newline at end of file diff --git a/scripts/python/map_generator/main.py b/scripts/python/map_generator/main.py index baa269d8..40ce8907 100644 --- a/scripts/python/map_generator/main.py +++ b/scripts/python/map_generator/main.py @@ -2,7 +2,6 @@ import sys import yaml import json import requests -import time from pyproj import Geod from fastkml import kml @@ -11,6 +10,9 @@ from datetime import timedelta import map_generator +# Port on which the camera control module is listening +port = 3003 + if len(sys.argv) == 1: print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.") else: @@ -53,9 +55,12 @@ else: if 'geo_width' not in map_config: # Let the user input the size of the screen to compute resolution - data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)}) - r = requests.put('http://localhost:8080', data = data) - print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles") + data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'}) + try: + r = requests.put(f'http://127.0.0.1:{port}', data = data) + print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles") + except: + print("No running DCS instance detected. You can still run the algorithm if you already took the screenshots, otherwise you will not be able to produce a map.") map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ") map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width'] @@ -72,7 +77,7 @@ else: print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)") input("Press enter to continue...") - map_generator.run(map_config) + map_generator.run(map_config, port) diff --git a/scripts/python/map_generator/map_generator.py b/scripts/python/map_generator/map_generator.py index 27726b27..78f914c7 100644 --- a/scripts/python/map_generator/map_generator.py +++ b/scripts/python/map_generator/map_generator.py @@ -5,6 +5,7 @@ import time import os import yaml import json +import numpy from fastkml import kml from shapely import wkt, Point @@ -19,7 +20,7 @@ tot_futs = 0 # constants C = 40075016.686 # meters, Earth equatorial circumference -R = C / (2 * math.pi) +R = C / (2 * math.pi) # meters, Earth equatorial radius def deg_to_num(lat_deg, lon_deg, zoom): lat_rad = math.radians(lat_deg) @@ -60,9 +61,9 @@ def extract_tiles(n, screenshots_XY, params): n_height = params['n_height'] XY = screenshots_XY[n] - if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))): + if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))): # Open the source screenshot - img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")) + img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg")) # Compute the Web Mercator Projection position of the top left corner of the most centered tile X_center, Y_center = XY[0], XY[1] @@ -84,6 +85,7 @@ def extract_tiles(n, screenshots_XY, params): try: os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X))) except FileExistsError: + # Ignore this error, it means one other thread has already created the folder continue except Exception as e: raise e @@ -91,33 +93,53 @@ def extract_tiles(n, screenshots_XY, params): n += 1 else: - raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}.jpg')} missing") + raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}_{zoom}.jpg')} missing") def merge_tiles(base_path, zoom, tile): X = tile[0] Y = tile[1] - positions = [(0, 0), (0, 1), (1, 0), (1, 1)] + # If the image already exists, open it so we can paste the higher quality data in it + if os.path.exists(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg")): + dst = Image.open(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg")) + dst = make_background_transparent(dst) + else: + dst = Image.new('RGB', (256, 256), (221, 221, 221)) - dst = Image.new('RGB', (256, 256), (0, 0, 0, 0)) + # Loop on all the 4 subtiles in the tile + positions = [(0, 0), (0, 1), (1, 0), (1, 1)] for i in range(0, 4): + # Open the subtile, if it exists, and resize it down to 128x128 if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")): im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128)) - else: - im = Image.new('RGB', (128, 128), (0, 0, 0, 0)) - dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128)) + im = make_background_transparent(im) + dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128)) + # Create the output folder if it exists if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))): try: os.mkdir(os.path.join(base_path, str(zoom - 1), str(X))) except FileExistsError: + # Ignore this error, it means one other thread has already created the folder pass except Exception as e: raise e - - dst.save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95) -def run(map_config): + # Save the image + dst.convert('RGB').save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95) + +def make_background_transparent(im): + im.putalpha(255) + data = numpy.array(im) + red, green, blue, alpha = data.T + + # If present, remove any "background" areas + background_areas = (red == 221) & (blue == 221) & (green == 221) + data[..., :][background_areas.T] = (0, 0, 0, 0) # make transparent + + return Image.fromarray(data) + +def run(map_config, port): global tot_futs, fut_counter with open('configs/screen_properties.yml', 'r') as sp: @@ -203,8 +225,8 @@ def run(map_config): # Making PUT request # If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile! lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom) - data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)}) - r = requests.put('http://localhost:8080', data = data) + data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'}) + r = requests.put(f'http://127.0.0.1:{port}', data = data) geo_data = json.loads(r.text) @@ -233,7 +255,7 @@ def run(map_config): sy = s_height / m_height # Resize, rotate and save the screenshot - screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"), quality=95) + screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"), quality=95) printProgressBar(n + 1, len(screenshots_XY)) n += 1 From 4782596e3c9e3cd6e54c184a5eda38bbace310e4 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 29 Feb 2024 10:54:52 +0100 Subject: [PATCH 08/10] Added installation/deletion of camera control plugin from manager --- frontend/website/@types/olympus/index.d.ts | 16 +++- manager/ejs/camera.ejs | 25 ++++++ manager/ejs/connections.ejs | 2 +- manager/ejs/connectionsType.ejs | 2 +- manager/ejs/expertsettings.ejs | 7 ++ manager/ejs/folder.ejs | 2 +- manager/ejs/passwords.ejs | 2 +- manager/ejs/type.ejs | 2 +- manager/javascripts/dcsinstance.js | 28 +++++-- manager/javascripts/filesystem.js | 52 +++++++++++- manager/javascripts/manager.js | 79 +++++++++++++------ .../OlympusCameraControl.lua | 0 12 files changed, 175 insertions(+), 42 deletions(-) create mode 100644 manager/ejs/camera.ejs rename scripts/lua/{camera => backend}/OlympusCameraControl.lua (100%) diff --git a/frontend/website/@types/olympus/index.d.ts b/frontend/website/@types/olympus/index.d.ts index 8886a56c..58296eed 100644 --- a/frontend/website/@types/olympus/index.d.ts +++ b/frontend/website/@types/olympus/index.d.ts @@ -176,7 +176,7 @@ declare module "constants/constants" { zoom: number; }; }; - export const mapLayers: { + export const defaultMapLayers: { "ArcGIS Satellite": { urlTemplate: string; minZoom: number; @@ -235,6 +235,7 @@ declare module "constants/constants" { export const SHOW_UNIT_CONTACTS = "Show selected units contact lines"; export const SHOW_UNIT_PATHS = "Show selected unit paths"; export const SHOW_UNIT_TARGETS = "Show selected unit targets"; + export const DCS_LINK_PORT = "DCS Camera link port"; export enum DataIndexes { startOfData = 0, category = 1, @@ -362,6 +363,7 @@ declare module "controls/dropdown" { setOptionsElements(optionsElements: HTMLElement[]): void; getOptionElements(): HTMLCollection; addOptionElement(optionElement: HTMLElement): void; + addHorizontalDivider(): void; /** Select the active value of the dropdown * * @param idx The index of the element to select @@ -851,10 +853,11 @@ declare module "other/utils" { export function enumToCoalition(coalitionID: number): "" | "blue" | "red" | "neutral"; export function coalitionToEnum(coalition: string): 0 | 1 | 2; export function convertDateAndTimeToDate(dateAndTime: DateAndTime): Date; - export function createCheckboxOption(value: string, text: string, checked?: boolean, callback?: CallableFunction, options?: any): HTMLElement; + export function createCheckboxOption(text: string, description: string, checked?: boolean, callback?: CallableFunction, options?: any): HTMLElement; export function getCheckboxOptions(dropdown: Dropdown): { [key: string]: boolean; }; + export function createTextInputOption(text: string, description: string, initialValue: string, type: string, callback?: CallableFunction, options?: any): HTMLElement; export function getGroundElevation(latlng: LatLng, callback: CallableFunction): void; } declare module "controls/slider" { @@ -1624,7 +1627,9 @@ declare module "map/map" { * @param ID - the ID of the HTML element which will contain the context menu */ constructor(ID: string); - addVisibilityOption(option: string, defaultValue: boolean): void; + addVisibilityOption(option: string, defaultValue: boolean | number | string, options?: { + [key: string]: any; + }): void; setLayer(layerName: string): void; getLayers(): string[]; setState(state: string): void; @@ -1660,12 +1665,14 @@ declare module "map/map" { getSelectedCoalitionArea(): CoalitionArea | undefined; bringCoalitionAreaToBack(coalitionArea: CoalitionArea): void; getVisibilityOptions(): { - [key: string]: boolean; + [key: string]: string | number | boolean; }; isZooming(): boolean; getPreviousZoom(): number; getIsUnitProtected(unit: Unit): boolean; getMapMarkerVisibilityControls(): MapMarkerVisibilityControl[]; + setSlaveDCSCamera(newSlaveDCSCamera: boolean): void; + setCameraControlMode(newCameraControlMode: string): void; } } declare module "mission/bullseye" { @@ -2612,6 +2619,7 @@ declare module "olympusapp" { */ setLoginStatus(status: string): void; start(): void; + getConfig(): any; } } declare module "index" { diff --git a/manager/ejs/camera.ejs b/manager/ejs/camera.ejs new file mode 100644 index 00000000..ba6d3748 --- /dev/null +++ b/manager/ejs/camera.ejs @@ -0,0 +1,25 @@ + +
+
+
+ Step <%= instances.length === 1? "5": "6" %> of <%= instances.length === 1? "5": "6" %> +
+
+ Do you want to install the camera control plugin? +
+
+ The camera control plugin allows you to control the camera position in DCS from Olympus.
+ It is necessary to install it in DCS even if you plan to use Olympus on a remote machine via your browser. +
+
+
+
+ Install +
+
+ Do not install +
+
+
diff --git a/manager/ejs/connections.ejs b/manager/ejs/connections.ejs index 4ee16210..5c33209c 100644 --- a/manager/ejs/connections.ejs +++ b/manager/ejs/connections.ejs @@ -4,7 +4,7 @@
- Step <%= instances.length === 1? "3": "4" %> of <%= instances.length === 1? "4": "5" %> + Step <%= instances.length === 1? "3": "4" %> of <%= instances.length === 1? "5": "6" %>
Manually set Olympus port and address settings diff --git a/manager/ejs/connectionsType.ejs b/manager/ejs/connectionsType.ejs index 73724f1a..3d9a67ed 100644 --- a/manager/ejs/connectionsType.ejs +++ b/manager/ejs/connectionsType.ejs @@ -4,7 +4,7 @@
- Step <%= instances.length === 1? "2": "3" %> of <%= instances.length === 1? "4": "5" %> + Step <%= instances.length === 1? "2": "3" %> of <%= instances.length === 1? "5": "6" %>
Do you want to set port and address settings? diff --git a/manager/ejs/expertsettings.ejs b/manager/ejs/expertsettings.ejs index 110d4142..fc44d79b 100644 --- a/manager/ejs/expertsettings.ejs +++ b/manager/ejs/expertsettings.ejs @@ -73,6 +73,13 @@ title="Allows services to connect to Olympus directly. This is NOT NEEDED for normal Olympus operation, even for dedicated servers. Leave it unchecked if in doubt.">
+
+ +
Enable camera control plugin + +
+
diff --git a/manager/ejs/folder.ejs b/manager/ejs/folder.ejs index 51f24901..2bd0e181 100644 --- a/manager/ejs/folder.ejs +++ b/manager/ejs/folder.ejs @@ -5,7 +5,7 @@
<% if (instances.length > 0) { %>
- Step 1 of <%= instances.length === 1? "4": "5" %> + Step 1 of <%= instances.length === 1? "5": "6" %>
Which DCS instance you want to add Olympus to? diff --git a/manager/ejs/passwords.ejs b/manager/ejs/passwords.ejs index 68a52f0e..cf8f45d0 100644 --- a/manager/ejs/passwords.ejs +++ b/manager/ejs/passwords.ejs @@ -4,7 +4,7 @@
- Step <%= instances.length === 1? "4": "5" %> of <%= instances.length === 1? "4": "5" %> + Step <%= instances.length === 1? "4": "5" %> of <%= instances.length === 1? "5": "6" %>
Enter your passwords for Olympus diff --git a/manager/ejs/type.ejs b/manager/ejs/type.ejs index b0781fb6..beb9a206 100644 --- a/manager/ejs/type.ejs +++ b/manager/ejs/type.ejs @@ -4,7 +4,7 @@
- Step <%= instances.length === 1? "1": "2" %> of <%= instances.length === 1? "4": "5" %> + Step <%= instances.length === 1? "1": "2" %> of <%= instances.length === 1? "5": "6" %>
Do you want to add Olympus for singleplayer or multiplayer? diff --git a/manager/javascripts/dcsinstance.js b/manager/javascripts/dcsinstance.js index 2235b50d..73ce935e 100644 --- a/manager/javascripts/dcsinstance.js +++ b/manager/javascripts/dcsinstance.js @@ -6,7 +6,7 @@ const { checkPort, fetchWithTimeout, getFreePort } = require('./net') const dircompare = require('dir-compare'); const { spawn } = require('child_process'); const find = require('find-process'); -const { installHooks, installMod, installJSON, applyConfiguration, installShortCuts, deleteMod, deleteHooks, deleteJSON, deleteShortCuts } = require('./filesystem') +const { installHooks, installMod, installJSON, applyConfiguration, installShortCuts, deleteMod, deleteHooks, deleteJSON, deleteShortCuts, installCameraPlugin, deleteCameraPlugin } = require('./filesystem') const { showErrorPopup, showConfirmPopup, showWaitLoadingPopup, setPopupLoadingProgress } = require('./popup') const { logger } = require("./filesystem") const { hidePopup } = require('./popup'); @@ -129,6 +129,7 @@ class DCSInstance { fps = 0; installationType = 'singleplayer'; connectionsType = 'auto'; + installCameraPlugin = 'install'; gameMasterPasswordEdited = false; blueCommanderPasswordEdited = false; redCommanderPasswordEdited = false; @@ -154,6 +155,7 @@ class DCSInstance { this.error = false; this.installationType = 'singleplayer'; this.connectionsType = 'auto'; + this.installCameraPlugin = 'install'; /* Check if the olympus.json file is detected. If true, Olympus is considered to be installed */ if (fs.existsSync(path.join(this.folder, "Config", "olympus.json"))) { @@ -518,22 +520,28 @@ class DCSInstance { await sleep(100); await installHooks(getManager().getActiveInstance().folder); - setPopupLoadingProgress("Installing mod folder...", 20); + setPopupLoadingProgress("Installing mod folder...", 16); await sleep(100); await installMod(getManager().getActiveInstance().folder, getManager().getActiveInstance().name); - setPopupLoadingProgress("Installing JSON file...", 40); + setPopupLoadingProgress("Installing JSON file...", 33); await sleep(100); await installJSON(getManager().getActiveInstance().folder); - setPopupLoadingProgress("Applying configuration...", 60); + setPopupLoadingProgress("Applying configuration...", 50); await sleep(100); await applyConfiguration(getManager().getActiveInstance().folder, getManager().getActiveInstance()); - setPopupLoadingProgress("Creating shortcuts...", 80); + setPopupLoadingProgress("Creating shortcuts...", 67); await sleep(100); await installShortCuts(getManager().getActiveInstance().folder, getManager().getActiveInstance().name); + if (getManager().getActiveInstance().installCameraPlugin === 'install') { + setPopupLoadingProgress("Installing camera plugin...", 83); + await sleep(100); + await installCameraPlugin(getManager().getActiveInstance().folder); + } + setPopupLoadingProgress("Installation completed!", 100); await sleep(500); logger.log(`Installation completed successfully`); @@ -575,18 +583,22 @@ class DCSInstance { await sleep(100); await deleteMod(this.folder, this.name); - setPopupLoadingProgress("Deleting hook scripts...", 25); + setPopupLoadingProgress("Deleting hook scripts...", 20); await sleep(100); await deleteHooks(this.folder); - setPopupLoadingProgress("Deleting JSON...", 50); + setPopupLoadingProgress("Deleting JSON...", 40); await sleep(100); await deleteJSON(this.folder); - setPopupLoadingProgress("Deleting shortcuts...", 75); + setPopupLoadingProgress("Deleting shortcuts...", 60); await sleep(100); await deleteShortCuts(this.folder, this.name); + setPopupLoadingProgress("Deleting camera plugin...", 80); + await sleep(100); + await deleteCameraPlugin(this.folder); + await sleep(500); setPopupLoadingProgress("Instance removed!", 100); logger.log(`Olympus removed from ${this.folder}`) diff --git a/manager/javascripts/filesystem.js b/manager/javascripts/filesystem.js index 96c2fd7e..75b394e7 100644 --- a/manager/javascripts/filesystem.js +++ b/manager/javascripts/filesystem.js @@ -11,6 +11,8 @@ var logger = new Console(output, output); const date = new Date(); output.write(` ======================= New log starting at ${date.toString()} =======================\n`); +var EXPORT_STRING = "pcall(function() local olympusLFS=require('lfs');dofile(olympusLFS.writedir()..[[Mods\\Services\\Olympus\\Scripts\\OlympusCameraControl.lua]]); end,nil) "; + /** Conveniency function to asynchronously delete a single file, with error catching * * @param {String} filePath The path to the file to delete @@ -172,6 +174,29 @@ async function applyConfiguration(folder, instance) { } } +/** Asynchronously install the camera control plugin + * + * @param {String} folder The base Saved Games folder where Olympus is installed + */ +async function installCameraPlugin(folder) { + logger.log(`Installing camera support plugin to DCS in ${folder}`); + /* If the export file doesn't exist, create it */ + if (!(await exists(path.join(folder, "Scripts", "export.lua")))) { + await fsp.writeFile(path.join(folder, "Scripts", "export.lua"), EXPORT_STRING); + } else { + let content = await fsp.readFile(path.join(folder, "Scripts", "export.lua"), { encoding: 'utf8' }); + if (content.indexOf(EXPORT_STRING) != -1) { + /* Looks like the export string is already installed, nothing to do */ + } + else { + /* Append the export string at the end of the file */ + content += ("\n" + EXPORT_STRING); + } + /* Write the content of the file */ + await fsp.writeFile(path.join(folder, "Scripts", "export.lua"), content) + } +} + /** Asynchronously deletes the Hooks script * * @param {String} folder The base Saved Games folder where Olympus is installed @@ -231,15 +256,40 @@ async function deleteShortCuts(folder, name) { logger.log(`ShortCuts deleted from ${folder} and desktop`); } +/** Asynchronously removes the camera plugin string from the export lua file + * + * @param {String} folder The base Saved Games folder where Olympus is installed + */ +async function deleteCameraPlugin(folder) { + logger.log(`Deleting camera support plugin to DCS in ${folder}`); + if (!(await exists(path.join(folder, "Scripts", "export.lua")))) { + /* If the export file doesn't exist, nothing to do */ + } else { + let content = await fsp.readFile(path.join(folder, "Scripts", "export.lua"), { encoding: 'utf8' }); + if (content.indexOf(EXPORT_STRING) ==+ -1) { + /* Looks like the export string is not installed, nothing to do */ + } + else { + /* Remove the export string from the file */ + content = content.replace(EXPORT_STRING, "") + + /* Write the content of the file */ + await fsp.writeFile(path.join(folder, "Scripts", "export.lua"), content) + } + } +} + module.exports = { applyConfiguration: applyConfiguration, installJSON: installJSON, installHooks: installHooks, installMod: installMod, - installShortCuts, installShortCuts, + installShortCuts: installShortCuts, + installCameraPlugin: installCameraPlugin, deleteHooks: deleteHooks, deleteJSON: deleteJSON, deleteMod: deleteMod, deleteShortCuts: deleteShortCuts, + deleteCameraPlugin: deleteCameraPlugin, logger: logger } diff --git a/manager/javascripts/manager.js b/manager/javascripts/manager.js index 353e8a3e..91848635 100644 --- a/manager/javascripts/manager.js +++ b/manager/javascripts/manager.js @@ -32,6 +32,7 @@ class Manager { connectionsTypePage = null; connectionsPage = null; passwordsPage = null; + cameraPage = null; resultPage = null; instancesPage = null; expertSettingsPage = null; @@ -103,9 +104,9 @@ class Manager { /* Get my public IP */ this.getPublicIP().then( (IP) => { this.setIP(IP); }, - (err) => { + (err) => { logger.log(err) - this.setIP(undefined); + this.setIP(undefined); } ) @@ -142,6 +143,7 @@ class Manager { this.connectionsTypePage = new WizardPage(this, "./ejs/connectionsType.ejs"); this.connectionsPage = new WizardPage(this, "./ejs/connections.ejs"); this.passwordsPage = new WizardPage(this, "./ejs/passwords.ejs"); + this.cameraPage = new WizardPage(this, "./ejs/camera.ejs"); this.resultPage = new ManagerPage(this, "./ejs/result.ejs"); this.instancesPage = new ManagerPage(this, "./ejs/instances.ejs"); this.expertSettingsPage = new WizardPage(this, "./ejs/expertsettings.ejs"); @@ -159,7 +161,7 @@ class Manager { this.setPort('backend', this.getActiveInstance().backendPort); } } - + /* Always force the IDLE state when reaching the menu page */ this.menuPage.options.onShow = async () => { await this.setState('IDLE'); @@ -337,6 +339,17 @@ class Manager { } } + /* When the camera control installation is selected */ + async onInstallCameraControlClicked(type) { + this.connectionsTypePage.getElement().querySelector(`.install`).classList.toggle("selected", type === 'install'); + this.connectionsTypePage.getElement().querySelector(`.no-install`).classList.toggle("selected", type === 'no-install'); + if (this.getActiveInstance()) + this.getActiveInstance().installCameraPlugin = type; + else { + showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`); + } + } + /* When the next button of a wizard page is clicked */ async onNextClicked() { /* Choose which page to show depending on the active page */ @@ -360,11 +373,11 @@ class Manager { this.activePage.hide(); this.typePage.show(); } - /* Installation type page */ + /* Installation type page */ } else if (this.activePage == this.typePage) { this.activePage.hide(); this.connectionsTypePage.show(); - /* Connection type page */ + /* Connection type page */ } else if (this.activePage == this.connectionsTypePage) { if (this.getActiveInstance()) { if (this.getActiveInstance().connectionsType === 'auto') { @@ -374,24 +387,28 @@ class Manager { else { this.activePage.hide(); this.connectionsPage.show(); - (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*') + (this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(".backend-address .checkbox").classList.toggle("checked", this.getActiveInstance().backendAddress === '*') } } else { showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`) } - /* Connection page */ + /* Connection page */ } else if (this.activePage == this.connectionsPage) { if (await this.checkPorts()) { this.activePage.hide(); this.passwordsPage.show(); - } - /* Passwords page */ + } + /* Passwords page */ } else if (this.activePage == this.passwordsPage) { if (await this.checkPasswords()) { this.activePage.hide(); - this.getState() === 'INSTALL' ? this.getActiveInstance().install() : this.getActiveInstance().edit(); + this.cameraPage.show() } - /* Expert settings page */ + /* Installation type page */ + } else if (this.activePage == this.cameraPage) { + this.activePage.hide(); + this.getState() === 'INSTALL' ? this.getActiveInstance().install() : this.getActiveInstance().edit(); + /* Expert settings page */ } else if (this.activePage == this.expertSettingsPage) { if (await this.checkPorts() && await this.checkPasswords()) { this.activePage.hide(); @@ -416,7 +433,7 @@ class Manager { async onCancelClicked() { this.activePage.hide(); await this.setState('IDLE'); - if (this.getMode() === "basic") + if (this.getMode() === "basic") this.menuPage.show(true); else this.instancesPage.show(true); @@ -441,7 +458,7 @@ class Manager { if (this.getActiveInstance()) this.getActiveInstance().setBlueCommanderPassword(value); - else + else showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`); } @@ -450,9 +467,9 @@ class Manager { input.placeholder = ""; } - if (this.getActiveInstance()) + if (this.getActiveInstance()) this.getActiveInstance().setRedCommanderPassword(value); - else + else showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`); } @@ -485,6 +502,20 @@ class Manager { } } + /* When the "Enable camera control plugin" checkbox is clicked */ + async onEnableCameraPluginClicked() { + if (this.getActiveInstance()) { + if (this.getActiveInstance().installCameraPlugin === 'install') { + this.getActiveInstance().installCameraPlugin = 'no-install'; + } else { + this.getActiveInstance().installCameraPlugin = 'install'; + } + this.expertSettingsPage.getElement().querySelector(".camera-plugin .checkbox").classList.toggle("checked", this.getActiveInstance().installCameraPlugin === 'install') + } else { + showErrorPopup(`
A critical error occurred!
Check ${this.getLogLocation()} for more info.
`) + } + } + /* When the "Return to manager" button is pressed */ async onReturnClicked() { await this.reload(); @@ -562,7 +593,7 @@ class Manager { this.setActiveInstance(instance); await this.setState('EDIT'); this.activePage.hide(); - (this.getMode() === 'basic'? this.typePage: this.expertSettingsPage).show(); + (this.getMode() === 'basic' ? this.typePage : this.expertSettingsPage).show(); } } @@ -571,7 +602,7 @@ class Manager { this.setActiveInstance(instance); await this.setState('INSTALL'); this.activePage.hide(); - (this.getMode() === 'basic'? this.typePage: this.expertSettingsPage).show(); + (this.getMode() === 'basic' ? this.typePage : this.expertSettingsPage).show(); } async onUninstallClicked(name) { @@ -579,7 +610,7 @@ class Manager { this.setActiveInstance(instance); await this.setState('UNINSTALL'); if (instance.webserverOnline || instance.backendOnline) - showErrorPopup("
The selected Olympus instance is currently active
Please stop DCS and Olympus Server/Client before removing it!
") + showErrorPopup("
The selected Olympus instance is currently active
Please stop DCS and Olympus Server/Client before removing it!
") else await instance.uninstall(); } @@ -620,11 +651,11 @@ class Manager { this.getActiveInstance().setBackendPort(value); } - var successEls = (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".success"); + var successEls = (this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".success"); for (let i = 0; i < successEls.length; i++) { successEls[i].classList.toggle("hide", !success); } - var errorEls = (this.getMode() === 'basic'? this.connectionsPage: this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".error"); + var errorEls = (this.getMode() === 'basic' ? this.connectionsPage : this.expertSettingsPage).getElement().querySelector(`.${port}-port`).querySelectorAll(".error"); for (let i = 0; i < errorEls.length; i++) { errorEls[i].classList.toggle("hide", success); } @@ -693,7 +724,7 @@ class Manager { document.getElementById("loader").style.opacity = "0%"; window.setTimeout(() => { document.getElementById("loader").classList.add("hide"); - }, 250); + }, 250); } async setActiveInstance(newActiveInstance) { @@ -718,12 +749,12 @@ class Manager { async setLogLocation(newLogLocation) { this.options.logLocation = newLogLocation; - } - + } + async setState(newState) { this.options.state = newState; await DCSInstance.reloadInstances(); - if (newState === 'IDLE') + if (newState === 'IDLE') this.setActiveInstance(undefined); } diff --git a/scripts/lua/camera/OlympusCameraControl.lua b/scripts/lua/backend/OlympusCameraControl.lua similarity index 100% rename from scripts/lua/camera/OlympusCameraControl.lua rename to scripts/lua/backend/OlympusCameraControl.lua From a60f2e7b62eb3bc808ff734addb4587cc8f297e4 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 29 Feb 2024 12:11:11 +0100 Subject: [PATCH 09/10] Modified website to keep camera link if dropped --- frontend/server/public/stylesheets/style/style.css | 2 +- frontend/website/src/map/map.ts | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/server/public/stylesheets/style/style.css b/frontend/server/public/stylesheets/style/style.css index 3ce629ca..b11b094c 100644 --- a/frontend/server/public/stylesheets/style/style.css +++ b/frontend/server/public/stylesheets/style/style.css @@ -689,7 +689,7 @@ nav.ol-panel { stroke: white !important; } -.ol-navbar-buttons-group button.off.red svg *[fill="black"] { +.ol-navbar-buttons-group button.red svg *[fill="black"] { fill: red !important; } diff --git a/frontend/website/src/map/map.ts b/frontend/website/src/map/map.ts index 33daa13f..464019cc 100644 --- a/frontend/website/src/map/map.ts +++ b/frontend/website/src/map/map.ts @@ -223,15 +223,15 @@ export class Map extends L.Map { }) document.addEventListener("toggleCameraLinkStatus", () => { - if (this.#slaveDCSCameraAvailable) { + // if (this.#slaveDCSCameraAvailable) { // Commented to experiment with usability this.setSlaveDCSCamera(!this.#slaveDCSCamera); - } + // } }) document.addEventListener("slewCameraToPosition", () => { - if (this.#slaveDCSCameraAvailable) { + // if (this.#slaveDCSCameraAvailable) { // Commented to experiment with usability this.#broadcastPosition(); - } + // } }) /* Pan interval */ @@ -571,13 +571,13 @@ export class Map extends L.Map { } setSlaveDCSCamera(newSlaveDCSCamera: boolean) { - if (this.#slaveDCSCameraAvailable || !newSlaveDCSCamera) { + // if (this.#slaveDCSCameraAvailable || !newSlaveDCSCamera) { // Commented to experiment with usability this.#slaveDCSCamera = newSlaveDCSCamera; let button = document.getElementById("camera-link-control"); button?.classList.toggle("off", !newSlaveDCSCamera); if (newSlaveDCSCamera) this.#broadcastPosition(); - } + // } } setCameraControlMode(newCameraControlMode: string) { @@ -970,7 +970,7 @@ export class Map extends L.Map { let linkButton = document.getElementById("camera-link-control"); if (linkButton) { if (!newSlaveDCSCameraAvailable) { - this.setSlaveDCSCamera(false); + //this.setSlaveDCSCamera(false); // Commented to experiment with usability linkButton.classList.add("red"); linkButton.title = "Camera link to DCS is not available"; } else { From e0238c268016ebebd3c2431604dffc8b68eaa147 Mon Sep 17 00:00:00 2001 From: Davide Passoni Date: Thu, 29 Feb 2024 12:12:34 +0100 Subject: [PATCH 10/10] Removed DCS map from default json --- olympus.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/olympus.json b/olympus.json index ccac4692..079aefeb 100644 --- a/olympus.json +++ b/olympus.json @@ -16,13 +16,6 @@ "password": null }, "additionalMaps": { - "DCS": { - "urlTemplate": "http://localhost:3000/maps/dcs/{z}/{x}/{y}.jpg", - "minZoom": 8, - "maxZoom": 20, - "maxNativeZoom": 17, - "attribution": "Eagle Dynamics" - } } } } \ No newline at end of file