mirror of
https://github.com/Pax1601/DCSOlympus.git
synced 2025-10-29 16:56:34 +00:00
Completed automatic algorithm
This commit is contained in:
parent
2e1c3ec4b9
commit
c74258e3ad
@ -29,127 +29,6 @@ module.exports = function (configLocation) {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/maps/:map/:z/:x/:y.png', async function (req, res, next) {
|
||||
let map = req.params.map;
|
||||
let x = req.params.x;
|
||||
let y = req.params.y;
|
||||
let z = req.params.z;
|
||||
|
||||
if (fs.existsSync(`.\\public\\maps\\${map}`)) {
|
||||
if (!await renderImage(map, z, x, y, res)) {
|
||||
/* No image was found */
|
||||
res.sendStatus(404);
|
||||
}
|
||||
} else {
|
||||
/* The requested map does not exist */
|
||||
res.sendStatus(404);
|
||||
}
|
||||
});
|
||||
|
||||
async function renderImage(map, z, x, y, res, recursionDepth = 0) {
|
||||
if (recursionDepth == 20) {
|
||||
console.log("Render image, maximum recursion depth reached")
|
||||
/* We have reached limit recusion depth, something went wrong */
|
||||
return false;
|
||||
}
|
||||
/* If the requested image exists, send it straight away */
|
||||
if (fs.existsSync(`.\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
|
||||
res.sendFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
|
||||
return true;
|
||||
} else {
|
||||
/* If the requested image doesn't exist check if there is a "source" tile at a lower zoom level we can split up */
|
||||
let sourceZoom = z - 1;
|
||||
let sourceX = Math.floor(x / 2);
|
||||
let sourceY = Math.floor(y / 2);
|
||||
|
||||
/* Keep decreasing the zoom level until we either find an image or reach maximum zoom out and must return false */
|
||||
while (sourceZoom >= 0) {
|
||||
/* We have found a possible source image */
|
||||
if (fs.existsSync(`.\\public\\maps\\${map}\\${sourceZoom}\\${sourceX}\\${sourceY}.png`)) {
|
||||
/* Split the image into four. We can retry up to 10 times to clear any race condition on the files */
|
||||
let retries = 10;
|
||||
while (!await splitTile(map, sourceZoom, sourceX, sourceY) && retries > 0) {
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
retries--;
|
||||
}
|
||||
/* Check if we exited because we reached the maximum retry value */
|
||||
if (retries != 0) {
|
||||
/* Recursively recall the function now that we have a new "source" image */
|
||||
return await renderImage(map, z, x, y, res, recursionDepth + 1);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
/* Keep searching at a higher level */
|
||||
sourceZoom = sourceZoom - 1;
|
||||
sourceX = Math.floor(sourceX / 2);
|
||||
sourceY = Math.floor(sourceY / 2);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function splitTile(map, z, x, y) {
|
||||
try {
|
||||
/* Load the source image */
|
||||
let img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
|
||||
|
||||
/* Create the necessary folders */
|
||||
await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x}`, { recursive: true })
|
||||
await pfs.mkdir(`${process.cwd()}\\public\\maps\\${map}\\${z + 1}\\${2*x + 1}`, { recursive: true })
|
||||
|
||||
/* Split the image into four parts */
|
||||
await resizePromise(img, 0, 0, map, z + 1, 2 * x, 2 * y);
|
||||
img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
|
||||
await resizePromise(img, 128, 0, map, z + 1, 2 * x + 1, 2 * y);
|
||||
img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
|
||||
await resizePromise(img, 0, 128, map, z + 1, 2 * x, 2 * y + 1);
|
||||
img = sharp(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
|
||||
await resizePromise(img, 128, 128, map, z + 1, 2 * x + 1, 2 * y + 1);
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (err.code !== 'EBUSY') {
|
||||
console.error(err);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns a promise, extracts a 128x128 pixel chunk from an image, resizes it to 256x256, and saves it to file */
|
||||
function resizePromise(img, left, top, map, z, x, y) {
|
||||
return new Promise((res, rej) => {
|
||||
if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
|
||||
res(true);
|
||||
}
|
||||
img.extract({ left: left, top: top, width: 128, height: 128 }).resize(256, 256).toFile(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, function (err) {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else {
|
||||
try {
|
||||
fs.renameSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.temp.png`, `${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`);
|
||||
} catch (err) {
|
||||
if (err.code === 'EBUSY') {
|
||||
/* The resource is busy, someone else is writing or renaming it. Reject the promise so we can try again */
|
||||
rej(err);
|
||||
} else if (err.code === 'ENOENT') {
|
||||
/* Someone else renamed the file already so this is not a real error */
|
||||
if (fs.existsSync(`${process.cwd()}\\public\\maps\\${map}\\${z}\\${x}\\${y}.png`)) {
|
||||
res(true);
|
||||
}
|
||||
/* Something odd happened, reject and try again */
|
||||
else {
|
||||
rej(err);
|
||||
}
|
||||
} else {
|
||||
rej(err);
|
||||
}
|
||||
}
|
||||
res(true)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@ -252,7 +252,9 @@ export class Map extends L.Map {
|
||||
var options: L.TileLayerOptions = {
|
||||
attribution: layerData.attribution,
|
||||
minZoom: layerData.minZoom,
|
||||
maxZoom: layerData.maxZoom
|
||||
maxZoom: layerData.maxZoom,
|
||||
minNativeZoom: layerData.minNativeZoom,
|
||||
maxNativeZoom: layerData.maxNativeZoom
|
||||
};
|
||||
this.#layer = new L.TileLayer(layerData.urlTemplate, options);
|
||||
}
|
||||
|
||||
@ -17,9 +17,10 @@
|
||||
},
|
||||
"additionalMaps": {
|
||||
"DCS": {
|
||||
"urlTemplate": "http://localhost:3000/resources/maps/dcs/{z}/{x}/{y}.png",
|
||||
"urlTemplate": "http://localhost:3000/maps/dcs/{z}/{x}/{y}.jpg",
|
||||
"minZoom": 8,
|
||||
"maxZoom": 20,
|
||||
"maxNativeZoom": 17,
|
||||
"attribution": "Eagle Dynamics"
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
"request": "launch",
|
||||
"program": "main.py",
|
||||
"console": "integratedTerminal",
|
||||
"args": ["./configs/LasVegas/LasVegas.yml"]
|
||||
"args": ["./configs/NTTR/config.yml"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
'output_directory': '.\LasVegas', # Where to save the output files
|
||||
'boundary_file': '.\configs\LasVegas\LasVegas.kml'
|
||||
}
|
||||
6
scripts/python/map_generator/configs/LasVegas/config.yml
Normal file
6
scripts/python/map_generator/configs/LasVegas/config.yml
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
'output_directory': '.\LasVegas', # Where to save the output files
|
||||
'boundary_file': '.\configs\LasVegas\boundary.kml', # Input kml file setting the boundary of the map to create
|
||||
'zoom_factor': 0.02, # [0: maximum zoom in (things look very big), 1: maximum zoom out (things look very small)]
|
||||
'geo_width': 1.14
|
||||
}
|
||||
84
scripts/python/map_generator/configs/NTTR/boundary.kml
Normal file
84
scripts/python/map_generator/configs/NTTR/boundary.kml
Normal file
@ -0,0 +1,84 @@
|
||||
<?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>
|
||||
<gx:CascadingStyle kml:id="__managed_style_1847AF2A832F1651A60F">
|
||||
<Style>
|
||||
<IconStyle>
|
||||
<Icon>
|
||||
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&id=2000&scale=4</href>
|
||||
</Icon>
|
||||
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
|
||||
</IconStyle>
|
||||
<LabelStyle>
|
||||
</LabelStyle>
|
||||
<LineStyle>
|
||||
<color>ff2dc0fb</color>
|
||||
<width>4</width>
|
||||
</LineStyle>
|
||||
<PolyStyle>
|
||||
<color>40ffffff</color>
|
||||
</PolyStyle>
|
||||
<BalloonStyle>
|
||||
<displayMode>hide</displayMode>
|
||||
</BalloonStyle>
|
||||
</Style>
|
||||
</gx:CascadingStyle>
|
||||
<gx:CascadingStyle kml:id="__managed_style_2C7F63B5A12F1651A60F">
|
||||
<Style>
|
||||
<IconStyle>
|
||||
<scale>1.2</scale>
|
||||
<Icon>
|
||||
<href>https://earth.google.com/earth/rpc/cc/icon?color=1976d2&id=2000&scale=4</href>
|
||||
</Icon>
|
||||
<hotSpot x="64" y="128" xunits="pixels" yunits="insetPixels"/>
|
||||
</IconStyle>
|
||||
<LabelStyle>
|
||||
</LabelStyle>
|
||||
<LineStyle>
|
||||
<color>ff2dc0fb</color>
|
||||
<width>6</width>
|
||||
</LineStyle>
|
||||
<PolyStyle>
|
||||
<color>40ffffff</color>
|
||||
</PolyStyle>
|
||||
<BalloonStyle>
|
||||
<displayMode>hide</displayMode>
|
||||
</BalloonStyle>
|
||||
</Style>
|
||||
</gx:CascadingStyle>
|
||||
<StyleMap id="__managed_style_043F3D3A202F1651A60F">
|
||||
<Pair>
|
||||
<key>normal</key>
|
||||
<styleUrl>#__managed_style_1847AF2A832F1651A60F</styleUrl>
|
||||
</Pair>
|
||||
<Pair>
|
||||
<key>highlight</key>
|
||||
<styleUrl>#__managed_style_2C7F63B5A12F1651A60F</styleUrl>
|
||||
</Pair>
|
||||
</StyleMap>
|
||||
<Placemark id="0F15269F3D2F1651A60F">
|
||||
<name>NTTR</name>
|
||||
<LookAt>
|
||||
<longitude>-117.2703145690532</longitude>
|
||||
<latitude>37.39557832822189</latitude>
|
||||
<altitude>1754.517427470683</altitude>
|
||||
<heading>359.4706465490362</heading>
|
||||
<tilt>0</tilt>
|
||||
<gx:fovy>35</gx:fovy>
|
||||
<range>1393300.815671235</range>
|
||||
<altitudeMode>absolute</altitudeMode>
|
||||
</LookAt>
|
||||
<styleUrl>#__managed_style_043F3D3A202F1651A60F</styleUrl>
|
||||
<Polygon>
|
||||
<outerBoundaryIs>
|
||||
<LinearRing>
|
||||
<coordinates>
|
||||
-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
|
||||
</coordinates>
|
||||
</LinearRing>
|
||||
</outerBoundaryIs>
|
||||
</Polygon>
|
||||
</Placemark>
|
||||
</Document>
|
||||
</kml>
|
||||
5
scripts/python/map_generator/configs/NTTR/config.yml
Normal file
5
scripts/python/map_generator/configs/NTTR/config.yml
Normal file
@ -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)]
|
||||
}
|
||||
@ -1,10 +1,4 @@
|
||||
{
|
||||
'width': 1920, # The width of your screen, in pixels
|
||||
'height': 1080, # The height of your screen, in pixels
|
||||
'geo_resolution': 1.0 # The resolution of the map on the screen, in meters per pixel.
|
||||
# To measure this value, first set the F10 map at the desired zoom level.
|
||||
# Then, use F10's map measure tool, and measure the width of the screen in meters.
|
||||
# Finally, divide that value by the width in pixels.
|
||||
# A good value would be around 1 meter per pixel, meaning a 1920px wide map would measure about 1 nautical mile across on the F10 map
|
||||
# Lower values will produce higher resolution maps, but beware of space usage!
|
||||
'width': 1920, # The width of your screen, in pixels
|
||||
'height': 1080 # The height of your screen, in pixels
|
||||
}
|
||||
@ -1,5 +1,9 @@
|
||||
import sys
|
||||
import yaml
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
|
||||
from pyproj import Geod
|
||||
from fastkml import kml
|
||||
from shapely import wkt
|
||||
@ -18,20 +22,19 @@ else:
|
||||
screen_config = yaml.safe_load(sp)
|
||||
map_config = yaml.safe_load(cp)
|
||||
|
||||
print("#################################################################################################################################################")
|
||||
print("# IMPORTANT NOTE: the screen properties must be configured according to your screen and desired zoom level. Make sure you set them accordingly. #")
|
||||
print("#################################################################################################################################################")
|
||||
|
||||
print("Screen parameters:")
|
||||
print(f"-> Screen width: {screen_config["width"]}px")
|
||||
print(f"-> Screen height: {screen_config["height"]}px")
|
||||
print(f"-> Geographic resolution: {screen_config["geo_resolution"]} meters/pixel")
|
||||
print(f"-> Screen width: {screen_config['width']}px")
|
||||
print(f"-> Screen height: {screen_config['height']}px")
|
||||
|
||||
print("Map parameters:")
|
||||
print(f"-> Output directory: {map_config["output_directory"]}")
|
||||
print(f"-> Boundary file: {map_config["boundary_file"]}")
|
||||
print(f"-> Output directory: {map_config['output_directory']}")
|
||||
print(f"-> Boundary file: {map_config['boundary_file']}")
|
||||
print(f"-> Zoom factor: {map_config['zoom_factor']}")
|
||||
|
||||
with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp:
|
||||
if 'geo_width' in map_config:
|
||||
print(f"-> Geo width: {map_config['geo_width']}NM")
|
||||
|
||||
with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
|
||||
# Read the config file and compute the total area of the covered map
|
||||
doc = bp.read()
|
||||
k = kml.KML()
|
||||
@ -48,8 +51,17 @@ else:
|
||||
|
||||
print(f"Found {len(features)} features in the provided kml file")
|
||||
|
||||
tile_size = 256 * screen_config["geo_resolution"] # meters
|
||||
tiles_per_screenshot = int(screen_config["width"] / 256) * int(screen_config["height"] / 256)
|
||||
if 'geo_width' not in map_config:
|
||||
# Let the user input the size of the screen to compute resolution
|
||||
data = json.dumps({'lat': features[0].geometry.bounds[1], 'lng': features[0].geometry.bounds[0], 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)})
|
||||
r = requests.put('http://localhost:8080', data = data)
|
||||
print("The F10 map in your DCS installation was setup. Please, use the measure tool and measure the width of the screen in Nautical Miles")
|
||||
map_config['geo_width'] = input("Insert the width of the screen in Nautical Miles: ")
|
||||
|
||||
map_config['mpps'] = float(map_config['geo_width']) * 1852 / screen_config['width']
|
||||
|
||||
tile_size = 256 * map_config['mpps'] # meters
|
||||
tiles_per_screenshot = int(screen_config['width'] / 256) * int(screen_config['height'] / 256)
|
||||
tiles_num = int(area / (tile_size * tile_size))
|
||||
screenshots_num = int(tiles_num / tiles_per_screenshot)
|
||||
total_time = int(screenshots_num / 1.0)
|
||||
@ -57,11 +69,11 @@ else:
|
||||
print(f"Total area: {int(area / 1e6)} square kilometers")
|
||||
print(f"Estimated number of tiles: {tiles_num}")
|
||||
print(f"Estimated number of screenshots: {screenshots_num}")
|
||||
print(f"Estimated time to complete: {timedelta(seconds=total_time)} (hh:mm:ss)")
|
||||
print("The script is ready to go. After you press enter, it will wait for 5 seconds, then it will start.")
|
||||
|
||||
print(f"Estimated time to complete: {timedelta(seconds=total_time * 0.15)} (hh:mm:ss)")
|
||||
input("Press enter to continue...")
|
||||
|
||||
map_generator.run(map_config)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -4,11 +4,14 @@ import pyautogui
|
||||
import time
|
||||
import os
|
||||
import yaml
|
||||
import json
|
||||
|
||||
from fastkml import kml
|
||||
from shapely import wkt, Point
|
||||
from PIL import Image
|
||||
from concurrent import futures
|
||||
from os import listdir
|
||||
from os.path import isfile, isdir, join
|
||||
|
||||
# global counters
|
||||
fut_counter = 0
|
||||
@ -16,6 +19,7 @@ tot_futs = 0
|
||||
|
||||
# constants
|
||||
C = 40075016.686 # meters, Earth equatorial circumference
|
||||
R = C / (2 * math.pi)
|
||||
|
||||
def deg_to_num(lat_deg, lon_deg, zoom):
|
||||
lat_rad = math.radians(lat_deg)
|
||||
@ -48,33 +52,24 @@ def done_callback(fut):
|
||||
fut_counter += 1
|
||||
printProgressBar(fut_counter, tot_futs)
|
||||
|
||||
def extract_tiles(n, screenshots_coordinates, params):
|
||||
f = params["f"]
|
||||
zoom = params["zoom"]
|
||||
output_directory = params["output_directory"]
|
||||
n_width = params["n_width"]
|
||||
n_height = params["n_height"]
|
||||
screen_resolution = params["screen_resolution"]
|
||||
mpps = params["mpps"]
|
||||
def extract_tiles(n, screenshots_XY, params):
|
||||
f = params['f']
|
||||
zoom = params['zoom']
|
||||
output_directory = params['output_directory']
|
||||
n_width = params['n_width']
|
||||
n_height = params['n_height']
|
||||
|
||||
coords = screenshots_coordinates[n]
|
||||
XY = screenshots_XY[n]
|
||||
if (os.path.exists(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))):
|
||||
# Open the source screenshot
|
||||
img = Image.open(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))
|
||||
|
||||
# Scale the image so that tiles are 256x256
|
||||
scale = screen_resolution / mpps
|
||||
w, h = img.size
|
||||
img = img.resize((int(w * scale), int(h * scale)))
|
||||
|
||||
# Compute the Web Mercator Projection position of the top left corner of the most centered tile
|
||||
lat = coords[0]
|
||||
lng = coords[1]
|
||||
X_center, Y_center = deg_to_num(lat, lng, zoom)
|
||||
X_center, Y_center = XY[0], XY[1]
|
||||
|
||||
# Compute the position of the top left corner of the top left tile
|
||||
start_x = w / 2 - n_width / 2 * 256
|
||||
start_y = h / 2 - n_height / 2 * 256
|
||||
start_x = img.width / 2 - n_width / 2 * 256
|
||||
start_y = img.height / 2 - n_height / 2 * 256
|
||||
|
||||
# Iterate on the grid
|
||||
for column in range(0, n_width):
|
||||
@ -96,17 +91,44 @@ def extract_tiles(n, screenshots_coordinates, params):
|
||||
n += 1
|
||||
|
||||
else:
|
||||
raise Exception(f"{os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg")} missing")
|
||||
raise Exception(f"{os.path.join(output_directory, 'screenshots', f'{f}_{n}.jpg')} missing")
|
||||
|
||||
def merge_tiles(base_path, zoom, tile):
|
||||
X = tile[0]
|
||||
Y = tile[1]
|
||||
|
||||
positions = [(0, 0), (0, 1), (1, 0), (1, 1)]
|
||||
|
||||
dst = Image.new('RGB', (256, 256), (0, 0, 0, 0))
|
||||
for i in range(0, 4):
|
||||
if os.path.exists(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")):
|
||||
im = Image.open(os.path.join(base_path, str(zoom), str(2*X + positions[i][0]), f"{2*Y + positions[i][1]}.jpg")).resize((128, 128))
|
||||
else:
|
||||
im = Image.new('RGB', (128, 128), (0, 0, 0, 0))
|
||||
dst.paste(im, (positions[i][0] * 128, positions[i][1] * 128))
|
||||
|
||||
if not os.path.exists(os.path.join(base_path, str(zoom - 1), str(X))):
|
||||
try:
|
||||
os.mkdir(os.path.join(base_path, str(zoom - 1), str(X)))
|
||||
except FileExistsError:
|
||||
pass
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
dst.save(os.path.join(base_path, str(zoom - 1), str(X), f"{Y}.jpg"), quality=95)
|
||||
|
||||
def run(map_config):
|
||||
global tot_futs, fut_counter
|
||||
|
||||
with open('configs/screen_properties.yml', 'r') as sp:
|
||||
screen_config = yaml.safe_load(sp)
|
||||
|
||||
# Create output folders
|
||||
output_directory = map_config["output_directory"]
|
||||
output_directory = map_config['output_directory']
|
||||
if not os.path.exists(output_directory):
|
||||
os.mkdir(output_directory)
|
||||
|
||||
skip_screenshots = False
|
||||
if not os.path.exists(os.path.join(output_directory, "screenshots")):
|
||||
os.mkdir(os.path.join(output_directory, "screenshots"))
|
||||
else:
|
||||
@ -116,10 +138,10 @@ def run(map_config):
|
||||
os.mkdir(os.path.join(output_directory, "tiles"))
|
||||
|
||||
# Compute the optimal zoom level
|
||||
usable_width = screen_config["width"] - 200 # Keep a margin around the center
|
||||
usable_height = screen_config["height"] - 200 # Keep a margin around the center
|
||||
usable_width = screen_config['width'] - 400 # Keep a margin around the center
|
||||
usable_height = screen_config['height'] - 400 # Keep a margin around the center
|
||||
|
||||
with open(map_config["boundary_file"], 'rt', encoding="utf-8") as bp:
|
||||
with open(map_config['boundary_file'], 'rt', encoding="utf-8") as bp:
|
||||
# Read the config file
|
||||
doc = bp.read()
|
||||
k = kml.KML()
|
||||
@ -134,7 +156,8 @@ def run(map_config):
|
||||
# Iterate over all the closed features in the kml file
|
||||
f = 1
|
||||
for feature in features:
|
||||
geo = sub_feature.geometry
|
||||
########### Take screenshots
|
||||
geo = feature.geometry
|
||||
|
||||
# Define the boundary rect around the area
|
||||
start_lat = geo.bounds[3]
|
||||
@ -143,15 +166,14 @@ def run(map_config):
|
||||
end_lng = geo.bounds[2]
|
||||
|
||||
# Find the zoom level that better approximates the provided resolution
|
||||
screen_resolution = screen_config['geo_resolution']
|
||||
mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - screen_resolution) for z in range(0, 21)]
|
||||
mpps_delta = [abs(compute_mpps((start_lat + end_lat) / 2, z) - map_config['mpps']) for z in range(0, 21)]
|
||||
zoom = mpps_delta.index(min(mpps_delta))
|
||||
|
||||
print(f"Feature {f} of {len(features)}, using zoom level {zoom}")
|
||||
|
||||
# Find the maximum dimension of the tiles at the given resolution
|
||||
mpps = compute_mpps(end_lat, zoom)
|
||||
d = 256 * mpps / screen_resolution
|
||||
d = 256 * mpps / map_config['mpps']
|
||||
|
||||
n_height = math.floor(usable_height / d)
|
||||
n_width = math.floor(usable_width / d)
|
||||
@ -163,35 +185,60 @@ def run(map_config):
|
||||
end_X, end_Y = deg_to_num(end_lat, end_lng, zoom)
|
||||
|
||||
# Find all the X, Y coordinates inside of the provided area
|
||||
screenshots_coordinates = []
|
||||
screenshots_XY = []
|
||||
for X in range(start_X, end_X, n_width):
|
||||
for Y in range(start_Y, end_Y, n_height):
|
||||
lat, lng = num_to_deg(X, Y, zoom)
|
||||
p = Point(lng, lat)
|
||||
if p.within(wkt.loads(geo.wkt)):
|
||||
screenshots_coordinates.append((lat, lng))
|
||||
screenshots_XY.append((X, Y))
|
||||
|
||||
print(f"Feature {f} of {len(features)}, {len(screenshots_coordinates)} screenshots will be taken")
|
||||
print(f"Feature {f} of {len(features)}, {len(screenshots_XY)} screenshots will be taken")
|
||||
|
||||
# Start looping
|
||||
if not skip_screenshots:
|
||||
print(f"Feature {f} of {len(features)}, taking screenshots...")
|
||||
n = 0
|
||||
for coords in screenshots_coordinates:
|
||||
for XY in screenshots_XY:
|
||||
# Making PUT request
|
||||
#data = json.dumps({'lat': coords[0], 'lng': coords[1]})
|
||||
#r = requests.put('http://localhost:8080', data = data)
|
||||
# If the number of rows or columns is odd, we need to take the picture at the CENTER of the tile!
|
||||
lat, lng = num_to_deg(XY[0] + (n_width % 2) / 2, XY[1] + (n_height % 2) / 2, zoom)
|
||||
data = json.dumps({'lat': lat, 'lng': lng, 'alt': 1350 + map_config['zoom_factor'] * (25000 - 1350)})
|
||||
r = requests.put('http://localhost:8080', data = data)
|
||||
|
||||
geo_data = json.loads(r.text)
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
## Take and save screenshot
|
||||
# Take and save screenshot. The response to the put request contains data, among which there is the north rotation at that point.
|
||||
screenshot = pyautogui.screenshot()
|
||||
screenshot.save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"))
|
||||
|
||||
printProgressBar(n + 1, len(screenshots_coordinates))
|
||||
# Scale the screenshot to account for Mercator Map Deformation
|
||||
lat1, lng1 = num_to_deg(XY[0], XY[1], zoom)
|
||||
lat2, lng2 = num_to_deg(XY[0] + 1, XY[1] + 1, zoom)
|
||||
|
||||
deltaLat = abs(lat2 - lat1)
|
||||
deltaLng = abs(lng2 - lng1)
|
||||
|
||||
# Compute the height and width the screenshot should have
|
||||
m_height = math.radians(deltaLat) * R * n_height
|
||||
m_width = math.radians(deltaLng) * R * math.cos(math.radians(lat1)) * n_width
|
||||
|
||||
# Compute the height and width the screenshot has
|
||||
s_height = map_config['mpps'] * 256 * n_height
|
||||
s_width = map_config['mpps'] * 256 * n_width
|
||||
|
||||
# Compute the scaling required to achieve that
|
||||
sx = s_width / m_width
|
||||
sy = s_height / m_height
|
||||
|
||||
# Resize, rotate and save the screenshot
|
||||
screenshot.resize((int(sx * screenshot.width), int(sy * screenshot.height))).rotate(math.degrees(geo_data['northRotation'])).save(os.path.join(output_directory, "screenshots", f"{f}_{n}.jpg"), quality=95)
|
||||
|
||||
printProgressBar(n + 1, len(screenshots_XY))
|
||||
n += 1
|
||||
|
||||
# Extract the tiles
|
||||
########### Extract the tiles
|
||||
if not os.path.exists(os.path.join(output_directory, "tiles", str(zoom))):
|
||||
os.mkdir(os.path.join(output_directory, "tiles", str(zoom)))
|
||||
|
||||
@ -201,15 +248,12 @@ def run(map_config):
|
||||
"output_directory": output_directory,
|
||||
"n_width": n_width,
|
||||
"n_height": n_height,
|
||||
"screen_resolution": screen_resolution,
|
||||
"mpps": mpps
|
||||
}
|
||||
|
||||
# Extract the tiles with parallel thread execution
|
||||
with futures.ThreadPoolExecutor() as executor:
|
||||
print(f"Feature {f} of {len(features)}, extracting tiles...")
|
||||
global tot_futs, fut_counter
|
||||
futs = [executor.submit(extract_tiles, n, screenshots_coordinates, params) for n in range(0, len(screenshots_coordinates))]
|
||||
futs = [executor.submit(extract_tiles, n, screenshots_XY, params) for n in range(0, len(screenshots_XY))]
|
||||
tot_futs = len(futs)
|
||||
fut_counter = 0
|
||||
[fut.add_done_callback(done_callback) for fut in futs]
|
||||
@ -219,6 +263,36 @@ def run(map_config):
|
||||
print(f"Feature {f} of {len(features)} completed!")
|
||||
f += 1
|
||||
|
||||
########### Assemble tiles to get lower zoom levels
|
||||
for current_zoom in range(zoom, 8, -1):
|
||||
Xs = [int(d) for d in listdir(os.path.join(output_directory, "tiles", str(current_zoom))) if isdir(join(output_directory, "tiles", str(current_zoom), d))]
|
||||
existing_tiles = []
|
||||
for X in Xs:
|
||||
Ys = [int(f.removesuffix(".jpg")) for f in listdir(os.path.join(output_directory, "tiles", str(current_zoom), str(X))) if isfile(join(output_directory, "tiles", str(current_zoom), str(X), f))]
|
||||
for Y in Ys:
|
||||
existing_tiles.append((X, Y))
|
||||
|
||||
tiles_to_produce = []
|
||||
for tile in existing_tiles:
|
||||
if (int(tile[0] / 2), int(tile[1] / 2)) not in tiles_to_produce:
|
||||
tiles_to_produce.append((int(tile[0] / 2), int(tile[1] / 2)))
|
||||
|
||||
# Merge the tiles with parallel thread execution
|
||||
with futures.ThreadPoolExecutor() as executor:
|
||||
print(f"Merging tiles for zoom level {current_zoom - 1}...")
|
||||
|
||||
if not os.path.exists(os.path.join(output_directory, "tiles", str(current_zoom - 1))):
|
||||
os.mkdir(os.path.join(output_directory, "tiles", str(current_zoom - 1)))
|
||||
|
||||
futs = [executor.submit(merge_tiles, os.path.join(output_directory, "tiles"), current_zoom, tile) for tile in tiles_to_produce]
|
||||
tot_futs = len(futs)
|
||||
fut_counter = 0
|
||||
[fut.add_done_callback(done_callback) for fut in futs]
|
||||
[fut.result() for fut in futures.as_completed(futs)]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user