Merge branch 'release-candidate' of https://github.com/Pax1601/DCSOlympus into release-candidate

This commit is contained in:
Davide Passoni 2024-03-11 15:37:21 +01:00
commit 539b183da8
30 changed files with 794 additions and 358 deletions

1
.gitignore vendored
View File

@ -39,3 +39,4 @@ frontend/website/plugins/controltips/index.js
/frontend/server/public/maps
*.pyc
/scripts/**/*.jpg
manager/manager.log

View File

@ -31,9 +31,11 @@ module.exports = function (configLocation) {
}
var app = express();
var backendAddress = config["backend"]["address"];
/* Define middleware */
app.use(logger('dev'));
app.use('/olympus', createProxyMiddleware({ target: `http://${config["backend"]["address"]}:${config["backend"]["port"]}`, changeOrigin: true }));
app.use('/olympus', createProxyMiddleware({ target: `http://${backendAddress === '*'? 'localhost': backendAddress}:${config["backend"]["port"]}`, changeOrigin: true }));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(express.static(path.join(__dirname, 'public')));

View File

@ -458,7 +458,7 @@ module.exports = function (configLocation) {
};
mission(req, res){
var ret = {mission: {theatre: "Nevada"}};
var ret = {mission: {theatre: "MarianaIslands"}};
ret.time = Date.now();
ret.mission.dateAndTime = {

View File

@ -122,18 +122,18 @@ export const minimapBoundaries = {
],
"Falklands": [ // South Atlantic
new LatLng(-49.097217, -79.418267),
new LatLng(-56.874517,-79.418267),
new LatLng(-56.874517, -79.418267),
new LatLng(-56.874517, -43.316433),
new LatLng(-49.097217, -43.316433),
new LatLng(-49.097217, -79.418267)
],
],
"Normandy": [ // Normandy
new LatLng(50.44, -3.29),
new LatLng(48.12,-3.29),
new LatLng(48.12, -3.29),
new LatLng(48.12, 3.70),
new LatLng(50.44, 3.70),
new LatLng(50.44, -3.29)
],
],
"SinaiMap": [ // Sinai
new LatLng(34.312222, 28.523333),
new LatLng(25.946944, 28.523333),
@ -161,36 +161,34 @@ export const defaultMapLayers = {
maxZoom: 19,
attribution: "Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Mapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community"
},
"USGS Topo": {
urlTemplate: 'https://basemap.nationalmap.gov/arcgis/rest/services/USGSTopo/MapServer/tile/{z}/{y}/{x}',
minZoom: 1,
maxZoom: 14,
attribution: 'Tiles courtesy of the <a href="https://usgs.gov/">U.S. Geological Survey</a>'
},
"OpenStreetMap Mapnik": {
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 20,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
},
"OPENVKarte": {
urlTemplate: 'https://tileserver.memomaps.de/tilegen/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 20,
attribution: 'Map <a href="https://memomaps.de/">memomaps.de</a> <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, map data &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
},
"Esri.DeLorme": {
urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{z}/{y}/{x}',
minZoom: 1,
maxZoom: 11,
attribution: 'Tiles &copy; Esri &mdash; Copyright: &copy;2012 DeLorme',
},
"CyclOSM": {
urlTemplate: 'https://{s}.tile-cyclosm.openstreetmap.fr/cyclosm/{z}/{x}/{y}.png',
minZoom: 1,
maxZoom: 20,
attribution: '<a href="https://github.com/cyclosm/cyclosm-cartocss-style/releases" title="CyclOSM - Open Bicycle render">CyclOSM</a> | Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}
"DCS Marianas Modern": [
{
urlTemplate: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
minZoom: 1,
maxZoom: 19
},
{
urlTemplate: 'http://maps.dcsolympus.com/maps/marianas-modern/{z}/{x}/{y}.png',
minNativeZoom: 1,
maxNativeZoom: 13,
minZoom: 1,
maxZoom: 20
},
{
urlTemplate: 'http://maps.dcsolympus.com/maps/marianas-modern/{z}/{x}/{y}.png',
minNativeZoom: 14,
maxNativeZoom: 16,
minZoom: 14,
maxZoom: 20,
attribution: 'Map extracted by OzDeaDMeaT (DCS Olympus Team)'
}
]
}
/* Map constants */
@ -258,7 +256,7 @@ export const MAP_MARKER_CONTROLS: MapMarkerVisibilityControl[] = [{
export const IADSTypes = ["AAA", "SAM Site", "Radar (EWR)"];
export const IADSDensities: { [key: string]: number } = { "AAA": 0.8, "SAM Site": 0.1, "Radar (EWR)": 0.05 };
export const GROUND_UNIT_AIR_DEFENCE_REGEX:RegExp = /(\b(AAA|SAM|MANPADS?|[mM]anpads?)|[sS]tinger\b)/;
export const GROUND_UNIT_AIR_DEFENCE_REGEX: RegExp = /(\b(AAA|SAM|MANPADS?|[mM]anpads?)|[sS]tinger\b)/;
export const HIDE_GROUP_MEMBERS = "Hide group members when zoomed out";
export const SHOW_UNIT_LABELS = "Show unit labels (L)";
export const SHOW_UNITS_ENGAGEMENT_RINGS = "Show units threat range rings (Q)";

View File

@ -50,7 +50,7 @@ export type MapMarkerVisibilityControl = {
export class Map extends L.Map {
#ID: string;
#state: string;
#layer: L.TileLayer | null = null;
#layer: L.TileLayer | L.LayerGroup | null = null;
#preventLeftClick: boolean = false;
#leftClickTimer: number = 0;
#deafultPanDelta: number = 100;
@ -281,14 +281,14 @@ export class Map extends L.Map {
if (layerName in this.#mapLayers) {
const layerData = this.#mapLayers[layerName];
var options: L.TileLayerOptions = {
attribution: layerData.attribution,
minZoom: layerData.minZoom,
maxZoom: layerData.maxZoom,
minNativeZoom: layerData.minNativeZoom,
maxNativeZoom: layerData.maxNativeZoom
};
this.#layer = new L.TileLayer(layerData.urlTemplate, options);
if (layerData instanceof Array) {
let layers = layerData.map((layer: any) => {
return new L.TileLayer(layer.urlTemplate, layer);
})
this.#layer = new L.LayerGroup(layers);
} else {
this.#layer = new L.TileLayer(layerData.urlTemplate, layerData);
}
}
this.#layer?.addTo(this);

View File

@ -468,6 +468,7 @@ export function createCheckboxOption(text: string, description: string, checked:
"disabled": false,
"name": "",
"readOnly": false,
"value": null,
...options
};
var div = document.createElement("div");
@ -476,10 +477,11 @@ export function createCheckboxOption(text: string, description: string, checked:
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.checked = checked;
input.name = options.name;
input.disabled = options.disabled;
input.readOnly = options.readOnly;
input.value = options.value;
var span = document.createElement("span");
span.innerText = text;
label.appendChild(input);

View File

@ -31,10 +31,11 @@ export abstract class UnitDataFile {
headersHTML += `<th data-coalition="${coalition}">${coalition[0].toUpperCase() + coalition.substring(1)}</th>`;
const optionIsValid = this.data[category].hasOwnProperty(coalition);
let checkboxHTML = createCheckboxOption(`${category}:${coalition}`, category, optionIsValid, () => { }, {
let checkboxHTML = createCheckboxOption(``, category, optionIsValid, () => { }, {
"disabled": !optionIsValid,
"name": "category-coalition-selection",
"readOnly": !optionIsValid
"readOnly": !optionIsValid,
"value" : `${category}:${coalition}`
}).outerHTML;
if (optionIsValid)

View File

@ -1,123 +0,0 @@
======================= New log starting at Thu Feb 08 2024 21:55:39 GMT+0100 (Central European Standard Time) =======================
Running in C:\Users\dpass\Documents\DCSOlympus\manager\javascripts
Development build detected, skipping version checks...
======================= New log starting at Thu Feb 08 2024 21:55:43 GMT+0100 (Central European Standard Time) =======================
Running in C:\Users\dpass\Documents\DCSOlympus\manager\javascripts
Found instance in C:\Users\dpass\Saved Games\DCS.openbeta, checking for Olympus
Comparing Mods content in C:\Users\dpass\Saved Games\DCS.openbeta
Comparing Scripts content in C:\Users\dpass\Saved Games\DCS.openbeta
Error: ENOENT: no such file or directory, lstat 'C:\Users\dpass\Documents\DCSOlympus\scripts\OlympusHook.lua'
at Object.realpathSync (node:fs:2655:7)
at Object.realpathSync (node:electron/js2c/asar_bundle:2:5358)
at Object.compareSync (C:\Users\dpass\Documents\DCSOlympus\manager\node_modules\dir-compare\build\src\index.js:44:88)
at DCSInstance.checkInstallation (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:196:41)
at async DCSInstance.findInstances (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:68:21)
at async Manager.start (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:97:29)
at async C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\preload.js:267:5 {
errno: -4058,
syscall: 'lstat',
code: 'ENOENT',
path: 'C:\\Users\\dpass\\Documents\\DCSOlympus\\scripts\\OlympusHook.lua'
}
Differences found!
Comparing Mods content in C:\Users\dpass\Saved Games\DCS.openbeta
Development build detected, skipping version checks...
Comparing Scripts content in C:\Users\dpass\Saved Games\DCS.openbeta
Error: ENOENT: no such file or directory, lstat 'C:\Users\dpass\Documents\DCSOlympus\scripts\OlympusHook.lua'
at Object.realpathSync (node:fs:2655:7)
at Object.realpathSync (node:electron/js2c/asar_bundle:2:5358)
at Object.compareSync (C:\Users\dpass\Documents\DCSOlympus\manager\node_modules\dir-compare\build\src\index.js:44:88)
at DCSInstance.checkInstallation (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:196:41)
at async DCSInstance.reloadInstances (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:38:13)
at async Manager.setState (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:725:9)
at async menuPage.options.onShow (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:165:17) {
errno: -4058,
syscall: 'lstat',
code: 'ENOENT',
path: 'C:\\Users\\dpass\\Documents\\DCSOlympus\\scripts\\OlympusHook.lua'
}
Differences found!
Fixing Olympus in C:\Users\dpass\Saved Games\DCS.openbeta
Deleting mod from C:\Users\dpass\Saved Games\DCS.openbeta
Mod succesfully removed from C:\Users\dpass\Saved Games\DCS.openbeta
Deleting hooks from C:\Users\dpass\Saved Games\DCS.openbeta
Deleting C:\Users\dpass\Saved Games\DCS.openbeta\Scripts\Hooks\OlympusHook.lua
C:\Users\dpass\Saved Games\DCS.openbeta\Scripts\Hooks\OlympusHook.lua does not exist, nothing to do
Installing mod in C:\Users\dpass\Saved Games\DCS.openbeta
Mod succesfully installed in C:\Users\dpass\Saved Games\DCS.openbeta
C:\Users\dpass\Documents\DCS Olympus backups\DCS.openbeta\databases
Backup databases found, copying over
Backup mods.lua found, copying over
Installing hooks in C:\Users\dpass\Saved Games\DCS.openbeta
[Error: ENOENT: no such file or directory, lstat 'C:\Users\dpass\Documents\DCSOlympus\scripts\OlympusHook.lua'] {
errno: -4058,
code: 'ENOENT',
syscall: 'lstat',
path: 'C:\\Users\\dpass\\Documents\\DCSOlympus\\scripts\\OlympusHook.lua'
}
Comparing Mods content in C:\Users\dpass\Saved Games\DCS.openbeta
Comparing Scripts content in C:\Users\dpass\Saved Games\DCS.openbeta
Error: ENOENT: no such file or directory, lstat 'C:\Users\dpass\Documents\DCSOlympus\scripts\OlympusHook.lua'
at Object.realpathSync (node:fs:2655:7)
at Object.realpathSync (node:electron/js2c/asar_bundle:2:5358)
at Object.compareSync (C:\Users\dpass\Documents\DCSOlympus\manager\node_modules\dir-compare\build\src\index.js:44:88)
at DCSInstance.checkInstallation (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:196:41)
at async DCSInstance.reloadInstances (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:38:13)
at async Manager.setState (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:725:9)
at async Manager.onEditMenuClicked (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:298:9) {
errno: -4058,
syscall: 'lstat',
code: 'ENOENT',
path: 'C:\\Users\\dpass\\Documents\\DCSOlympus\\scripts\\OlympusHook.lua'
}
Differences found!
Comparing Mods content in C:\Users\dpass\Saved Games\DCS.openbeta
Comparing Scripts content in C:\Users\dpass\Saved Games\DCS.openbeta
Error: ENOENT: no such file or directory, lstat 'C:\Users\dpass\Documents\DCSOlympus\scripts\OlympusHook.lua'
at Object.realpathSync (node:fs:2655:7)
at Object.realpathSync (node:electron/js2c/asar_bundle:2:5358)
at Object.compareSync (C:\Users\dpass\Documents\DCSOlympus\manager\node_modules\dir-compare\build\src\index.js:44:88)
at DCSInstance.checkInstallation (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:196:41)
at async DCSInstance.reloadInstances (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\dcsinstance.js:38:13)
at async Manager.setState (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:725:9)
at async Manager.onUninstallClicked (C:\Users\dpass\Documents\DCSOlympus\manager\javascripts\manager.js:580:9) {
errno: -4058,
syscall: 'lstat',
code: 'ENOENT',
path: 'C:\\Users\\dpass\\Documents\\DCSOlympus\\scripts\\OlympusHook.lua'
}
Differences found!
Uninstalling Olympus from C:\Users\dpass\Saved Games\DCS.openbeta
Deleting mod from C:\Users\dpass\Saved Games\DCS.openbeta
Mod succesfully removed from C:\Users\dpass\Saved Games\DCS.openbeta
Deleting hooks from C:\Users\dpass\Saved Games\DCS.openbeta
Deleting C:\Users\dpass\Saved Games\DCS.openbeta\Scripts\Hooks\OlympusHook.lua
C:\Users\dpass\Saved Games\DCS.openbeta\Scripts\Hooks\OlympusHook.lua does not exist, nothing to do
Deleting JSON from C:\Users\dpass\Saved Games\DCS.openbeta
Deleting C:\Users\dpass\Saved Games\DCS.openbeta\Config\olympus.json
C:\Users\dpass\Saved Games\DCS.openbeta\Config\olympus.json does not exist, nothing to do
Deleting ShortCuts from C:\Users\dpass\Saved Games\DCS.openbeta and desktop
Deleting C:\Users\dpass\Saved Games\DCS.openbeta\DCS Olympus Server (DCS.openbeta).lnk
C:\Users\dpass\Saved Games\DCS.openbeta\DCS Olympus Server (DCS.openbeta).lnk does not exist, nothing to do
Deleting C:\Users\dpass\Saved Games\DCS.openbeta\DCS Olympus Client (DCS.openbeta).lnk
C:\Users\dpass\Saved Games\DCS.openbeta\DCS Olympus Client (DCS.openbeta).lnk does not exist, nothing to do
Deleting C:\Users\dpass\Desktop\DCS Olympus Server (DCS.openbeta).lnk
C:\Users\dpass\Desktop\DCS Olympus Server (DCS.openbeta).lnk does not exist, nothing to do
Deleting C:\Users\dpass\Desktop\DCS Olympus Client (DCS.openbeta).lnk
C:\Users\dpass\Desktop\DCS Olympus Client (DCS.openbeta).lnk does not exist, nothing to do
ShortCuts deleted from C:\Users\dpass\Saved Games\DCS.openbeta and desktop
Olympus removed from C:\Users\dpass\Saved Games\DCS.openbeta
Installing hooks in C:\Users\dpass\Saved Games\DCS.openbeta
An error occurred during installation: Error: ENOENT: no such file or directory, lstat 'C:\Users\dpass\Documents\DCSOlympus\scripts\OlympusHook.lua'
======================= New log starting at Thu Feb 08 2024 21:58:08 GMT+0100 (Central European Standard Time) =======================
Running in C:\Users\dpass\Documents\DCSOlympus\manager\javascripts
Found instance in C:\Users\dpass\Saved Games\DCS.openbeta, checking for Olympus
Development build detected, skipping version checks...
======================= New log starting at Thu Feb 08 2024 21:59:00 GMT+0100 (Central European Standard Time) =======================
Running in C:\Users\dpass\Documents\DCSOlympus\manager\javascripts
Found instance in C:\Users\dpass\Saved Games\DCS.openbeta, checking for Olympus
Development build detected, skipping version checks...
======================= New log starting at Thu Feb 08 2024 21:59:52 GMT+0100 (Central European Standard Time) =======================
Running in C:\Users\dpass\Documents\DCSOlympus\manager\javascripts
Found instance in C:\Users\dpass\Saved Games\DCS.openbeta, checking for Olympus
Development build detected, skipping version checks...

View File

@ -16,6 +16,22 @@
"password": null
},
"additionalMaps": {
"TestMap": [
{
"urlTemplate": "http://localhost:3000/maps/TestMap/{z}/{x}/{y}.png",
"minNativeZoom": 1,
"maxNativeZoom": 13,
"minZoom": 1,
"maxZoom": 20
},
{
"urlTemplate": "http://localhost:3000/maps/TestMap/{z}/{x}/{y}.png",
"minNativeZoom": 14,
"maxNativeZoom": 17,
"minZoom": 14,
"maxZoom": 20
}
]
}
}
}

View File

@ -0,0 +1,3 @@
Syria
Test
Caucasus

View File

@ -5,12 +5,20 @@
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"name": "Main",
"type": "python",
"request": "launch",
"program": "main.py",
"console": "integratedTerminal",
"args": ["./configs/Caucasus/HighResolution.yml"]
"args": ["-s", "-l", "1", "./configs/Test/MediumResolution.yml"]
},
{
"name": "Convert",
"type": "python",
"request": "launch",
"program": "convert_to_jpg.py",
"console": "integratedTerminal",
"args": ["./Syria/tiles", "./Syria/jpg"]
}
]
}

View File

@ -1,15 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>Senza titolo</name>
<name>Low resolution</name>
<open>1</open>
<gx:CascadingStyle kml:id="__managed_style_280E5494AE2F24E92C22">
<Style>
<IconStyle>
<scale>1.2</scale>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2<![CDATA[&]]>id=2000<![CDATA[&]]>scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"></hotSpot>
</IconStyle>
<LabelStyle>
</LabelStyle>
@ -24,14 +25,13 @@
<displayMode>hide</displayMode>
</BalloonStyle>
</Style>
</gx:CascadingStyle>
<gx:CascadingStyle kml:id="__managed_style_1EB9027B622F24E92C22">
</gx:CascadingStyle><gx:CascadingStyle kml:id="__managed_style_1EB9027B622F24E92C22">
<Style>
<IconStyle>
<Icon>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&amp;id=2000&amp;scale=4</href>
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2<![CDATA[&]]>id=2000<![CDATA[&]]>scale=4</href>
</Icon>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"></hotSpot>
</IconStyle>
<LabelStyle>
</LabelStyle>
@ -50,31 +50,35 @@
<StyleMap id="__managed_style_0F57E9B9782F24E92C22">
<Pair>
<key>normal</key>
<styleUrl>#__managed_style_1EB9027B622F24E92C22</styleUrl>
<styleUrl>#failed0</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#__managed_style_280E5494AE2F24E92C22</styleUrl>
<styleUrl>#failed</styleUrl>
</Pair>
</StyleMap>
<Style id="failed">
</Style>
<Style id="failed0">
</Style>
<Placemark id="0975D432582F24E92C1E">
<name>Poligono senza titolo</name>
<name>Low resolution</name>
<LookAt>
<longitude>37.25019544589698</longitude>
<latitude>44.41771380726969</latitude>
<altitude>-138.6844933247498</altitude>
<heading>0</heading>
<tilt>0</tilt>
<gx:fovy>35</gx:fovy>
<range>3831683.119853139</range>
<altitudeMode>absolute</altitudeMode>
<gx:fovy>35</gx:fovy>
</LookAt>
<styleUrl>#__managed_style_0F57E9B9782F24E92C22</styleUrl>
<Polygon>
<outerBoundaryIs>
<LinearRing>
<coordinates>
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
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.82458556682658,39.86622166029085,0 45.8381687868953,39.59275112715873,0 45.98912922607954,43.86743059613963,0 46.10445698500042,45.31544913665732,0 34.15598911357598,46.421850808793,0 32.46459319237173,45.67416695848307,0
</coordinates>
</LinearRing>
</outerBoundaryIs>

File diff suppressed because one or more lines are too long

View File

@ -1,5 +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
'boundary_file': '.\configs\NTTR\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)]
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>MediumResolution.kml</name>
<StyleMap id="m_ylw-pushpin">
<Pair>
<key>normal</key>
<styleUrl>#s_ylw-pushpin</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#s_ylw-pushpin_hl</styleUrl>
</Pair>
</StyleMap>
<Style id="s_ylw-pushpin">
<IconStyle>
<scale>1.1</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
</Style>
<Style id="s_ylw-pushpin_hl">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
</Style>
<Placemark>
<name>Untitled Polygon</name>
<styleUrl>#m_ylw-pushpin</styleUrl>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
-114.934770823132,36.0279645610818,0 -114.8423692017325,36.14486669307939,0 -114.8222988019772,36.33833555517232,0 -115.1043264248258,36.37857716972037,0 -115.4103523219488,36.37739101654672,0 -115.4685702901508,36.22908230367275,0 -115.3526428993333,35.95526029180954,0 -115.070949845899,35.9300532859497,0 -114.934770823132,36.0279645610818,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Syria', # Where to save the output files
'boundary_file': '.\configs\Syria\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)]
}

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>LowResolution.kml</name>
<StyleMap id="msn_ylw-pushpin">
<Pair>
<key>normal</key>
<styleUrl>#sn_ylw-pushpin</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#sh_ylw-pushpin</styleUrl>
</Pair>
</StyleMap>
<Style id="sh_ylw-pushpin">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
<BalloonStyle>
</BalloonStyle>
<LineStyle>
<color>ccffffff</color>
</LineStyle>
<PolyStyle>
<color>ccffffff</color>
</PolyStyle>
</Style>
<Style id="sn_ylw-pushpin">
<IconStyle>
<scale>1.1</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
<BalloonStyle>
</BalloonStyle>
<LineStyle>
<color>ccffffff</color>
</LineStyle>
<PolyStyle>
<color>ccffffff</color>
</PolyStyle>
</Style>
<Placemark>
<name>Untitled Polygon</name>
<snippet></snippet>
<styleUrl>#msn_ylw-pushpin</styleUrl>
<MultiGeometry>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
32.41977350619059,35.86470912083693,0 33.56465531835013,35.91338825318785,0 34.80465378369205,36.47063196055174,0 35.60801631485135,36.34864532719317,0 35.49398676174558,34.21289059062669,0 34.5206672018921,31.81057604629355,0 43.95360383704013,31.37489080200468,0 44.25820669948404,37.93208169895772,0 30.99244388115055,37.7086326320776,0 31.19610811985261,36.49739270534763,0 32.41977350619059,35.86470912083693,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
32.49633696227876,34.50684717795233,0 33.09849375950411,34.48604280166101,0 33.90493204321842,34.73753410158289,0 34.25924480309209,34.95889575718786,0 34.62649679333629,35.4114629095852,0 34.72182823714807,35.80074945874743,0 32.99109621791929,35.58668302906084,0 32.02791860799897,35.17891332024415,0 32.07780557518355,34.72891507299879,0 32.49633696227876,34.50684717795233,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</MultiGeometry>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Syria', # Where to save the output files
'boundary_file': '.\configs\Syria\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)]
}

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>MediumResolution.kml</name>
<StyleMap id="msn_ylw-pushpin">
<Pair>
<key>normal</key>
<styleUrl>#sn_ylw-pushpin</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#sh_ylw-pushpin</styleUrl>
</Pair>
</StyleMap>
<Style id="sh_ylw-pushpin">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
<BalloonStyle>
</BalloonStyle>
<LineStyle>
<color>ccffffff</color>
</LineStyle>
<PolyStyle>
<color>73ffffff</color>
</PolyStyle>
</Style>
<Style id="sn_ylw-pushpin">
<IconStyle>
<scale>1.1</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
<BalloonStyle>
</BalloonStyle>
<LineStyle>
<color>ccffffff</color>
</LineStyle>
<PolyStyle>
<color>73ffffff</color>
</PolyStyle>
</Style>
<Placemark>
<name>Untitled Polygon</name>
<snippet></snippet>
<styleUrl>#msn_ylw-pushpin</styleUrl>
<MultiGeometry>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
34.67142705790602,31.82805055444974,0 35.02181508963836,31.82750977829048,0 36.01467177481414,31.98675180910247,0 36.23127775126287,32.04983840957699,0 35.9582646803902,32.28158293849557,0 35.99098147339456,32.44310980472873,0 36.09260593928732,32.55565643757006,0 36.61524825014531,32.48930025627462,0 36.60745776125667,32.80133625144484,0 36.47504812245365,33.05297304144123,0 36.35141964104501,33.3083703333435,0 36.35264203669159,33.49386454528514,0 36.47877595475967,33.58571959973897,0 36.19517390228444,33.5997606321796,0 35.83522397638479,33.61896603596939,0 35.91132793661291,33.79098072794213,0 36.15374710260814,34.26719887569751,0 36.25809031624735,34.42504420256356,0 36.50878759959249,34.48069803556503,0 36.72702134605258,34.5389760993956,0 36.91155927128435,34.70573020374997,0 36.93709483596604,34.8575453420497,0 36.84204849159945,35.0188804802811,0 36.85909086701554,35.12582156419841,0 36.80631271949839,35.2023007921343,0 36.70610451487763,35.28868757265818,0 36.64736791080302,35.33884042510283,0 36.58285870081706,35.39379914572466,0 36.45913791605572,35.785089727734,0 36.53039600108779,35.94339777542626,0 36.67529798763221,36.16555606706433,0 37.19354340712839,36.10461671438422,0 37.30210041940058,36.1672896392403,0 37.26914131633509,36.28180024676594,0 37.10359257192819,36.30310271023975,0 37.00689884978731,36.29046216813635,0 36.76464397760902,36.31629718954267,0 36.58584728151514,36.41485334551175,0 36.43276721089462,36.51419040401807,0 36.55081614559194,36.74504433804638,0 36.64379067414161,36.89890120179727,0 36.65347158496201,37.09461289580837,0 36.5670026915683,37.32284183349276,0 36.18331192127231,37.44082203549982,0 35.69883621035919,37.46691398938869,0 35.10303034414353,37.32678813082578,0 34.73373550417249,37.10256441028478,0 34.54672782985384,36.94278408400594,0 33.82387226696957,36.35935429361331,0 34.00780785927724,36.27288227125526,0 34.71609500104023,36.80115830525168,0 35.31231838128146,36.54974589517469,0 35.56842677899099,36.54590520153501,0 36.03653930366797,36.90903074000526,0 36.19000487976967,36.62318809953995,0 35.80177667905849,36.33612053423278,0 35.99066329315939,35.96583287218078,0 35.69248548202563,35.56301025420937,0 35.92254840918157,35.19455893234127,0 35.98470152257408,34.47521931409463,0 35.58103018441831,34.26637735596449,0 35.05797659081225,32.82931778284781,0 34.96691044071693,32.82607518990291,0 34.67142705790602,31.82805055444974,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
32.87727021160179,34.63039920198953,0 32.89796765395525,34.54413238804481,0 33.0513805484542,34.53311613075366,0 33.06805771189331,34.58449144684735,0 33.30594035942946,34.68068961400989,0 33.60655009787457,34.7834317386,0 33.66048901593272,34.8393762347752,0 33.70608869593579,34.95372458806005,0 33.90513134812218,34.93229745061615,0 34.1071842570156,34.94799211011232,0 33.94596222366071,35.13090630203342,0 34.03291716899977,35.33217278671437,0 34.00480141824541,35.41895359888199,0 33.86414270927582,35.36764075672064,0 33.48955253703217,35.27107279823272,0 32.94309978357973,35.39409014047924,0 32.72304170959251,35.13465820902891,0 33.02509969669595,35.06722355440451,0 33.44152949697186,35.00493863785168,0 33.43618672640994,34.91604930651071,0 33.31998050412736,34.76455001947792,0 32.87980862276667,34.77470445862257,0 32.7489895060826,34.66006328417646,0 32.87727021160179,34.63039920198953,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
37.316751241333,36.13625362966467,0 37.4802212403484,35.98662494935605,0 37.69339926931355,35.87154344120644,0 38.14290686820461,35.77632913948397,0 38.5968154612054,35.78564003854654,0 38.89284932130154,35.8779076116201,0 39.07507331612245,35.86639415034433,0 39.20140789434584,35.82480100674179,0 39.2433581758882,35.91988707796679,0 39.14777729544377,36.06853895742726,0 39.05867321853522,36.17697296815244,0 38.94276312671954,36.17039388164508,0 38.82993809202534,36.02864047564345,0 38.7422129714303,35.90314522534125,0 38.64230099519071,35.87869415817486,0 38.35798797186277,35.94095820496009,0 38.13165022867805,35.95691377930053,0 37.93541220365955,36.10819502548598,0 37.67868372711114,36.15507075771183,0 37.36536807770124,36.17289309161047,0 37.316751241333,36.13625362966467,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</MultiGeometry>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Syria', # Where to save the output files
'boundary_file': '.\configs\Syria\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)]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>LowResolution.kml</name>
<StyleMap id="m_ylw-pushpin">
<Pair>
<key>normal</key>
<styleUrl>#s_ylw-pushpin</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#s_ylw-pushpin_hl</styleUrl>
</Pair>
</StyleMap>
<Style id="s_ylw-pushpin">
<IconStyle>
<scale>1.1</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
</Style>
<Style id="s_ylw-pushpin_hl">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
</Style>
<Placemark>
<name>Untitled Polygon</name>
<styleUrl>#m_ylw-pushpin</styleUrl>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
36.39481280710839,34.53819230857288,0 37.1796694816022,34.53262792655826,0 37.16621384783188,34.96261757019548,0 36.40060390213443,34.96928234867986,0 36.39481280710839,34.53819230857288,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Test', # Where to save the output files
'boundary_file': '.\configs\Test\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)]
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Document>
<name>MediumResolution.kml</name>
<StyleMap id="m_ylw-pushpin">
<Pair>
<key>normal</key>
<styleUrl>#s_ylw-pushpin</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#s_ylw-pushpin_hl</styleUrl>
</Pair>
</StyleMap>
<Style id="s_ylw-pushpin">
<IconStyle>
<scale>1.1</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
</Style>
<Style id="s_ylw-pushpin_hl">
<IconStyle>
<scale>1.3</scale>
<Icon>
<href>http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png</href>
</Icon>
<hotSpot x="20" y="2" xunits="pixels" yunits="pixels"/>
</IconStyle>
</Style>
<Placemark>
<name>Untitled Polygon</name>
<styleUrl>#m_ylw-pushpin</styleUrl>
<Polygon>
<tessellate>1</tessellate>
<outerBoundaryIs>
<LinearRing>
<coordinates>
36.66523183398584,34.6677562111174,0 36.7942537132104,34.66437454529529,0 36.83169732011983,34.73421112648619,0 36.82035549501334,34.78872158568599,0 36.81876694797644,34.78938919967693,0 36.71678396314233,34.81175454434261,0 36.71358833183864,34.81177596936792,0 36.64080355603824,34.80367957661652,0 36.61013139556666,34.76382161679572,0 36.60930683760219,34.76054596757088,0 36.60928608362357,34.75792169295608,0 36.60927570797851,34.75660959986943,0 36.61934895519745,34.71456838542862,0 36.64380557289082,34.68164055942542,0 36.66523183398584,34.6677562111174,0
</coordinates>
</LinearRing>
</outerBoundaryIs>
</Polygon>
</Placemark>
</Document>
</kml>

View File

@ -0,0 +1,5 @@
{
'output_directory': '.\Test', # Where to save the output files
'boundary_file': '.\configs\Test\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)]
}

View File

@ -0,0 +1,98 @@
import sys
import os
import numpy
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
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 remove_black_areas(im):
data = numpy.array(im)
red, green, blue = data.T
# If present, remove any "black" areas
background_areas = (red < 10) & (blue < 10) & (green < 10)
data[..., :][background_areas.T] = (221, 221, 221)
return Image.fromarray(data)
def convert_tiles(source, dest, tile):
zoom = tile[0]
X = tile[1]
Y = tile[2]
if not os.path.exists(os.path.join(dest, str(zoom))):
try:
os.mkdir(os.path.join(dest, str(zoom)))
except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
pass
except Exception as e:
raise e
if not os.path.exists(os.path.join(dest, str(zoom), str(X))):
try:
os.mkdir(os.path.join(dest, str(zoom), str(X)))
except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
pass
except Exception as e:
raise e
remove_black_areas(Image.open(os.path.join(source, str(zoom), str(X), f"{Y}.png")).convert('RGB')).save(os.path.join(dest, str(zoom), str(X), f"{Y}.jpg"))
if len(sys.argv) < 3:
print("Please provide a source and a destination folder")
else:
source = sys.argv[1]
dest = sys.argv[2]
if not os.path.exists(dest):
try:
os.mkdir(dest)
except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
pass
except Exception as e:
raise e
print(f"Listing source tiles...")
existing_tiles = []
zooms = [int(f) for f in listdir(source) if isdir(join(source, f))]
for zoom in zooms:
Xs = [int(f) for f in listdir(join(source, str(zoom))) if isdir(join(source, str(zoom), f))]
for X in Xs:
Ys = [int(f.removesuffix(".png")) for f in listdir(os.path.join(source, str(zoom), str(X))) if isfile(join(source, str(zoom), str(X), f))]
for Y in Ys:
existing_tiles.append((zoom, X, Y))
print(f"{len(existing_tiles)} tiles will be converted")
# Merge the tiles with parallel thread execution
with futures.ThreadPoolExecutor() as executor:
print(f"Converting tiles to jpg...")
print(f"Initializing exectuion pool")
futs = [executor.submit(convert_tiles, source, dest, tile) for tile in existing_tiles]
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)]

View File

@ -1,84 +1,100 @@
import sys
import os
import yaml
import json
import requests
import argparse
from pyproj import Geod
from fastkml import kml
from shapely import wkt
from datetime import timedelta
import map_generator
parser = argparse.ArgumentParser(
prog='DCS Olympus map generator',
description='This script allows to automatically generate maps from DCS World',
epilog='Hit the DCS Olympus Discord for more information')
parser.add_argument('config', help='map configuration yaml file')
parser.add_argument('-s', '--skip_screenshots', action='store_true', help='if screenshots are already present, this flag will cause the script to completely skip the screenshot loop.')
parser.add_argument('-r', '--replace_screenshots', action='store_true', help='if screenshots are already present, this flag will cause the script to replace all screenshots, even those that already exist. Has no effect if -s or --skip_screenshots is present.')
parser.add_argument('-l', '--final_level', type=int, default=1, help='if tiles are already present for the zoom level that the script will output, this number will instruct up to which zoom level tile merging will be run. Defaults to 1.')
parser.add_argument('-f', '--screenshots_folder', help='if provided, will force the script to save the screenshots here. Defaults to output_directory/screenshots.')
parser.add_argument('-t', '--tiles_folder', help='if provided, will force the script to save the tiles here. Defaults to output_directory/tiles.')
parser.add_argument('-o', '--screenshots_only', action='store_true', help='if provided, the script will only run the screenshot acquisition algorithm.')
args = parser.parse_args()
# 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)
config_file = args.config
if config_file is None:
raise Exception("No configuration file provided as input. Please run script with -h argument for more info")
print("Screen parameters:")
print(f"-> Screen width: {screen_config['width']}px")
print(f"-> Screen height: {screen_config['height']}px")
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)
map_config.update(vars(args))
if map_config["screenshots_folder"] is None:
map_config["screenshots_folder"] = os.path.join(map_config['output_directory'], "screenshots")
if map_config["tiles_folder"] is None:
map_config["tiles_folder"] = os.path.join(map_config['output_directory'], "tiles")
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"-> Screenshots folder: {map_config['screenshots_folder']}")
print(f"-> Tiles folder: {map_config['tiles_folder']}")
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: ")
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)
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}")
map_generator.run(map_config, port)

View File

@ -6,6 +6,7 @@ import os
import yaml
import json
import numpy
import datetime
from fastkml import kml
from shapely import wkt, Point
@ -21,6 +22,7 @@ tot_futs = 0
# constants
C = 40075016.686 # meters, Earth equatorial circumference
R = C / (2 * math.pi) # meters, Earth equatorial radius
PUT_RETRIES = 10 # allowable number of retries for the PUT request
def deg_to_num(lat_deg, lon_deg, zoom):
lat_rad = math.radians(lat_deg)
@ -39,7 +41,7 @@ def num_to_deg(xtile, ytile, zoom):
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"):
def print_progress_bar(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)
@ -51,19 +53,20 @@ def printProgressBar(iteration, total, prefix = '', suffix = '', decimals = 1, l
def done_callback(fut):
global fut_counter, tot_futs
fut_counter += 1
printProgressBar(fut_counter, tot_futs)
print_progress_bar(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']
screenshots_folder = params['screenshots_folder']
tiles_folder = params['tiles_folder']
XY = screenshots_XY[n]
if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))):
if (os.path.exists(os.path.join(screenshots_folder, f"{f}_{n}_{zoom}.jpg"))):
# Open the source screenshot
img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}_{zoom}.jpg"))
img = Image.open(os.path.join(screenshots_folder, 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]
@ -81,39 +84,37 @@ def extract_tiles(n, screenshots_XY, params):
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))):
if not os.path.exists(os.path.join(tiles_folder, str(zoom), str(X))):
try:
os.mkdir(os.path.join(output_directory, "tiles", str(zoom), str(X)))
os.mkdir(os.path.join(tiles_folder, str(zoom), str(X)))
except FileExistsError:
# Ignore this error, it means one other thread has already created the folder
continue
pass
except Exception as e:
raise e
img.crop(box).save(os.path.join(output_directory, "tiles", str(zoom), str(X), f"{Y}.jpg"))
img.crop(box).convert('RGBA').save(os.path.join(tiles_folder, str(zoom), str(X), f"{Y}.png"))
n += 1
else:
raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}_{zoom}.jpg')} missing")
raise Exception(f"{os.path.join(screenshots_folder, 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)
if os.path.exists(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.png")):
dst = Image.open(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.png"))
else:
dst = Image.new('RGB', (256, 256), (221, 221, 221))
dst = Image.new('RGBA', (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))
im = make_background_transparent(im)
dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128))
if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.png")):
im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.png")).resize((128, 128))
dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128), im)
# Create the output folder if it exists
if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))):
@ -126,43 +127,147 @@ def merge_tiles(base_path, zoom, tile):
raise e
# Save the image
dst.convert('RGB').save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95)
dst.save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.png"), quality=98)
def compute_correction_factor(XY, n_width, n_height, map_config, zoom, screenshots_folder, port):
# Take screenshots at the given position
take_screenshot(XY, 0, 0, map_config, zoom, screenshots_folder, "calib", "ref", port)
calib_ref = Image.open(os.path.join(screenshots_folder, f"calib_ref_{zoom}.jpg"))
# These calibration boxes are located at the edge of the interest region
box1 = (calib_ref.width / 2 + n_width / 2 * 256 - 50, calib_ref.height / 2 - n_height / 2 * 256 + 10,
calib_ref.width / 2 + n_width / 2 * 256 + 50, calib_ref.height / 2 + n_height / 2 * 256 - 10)
box2 = (calib_ref.width / 2 - n_width / 2 * 256 - 50, calib_ref.height / 2 - n_height / 2 * 256 + 10,
calib_ref.width / 2 - n_width / 2 * 256 + 50, calib_ref.height / 2 + n_height / 2 * 256 - 10)
def make_background_transparent(im):
im.putalpha(255)
data = numpy.array(im)
red, green, blue, alpha = data.T
box3 = (calib_ref.width / 2 - n_width / 2 * 256 + 10, calib_ref.height / 2 + n_height / 2 * 256 - 50,
calib_ref.width / 2 + n_width / 2 * 256 - 10, calib_ref.height / 2 + n_height / 2 * 256 + 50)
box4 = (calib_ref.width / 2 - n_width / 2 * 256 + 10, calib_ref.height / 2 - n_height / 2 * 256 - 50,
calib_ref.width / 2 + n_width / 2 * 256 - 10, calib_ref.height / 2 - n_height / 2 * 256 + 50)
# If present, remove any "background" areas
background_areas = (red == 221) & (blue == 221) & (green == 221)
data[..., :][background_areas.T] = (0, 0, 0, 0) # make transparent
# Check if there is enough variation at the calibration locations
if compute_variation(calib_ref.crop(box1).convert('L')) < 30 or \
compute_variation(calib_ref.crop(box3).convert('L')) < 30:
return None # Not enough variation
return Image.fromarray(data)
# Take screenshot east and south of it
take_screenshot((XY[0] + n_width, XY[1]), 0, 0, map_config, zoom, screenshots_folder, "calib", "lng", port)
take_screenshot((XY[0], XY[1] + n_height), 0, 0, map_config, zoom, screenshots_folder, "calib", "lat", port)
calib_lat = Image.open(os.path.join(screenshots_folder, f"calib_lat_{zoom}.jpg"))
calib_lng = Image.open(os.path.join(screenshots_folder, f"calib_lng_{zoom}.jpg"))
# Find the best correction factor to bring the two images to be equal on the longitude direction
best_err = None
best_delta_width = 0
for delta_width in range(-15, 16):
calib_box1 = calib_ref.resize((calib_ref.width + delta_width, calib_ref.height)).crop(box1).convert('L')
calib_box2 = calib_lng.resize((calib_ref.width + delta_width, calib_ref.height)).crop(box2).convert('L')
err = compute_difference(calib_box1, calib_box2)
if best_err is None or err < best_err:
best_delta_width = delta_width
best_err = err
# Find the best correction factor to bring the two images to be equal on the latitude direction
best_err = None
best_delta_height = 0
for delta_height in range(-15, 16):
calib_box3 = calib_ref.resize((calib_ref.width, calib_ref.height + delta_height)).crop(box3).convert('L')
calib_box4 = calib_lat.resize((calib_ref.width, calib_ref.height + delta_height)).crop(box4).convert('L')
err = compute_difference(calib_box3, calib_box4)
if best_err is None or err < best_err:
best_delta_height = delta_height
best_err = err
return (best_delta_width, best_delta_height)
def compute_difference(imageA, imageB):
err = numpy.sum((numpy.array(imageA).astype('float') - numpy.array(imageB).astype('float')) ** 2)
err /= float(imageA.width * imageA.height)
return err
def compute_variation(imageA):
min = numpy.min((numpy.array(imageA)))
max = numpy.max((numpy.array(imageA)))
return max - min
def take_screenshot(XY, n_width, n_height, map_config, zoom, screenshots_folder, f, n, port, correction = (0, 0)):
# 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'})
# Try to send the PUT request, up to PUT_RETRIES
retries = PUT_RETRIES
success = False
while not success and retries > 0:
try:
r = requests.put(f'http://127.0.0.1:{port}', data = data)
success = True
except:
retries -= 1
time.sleep(0.5) # Wait for any error to clear
if success == False:
raise Exception(f"Could not fulfill PUT request after {PUT_RETRIES} retries")
geo_data = json.loads(r.text)
time.sleep(0.2)
# 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 each tile should have
m_height = math.radians(deltaLat) * R
m_width = math.radians(deltaLng) * R * math.cos(math.radians(lat1))
# Compute the height and width the tile has
s_height = map_config['mpps'] * 256
s_width = map_config['mpps'] * 256
# Compute the scaling required to achieve that
sx = s_width / m_width
sy = s_height / m_height
# Rotate, resize and save the screenshot
screenshot.rotate(math.degrees(geo_data['northRotation'])).resize((int(sx * screenshot.width) + correction[0], int(sy * screenshot.height)+ correction[1] )).save(os.path.join(screenshots_folder, f"{f}_{n}_{zoom}.jpg"), quality=98)
def run(map_config, port):
global tot_futs, fut_counter
print("Script start time: ", datetime.datetime.now())
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(map_config['tiles_folder']):
os.makedirs(map_config['tiles_folder'])
skip_screenshots = False
if not os.path.exists(os.path.join(output_directory, "screenshots")):
os.mkdir(os.path.join(output_directory, "screenshots"))
if not os.path.exists(os.path.join(map_config['screenshots_folder'])):
skip_screenshots = False
replace_screenshots = True
os.makedirs(os.path.join(map_config['screenshots_folder']))
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"))
skip_screenshots = map_config['skip_screenshots']
replace_screenshots = map_config['replace_screenshots']
# 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
existing_zoom_levels = [int(f) for f in listdir(os.path.join(map_config["tiles_folder"])) if isdir(join(map_config["tiles_folder"], f))]
if len(existing_zoom_levels) == 0:
final_level = 1
else:
final_level = max(existing_zoom_levels)
with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
# Read the config file
doc = bp.read()
@ -218,58 +323,36 @@ def run(map_config, port):
print(f"Feature {f} of {len(features)}, {len(screenshots_XY)} screenshots will be taken")
# Start looping
correction = None
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
if not os.path.exists(os.path.join(map_config['screenshots_folder'], f"{f}_{n}_{zoom}.jpg")) or replace_screenshots:
if n % 10 == 0 or correction is None:
new_correction = compute_correction_factor(XY, n_width, n_height, map_config, zoom, map_config['screenshots_folder'], port)
if new_correction is not None:
correction = new_correction
take_screenshot(XY, n_width, n_height, map_config, zoom, map_config['screenshots_folder'], f, n, port, correction if correction is not None else (0, 0))
# 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))
print_progress_bar(n + 1, len(screenshots_XY))
n += 1
if map_config["screenshots_only"]:
return
########### 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)))
print("Tiles extraction starting at: ", datetime.datetime.now())
if not os.path.exists(os.path.join(map_config["tiles_folder"], str(zoom))):
os.mkdir(os.path.join(map_config["tiles_folder"], str(zoom)))
params = {
"f": f,
"zoom": zoom,
"output_directory": output_directory,
"n_width": n_width,
"n_height": n_height,
"screenshots_folder": map_config['screenshots_folder'],
"tiles_folder": map_config['tiles_folder']
}
# Extract the tiles with parallel thread execution
@ -285,12 +368,16 @@ def run(map_config, port):
print(f"Feature {f} of {len(features)} completed!")
f += 1
if zoom <= final_level:
final_level = map_config['final_level']
########### 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))]
print("Tiles merging start time: ", datetime.datetime.now())
for current_zoom in range(zoom, final_level, -1):
Xs = [int(d) for d in listdir(os.path.join(map_config["tiles_folder"], str(current_zoom))) if isdir(join(map_config["tiles_folder"], 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))]
Ys = [int(f.removesuffix(".png")) for f in listdir(os.path.join(map_config["tiles_folder"], str(current_zoom), str(X))) if isfile(join(map_config["tiles_folder"], str(current_zoom), str(X), f))]
for Y in Ys:
existing_tiles.append((X, Y))
@ -298,20 +385,22 @@ def run(map_config, port):
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)))
if not os.path.exists(os.path.join(map_config["tiles_folder"], str(current_zoom - 1))):
os.mkdir(os.path.join(map_config["tiles_folder"], str(current_zoom - 1)))
futs = [executor.submit(merge_tiles, os.path.join(output_directory, "tiles"), current_zoom, tile) for tile in tiles_to_produce]
futs = [executor.submit(merge_tiles, os.path.join(map_config["tiles_folder"]), 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)]
print("Script end time: ", datetime.datetime.now())