- 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/olympus.json b/olympus.json
index e40a4843..079aefeb 100644
--- a/olympus.json
+++ b/olympus.json
@@ -14,6 +14,8 @@
"provider": "https://srtm.fasma.org/{lat}{lng}.SRTMGL3S.hgt.zip",
"username": null,
"password": null
- }
+ },
+ "additionalMaps": {
+ }
}
}
\ No newline at end of file
diff --git a/scripts/lua/backend/OlympusCameraControl.lua b/scripts/lua/backend/OlympusCameraControl.lua
new file mode 100644
index 00000000..5fa4894c
--- /dev/null
+++ b/scripts/lua/backend/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/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
diff --git a/scripts/python/map_generator/.vscode/launch.json b/scripts/python/map_generator/.vscode/launch.json
new file mode 100644
index 00000000..40a86611
--- /dev/null
+++ b/scripts/python/map_generator/.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": "main.py",
+ "console": "integratedTerminal",
+ "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/Caucasus/LowResolution.kml b/scripts/python/map_generator/configs/Caucasus/LowResolution.kml
new file mode 100644
index 00000000..e56c3210
--- /dev/null
+++ b/scripts/python/map_generator/configs/Caucasus/LowResolution.kml
@@ -0,0 +1,84 @@
+
+
+
+ Senza titolo
+
+
+
+
+
+
+
+
+ normal
+ #__managed_style_1EB9027B622F24E92C22
+
+
+ highlight
+ #__managed_style_280E5494AE2F24E92C22
+
+
+
+ Poligono senza titolo
+
+ 37.25019544589698
+ 44.41771380726969
+ -138.6844933247498
+ 0
+ 0
+ 35
+ 3831683.119853139
+ absolute
+
+ #__managed_style_0F57E9B9782F24E92C22
+
+
+
+
+ 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/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
new file mode 100644
index 00000000..d578b910
--- /dev/null
+++ b/scripts/python/map_generator/configs/screen_properties.yml
@@ -0,0 +1,4 @@
+{
+ '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
new file mode 100644
index 00000000..40ce8907
--- /dev/null
+++ b/scripts/python/map_generator/main.py
@@ -0,0 +1,84 @@
+import sys
+import yaml
+import json
+import requests
+
+from pyproj import Geod
+from fastkml import kml
+from shapely import wkt
+from datetime import timedelta
+
+import map_generator
+
+# Port on which the camera control module is listening
+port = 3003
+
+if len(sys.argv) == 1:
+ print("Please provide a configuration file as first argument. You can also drop the configuration file on this script to run it.")
+else:
+ config_file = sys.argv[1]
+ print(f"Using config file: {config_file}")
+
+ with open('configs/screen_properties.yml', 'r') as sp:
+ with open(config_file, 'r') as cp:
+ screen_config = yaml.safe_load(sp)
+ map_config = yaml.safe_load(cp)
+
+ print("Screen parameters:")
+ print(f"-> Screen width: {screen_config['width']}px")
+ print(f"-> Screen height: {screen_config['height']}px")
+
+ print("Map parameters:")
+ print(f"-> Output directory: {map_config['output_directory']}")
+ print(f"-> Boundary file: {map_config['boundary_file']}")
+ print(f"-> Zoom factor: {map_config['zoom_factor']}")
+
+ if 'geo_width' in map_config:
+ print(f"-> Geo width: {map_config['geo_width']}NM")
+
+ with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
+ # Read the config file and compute the total area of the covered map
+ doc = bp.read()
+ k = kml.KML()
+ k.from_string(doc)
+
+ geod = Geod(ellps="WGS84")
+ features = []
+ area = 0
+ for feature in k.features():
+ for sub_feature in list(feature.features()):
+ geo = sub_feature.geometry
+ area += abs(geod.geometry_area_perimeter(wkt.loads(geo.wkt))[0])
+ features.append(sub_feature)
+
+ print(f"Found {len(features)} features in the provided kml file")
+
+ if 'geo_width' not in map_config:
+ # Let the user input the size of the screen to compute resolution
+ data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'})
+ try:
+ r = requests.put(f'http://127.0.0.1:{port}', data = data)
+ print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles")
+ except:
+ print("No running DCS instance detected. You can still run the algorithm if you already took the screenshots, otherwise you will not be able to produce a map.")
+ map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ")
+
+ map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width']
+
+ tile_size = 256 * map_config['mpps'] # meters
+ tiles_per_screenshot = int(screen_config['width'] / 256) * int(screen_config['height'] / 256)
+ tiles_num = int(area / (tile_size * tile_size))
+ screenshots_num = int(tiles_num / tiles_per_screenshot)
+ total_time = int(screenshots_num / 1.0)
+
+ print(f"Total area: {int(area / 1e6)} square kilometers")
+ print(f"Estimated number of tiles: {tiles_num}")
+ print(f"Estimated number of screenshots: {screenshots_num}")
+ print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)")
+ input("Press enter to continue...")
+
+ map_generator.run(map_config, port)
+
+
+
+
diff --git a/scripts/python/map_generator/map_generator.py b/scripts/python/map_generator/map_generator.py
new file mode 100644
index 00000000..78f914c7
--- /dev/null
+++ b/scripts/python/map_generator/map_generator.py
@@ -0,0 +1,320 @@
+import math
+import requests
+import pyautogui
+import time
+import os
+import yaml
+import json
+import numpy
+
+from fastkml import kml
+from shapely import wkt, Point
+from PIL import Image
+from concurrent import futures
+from os import listdir
+from os.path import isfile, isdir, join
+
+# global counters
+fut_counter = 0
+tot_futs = 0
+
+# constants
+C = 40075016.686 # meters, Earth equatorial circumference
+R = C / (2 * math.pi) # meters, Earth equatorial radius
+
+def deg_to_num(lat_deg, lon_deg, zoom):
+ lat_rad = math.radians(lat_deg)
+ n = 1 << zoom
+ xtile = int((lon_deg + 180.0) / 360.0 * n)
+ ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
+ return xtile, ytile
+
+def num_to_deg(xtile, ytile, zoom):
+ n = 1 << zoom
+ lon_deg = xtile / n * 360.0 - 180.0
+ lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
+ lat_deg = math.degrees(lat_rad)
+ return lat_deg, lon_deg
+
+def compute_mpps(lat, z):
+ return C * math.cos(math.radians(lat)) / math.pow(2, z + 8)
+
+def printProgressBar(iteration, total, prefix = '', suffix = '', decimals = 1, length = 100, fill = '█', printEnd = "\r"):
+ percent = ("{0:." + str(decimals) + "f}").format(100 * (iteration / float(total)))
+ filledLength = int(length * iteration // total)
+ bar = fill * filledLength + '-' * (length - filledLength)
+ print(f'\r{prefix} |{bar}| {percent}% {suffix}', end = printEnd)
+ # Print New Line on Complete
+ if iteration == total:
+ print()
+
+def done_callback(fut):
+ global fut_counter, tot_futs
+ fut_counter += 1
+ printProgressBar(fut_counter, tot_futs)
+
+def extract_tiles(n, screenshots_XY, params):
+ f = params['f']
+ zoom = params['zoom']
+ output_directory = params['output_directory']
+ n_width = params['n_width']
+ n_height = params['n_height']
+
+ XY = screenshots_XY[n]
+ if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))):
+ # Open the source screenshot
+ img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))
+
+ # Compute the Web Mercator Projection position of the top left corner of the most centered tile
+ X_center, Y_center = XY[0], XY[1]
+
+ # Compute the position of the top left corner of the top left tile
+ start_x = img.width / 2 - n_width / 2 * 256
+ start_y = img.height / 2 - n_height / 2 * 256
+
+ # Iterate on the grid
+ for column in range(0, n_width):
+ for row in range(0, n_height):
+ # Crop the tile and compute its Web Mercator Projection position
+ box = (start_x + column * 256, start_y + row * 256, start_x + (column + 1) * 256, start_y + (row + 1) * 256)
+ X = X_center - math.floor(n_width / 2) + column
+ Y = Y_center - math.floor(n_height / 2) + row
+
+ # Save the tile
+ if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom), str(X))):
+ try:
+ os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X)))
+ except FileExistsError:
+ # Ignore this error, it means one other thread has already created the folder
+ continue
+ except Exception as e:
+ raise e
+ img.crop(box).save(os.path.join(output_directory, "tiles", str(zoom), str(X), f"{Y}.jpg"))
+ n += 1
+
+ else:
+ raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}_{zoom}.jpg')} missing")
+
+def merge_tiles(base_path, zoom, tile):
+ X = tile[0]
+ Y = tile[1]
+
+ # If the image already exists, open it so we can paste the higher quality data in it
+ if os.path.exists(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg")):
+ dst = Image.open(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"))
+ dst = make_background_transparent(dst)
+ else:
+ dst = Image.new('RGB', (256, 256), (221, 221, 221))
+
+ # Loop on all the 4 subtiles in the tile
+ positions = [(0, 0), (0, 1), (1, 0), (1, 1)]
+ for i in range(0, 4):
+ # Open the subtile, if it exists, and resize it down to 128x128
+ if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")):
+ im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128))
+ im = make_background_transparent(im)
+ dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128))
+
+ # Create the output folder if it exists
+ if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))):
+ try:
+ os.mkdir(os.path.join(base_path, str(zoom - 1), str(X)))
+ except FileExistsError:
+ # Ignore this error, it means one other thread has already created the folder
+ pass
+ except Exception as e:
+ raise e
+
+ # Save the image
+ dst.convert('RGB').save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95)
+
+def make_background_transparent(im):
+ im.putalpha(255)
+ data = numpy.array(im)
+ red, green, blue, alpha = data.T
+
+ # If present, remove any "background" areas
+ background_areas = (red == 221) & (blue == 221) & (green == 221)
+ data[..., :][background_areas.T] = (0, 0, 0, 0) # make transparent
+
+ return Image.fromarray(data)
+
+def run(map_config, port):
+ global tot_futs, fut_counter
+
+ with open('configs/screen_properties.yml', 'r') as sp:
+ screen_config = yaml.safe_load(sp)
+
+ # Create output folders
+ output_directory = map_config['output_directory']
+ if not os.path.exists(output_directory):
+ os.mkdir(output_directory)
+
+ skip_screenshots = False
+ if not os.path.exists(os.path.join(output_directory, "screenshots")):
+ os.mkdir(os.path.join(output_directory, "screenshots"))
+ else:
+ skip_screenshots = (input("Raw screenshots already found for this config, do you want to skip directly to tiles extraction? Enter y to skip: ") == "y")
+
+ if not os.path.exists(os.path.join(output_directory, "tiles")):
+ os.mkdir(os.path.join(output_directory, "tiles"))
+
+ # Compute the optimal zoom level
+ usable_width = screen_config['width'] - 400 # Keep a margin around the center
+ usable_height = screen_config['height'] - 400 # Keep a margin around the center
+
+ with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
+ # Read the config file
+ doc = bp.read()
+ k = kml.KML()
+ k.from_string(doc)
+
+ # Extract the features
+ features = []
+ for feature in k.features():
+ for sub_feature in list(feature.features()):
+ features.append(sub_feature)
+
+ # Iterate over all the closed features in the kml file
+ f = 1
+ for feature in features:
+ ########### Take screenshots
+ geo = feature.geometry
+
+ # Define the boundary rect around the area
+ start_lat = geo.bounds[3]
+ start_lng = geo.bounds[0]
+ end_lat = geo.bounds[1]
+ end_lng = geo.bounds[2]
+
+ # Find the zoom level that better approximates the provided resolution
+ mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - map_config['mpps']) for z in range(0, 21)]
+ zoom = mpps_delta.index(min(mpps_delta))
+
+ print(f"Feature {f} of {len(features)}, using zoom level {zoom}")
+
+ # Find the maximum dimension of the tiles at the given resolution
+ mpps = compute_mpps(end_lat, zoom)
+ d = 256 * mpps / map_config['mpps']
+
+ n_height = math.floor(usable_height / d)
+ n_width = math.floor(usable_width / d)
+
+ print(f"Feature {f} of {len(features)}, each screenshot will provide {n_height} tiles in height and {n_width} tiles in width")
+
+ # Find the starting and ending points
+ start_X, start_Y = deg_to_num(start_lat, start_lng, zoom)
+ end_X, end_Y = deg_to_num(end_lat, end_lng, zoom)
+
+ # Find all the X, Y coordinates inside of the provided area
+ screenshots_XY = []
+ for X in range(start_X, end_X, n_width):
+ for Y in range(start_Y, end_Y, n_height):
+ lat, lng = num_to_deg(X, Y, zoom)
+ p = Point(lng, lat)
+ if p.within(wkt.loads(geo.wkt)):
+ screenshots_XY.append((X, Y))
+
+ print(f"Feature {f} of {len(features)}, {len(screenshots_XY)} screenshots will be taken")
+
+ # Start looping
+ if not skip_screenshots:
+ print(f"Feature {f} of {len(features)}, taking screenshots...")
+ n = 0
+ for XY in screenshots_XY:
+ # Making PUT request
+ # If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile!
+ lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom)
+ data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350), 'mode': 'map'})
+ r = requests.put(f'http://127.0.0.1:{port}', data = data)
+
+ geo_data = json.loads(r.text)
+
+ time.sleep(0.1)
+
+ # Take and save screenshot. The response to the put request contains data, among which there is the north rotation at that point.
+ screenshot = pyautogui.screenshot()
+
+ # Scale the screenshot to account for Mercator Map Deformation
+ lat1, lng1 = num_to_deg(XY[0], XY[1], zoom)
+ lat2, lng2 = num_to_deg(XY[0] + 1, XY[1] + 1, zoom)
+
+ deltaLat = abs(lat2 - lat1)
+ deltaLng = abs(lng2 - lng1)
+
+ # Compute the height and width the screenshot should have
+ m_height = math.radians(deltaLat) * R * n_height
+ m_width = math.radians(deltaLng) * R * math.cos(math.radians(lat1)) * n_width
+
+ # Compute the height and width the screenshot has
+ s_height = map_config['mpps'] * 256 * n_height
+ s_width = map_config['mpps'] * 256 * n_width
+
+ # Compute the scaling required to achieve that
+ sx = s_width / m_width
+ sy = s_height / m_height
+
+ # Resize, rotate and save the screenshot
+ screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"), quality=95)
+
+ printProgressBar(n + 1, len(screenshots_XY))
+ n += 1
+
+ ########### Extract the tiles
+ if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom))):
+ os.mkdir(os.path.join(output_directory, "tiles", str(zoom)))
+
+ params = {
+ "f": f,
+ "zoom": zoom,
+ "output_directory": output_directory,
+ "n_width": n_width,
+ "n_height": n_height,
+ }
+
+ # Extract the tiles with parallel thread execution
+ with futures.ThreadPoolExecutor() as executor:
+ print(f"Feature {f} of {len(features)}, extracting tiles...")
+ futs = [executor.submit(extract_tiles, n, screenshots_XY, params) for n in range(0, len(screenshots_XY))]
+ tot_futs = len(futs)
+ fut_counter = 0
+ [fut.add_done_callback(done_callback) for fut in futs]
+ [fut.result() for fut in futures.as_completed(futs)]
+
+ # Increase the feature counter
+ print(f"Feature {f} of {len(features)} completed!")
+ f += 1
+
+ ########### Assemble tiles to get lower zoom levels
+ for current_zoom in range(zoom, 8, -1):
+ Xs = [int(d) for d in listdir(os.path.join(output_directory, "tiles", str(current_zoom))) if isdir(join(output_directory, "tiles", str(current_zoom), d))]
+ existing_tiles = []
+ for X in Xs:
+ Ys = [int(f.removesuffix(".jpg")) for f in listdir(os.path.join(output_directory, "tiles", str(current_zoom), str(X))) if isfile(join(output_directory, "tiles", str(current_zoom), str(X), f))]
+ for Y in Ys:
+ existing_tiles.append((X, Y))
+
+ tiles_to_produce = []
+ for tile in existing_tiles:
+ if (int(tile[0] / 2), int(tile[1] / 2)) not in tiles_to_produce:
+ tiles_to_produce.append((int(tile[0] / 2), int(tile[1] / 2)))
+
+ # Merge the tiles with parallel thread execution
+ with futures.ThreadPoolExecutor() as executor:
+ print(f"Merging tiles for zoom level {current_zoom - 1}...")
+
+ if not os.path.exists(os.path.join(output_directory, "tiles", str(current_zoom - 1))):
+ os.mkdir(os.path.join(output_directory, "tiles", str(current_zoom - 1)))
+
+ futs = [executor.submit(merge_tiles, os.path.join(output_directory, "tiles"), current_zoom, tile) for tile in tiles_to_produce]
+ tot_futs = len(futs)
+ fut_counter = 0
+ [fut.add_done_callback(done_callback) for fut in futs]
+ [fut.result() for fut in futures.as_completed(futs)]
+
+
+
+
+
+
+
diff --git a/scripts/python/map_generator/requirements.txt b/scripts/python/map_generator/requirements.txt
new file mode 100644
index 00000000..43e52dfd
--- /dev/null
+++ b/scripts/python/map_generator/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