mstr_photores = 2048 # <- Change this to 4096 for 16k resolution
# Radius of zoom level 18 aerials around airports with ICAO code
-mstr_airport_radius = 2
+# Value is in tiles - not in km
+#
+# Value = 5:
+# #####
+# #####
+# ##X##
+# #####
+# #####
+#
+# The "X" is the tile with the airport
+mstr_airport_radius = 5
# Clear cache after generating a complete tile?
mstr_clear_cache = True
("landuse", "farmland", 15),
("landuse", "farmyard", 15),
# Z-Order 2
- ("landuse", "forest", 15),
- ("leisure", "nature_reserve", 15),
+ ("landuse", "forest", 20),
+ ("leisure", "nature_reserve", 20),
("landuse", "military", 30),
# Z-Order 3
("natural", "bare_rock", 25),
("building", "industrial", 1),
("building", "yes", 1)
]
-
-
-# Define tile main colors by latitude-longitude region
-mstr_base_colors = [
- ((50,0), 100, 106, 77)
-]
return tiles
+# Find the tiles to keep around an airport, using the defined tile
+# radius amount in defines.py
+def findAirportTiles(av, ah):
+ # The tiles
+ tiles=[]
+
+ # Starting points
+ sty = av - int(mstr_airport_radius/2)
+ stx = ah - int(mstr_airport_radius/2)
+
+ for y in range(mstr_airport_radius):
+ for x in range(mstr_airport_radius):
+ a = ( sty+y, stx+x )
+ tiles.append(a)
+
+ # Return the tiles
+ return tiles
+
# Testing
def in_circle(center_x, center_y, radius, x, y):
ry = randrange(30,60)
f = randrange(1,10)
- # Do some magic
- if f != 5:
- imgd.ellipse((x-int(rx/2), y-int(ry/2), x+rx, y+ry), fill="black")
- if f == 3 or f == 7:
- imgd.ellipse((x-int(rx/2), y-int(ry/2), x+rx, y+ry), fill=(0,0,0,0))
+ # Do some magic - but not on edges
+ if x > 0 and x < osm_mask.width and y > 0 and y < osm_mask.height:
+ if f != 5:
+ imgd.ellipse((x-int(rx/2), y-int(ry/2), x+rx, y+ry), fill="black")
+ if f == 3 or f == 7:
+ imgd.ellipse((x-int(rx/2), y-int(ry/2), x+rx, y+ry), fill=(0,0,0,0))
# We need to change the image in certain conditions
# Let's try our hand at pseudo shadows
if mstr_shadow_enabled == True:
- shadow = Image.new("RGBA", (self._imgsize, self._imgsize))
- for sh in mstr_shadow_casters:
- if self._tag == sh[0] and self._value == sh[1]:
- mstr_msg("mstr_layergen", "Generating shadow for layer")
- shadow_pix = shadow.load()
- mask_pix = osm_mask.load()
- for y in range(self._imgsize-1):
- for x in range(self._imgsize-1):
- m = mask_pix[x,y]
- shf_x = x + mstr_shadow_shift
- if shf_x <= self._imgsize-1:
- a = mask_pix[x,y][3]
- st = random.uniform(0.45, mstr_shadow_strength)
- ca = a * st
- aa = int(ca)
- shadow_pix[shf_x, y] = (0,0,0,aa)
- shadow.save(mstr_datafolder + "_cache\\" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_shadow.png")
- mstr_msg("mstr_layergen", "Shadow layer completed")
+ if mstr_shadow_shift >= 2:
+ shadow = Image.new("RGBA", (self._imgsize, self._imgsize))
+ for sh in mstr_shadow_casters:
+ if self._tag == sh[0] and self._value == sh[1]:
+ mstr_msg("mstr_layergen", "Generating shadow for layer")
+ shadow_pix = shadow.load()
+ mask_pix = osm_mask.load()
+ for y in range(self._imgsize-1):
+ for x in range(self._imgsize-1):
+ m = mask_pix[x,y]
+ shf_x = 0
+ # Buildings get slightly closer shadows
+ if self._tag == "building":
+ shf_x = x + int(mstr_shadow_shift/2)
+ if self._tag != "building":
+ shf_x = x + mstr_shadow_shift
+ if shf_x <= self._imgsize-1:
+ a = mask_pix[x,y][3]
+ st = 0
+ if self._tag == "building":
+ st = random.uniform(0.25, mstr_shadow_strength/2)
+ if self._tag != "building":
+ st = random.uniform(0.45, mstr_shadow_strength)
+ ca = a * st
+ aa = int(ca)
+ shadow_pix[shf_x, y] = (0,0,0,aa)
+ shadow.save(mstr_datafolder + "_cache\\" + str(self._latitude) + "-" + str(self._lat_number) + "_" + str(self._longitude) + "-" + str(self._lng_number) + "_" + self._tag + "-" + self._value + "_layer_shadow.png")
+ mstr_msg("mstr_layergen", "Shadow layer completed")
mstr_msg("mstr_layergen", "Layer image finalized and saved.")
-
-
-'''
-lg1 = mstr_layergen("landuse", "forest", 51, 1, 7, 1)
-lg1.genlayer()
-lg2 = mstr_layergen("landuse", "farmland", 51, 1, 7, 1)
-lg2.genlayer()
-lg3 = mstr_layergen("leisure", "golf_course", 51, 1, 7, 1)
-lg3.genlayer()
-
-l = Image.new("RGBA", (3000, 3000))
-l1 = Image.open("M:\\Developer\\Projects\\orthographic\\_cache\\51-1_7-1_landuse-forest_layer.png")
-l2 = Image.open("M:\\Developer\\Projects\\orthographic\\_cache\\51-1_7-1_landuse-farmland_layer.png")
-l3 = Image.open("M:\\Developer\\Projects\\orthographic\\_cache\\51-1_7-1_leisure-golf_course_layer.png")
-l.alpha_composite(l3)
-l.alpha_composite(l2)
-l.alpha_composite(l1)
-l.save("M:\\layer.png")
-'''
mask_img.save(mstr_datafolder + "_cache\\" + fstr + "_" + self._tag + "-" + self._value + ".png")
# Inform
mstr_msg("mstr_maskgen", "Mask built.")
-
-
-#mg = mstr_maskgen([51, 1, 7, 1], 0.0100691262567974, "building", "yes", False)
-#mg = mstr_maskgen([51, 1, 7, 1], 0.0100691262567974, "natural", "bare_rock")
-#mg = mstr_maskgen([51, 1, 7, 1], 0.0100691262567974, "highway", "track")
-#mg = mstr_maskgen([51, 1, 7, 1], 0.0100691262567974, "landuse", "forest", False)
-#mg = mstr_maskgen([51, 1, 7, 1], 0.0100691262567974, "natural", "water")
-#mg = mstr_maskgen([51, 1, 7, 1], 0.0100691262567974, "leisure", "golf_course")
-#mg = mstr_maskgen([51, 2, 7, 5], 0.0100691262567974, "boundary", "administrative", "admin_level", ["6"])
-#mg._build_mask()
\ No newline at end of file
+
\ No newline at end of file
from layergen import *
from photogen import *
from osmxml import *
+from tilegen import *
# The main class which handles the rest
class mstr_orthographic:
+ # It did happen that the generation of photos crashed as, for some reason,
+ # a file in _cache was apparently used by another process (hint: it was
+ # not). I therefore need this test before deleting a file in _cache, so
+ # that generation of the orthos can move forward.
+ def _isFileAccessibleWin(self, src):
+ a = False
+ if os.path.isfile(src) == True:
+ try:
+ os.rename(src, src)
+ a = True
+ except OSError as e:
+ a = False
+ return a
+
+ # Need a same call for POSIX
+ def _isFileAccessiblePosix(self, src):
+ wildcard = "/proc/*/fd/*"
+ lfds = glob.glob(wildcard)
+ for fds in lfds:
+ try:
+ file = os.readlink(fds)
+ if file == src:
+ return True
+ except OSError as err:
+ if err.errno == 2:
+ file = None
+ else:
+ raise(err)
+ return False
+
+
# This will determine the vertical stepping in degrees in order to generate
# masks with a 1:1 square ratio. This is important as X-Plane textures for
# orthos can only be a power of 2, such as 2048x2048
def _buildTile(self):
mstr_msg("mstr_orthographic", "Beginning construction of tile")
+ # We need to know which platform we are on
+ os_platform = os.name
+
# Create the _cache folder, should it not exist.
# Temporary images for the ortho tile generation go here
if not os.path.exists(self._output + "/_cache"):
osmxml = mstr_osmxml(0,0)
mstr_msg("mstr_orthographic", "Set initial coordinates and bounding box for OSM acquisition")
+ # The highest encountered tile numbers
+ # This is needed to produce the zoom level 16 images
+ top_lat = 1
+ top_lng = 1
+
# Previously, I downloaded all XML files in one go - but to ease the
# stress on OSM servers and my server, we will do acquire the data
# only for the current processed part of the tile.
print("")
print("")
- # Store a terrain file
- '''
- dmt = self._findWidthOfLongitude(bb_lat) * mstr_zl_18
- sy = bb_lat + (self._vstep / 2)
- sx = bb_lng + (mstr_zl_18 / 2)
- ter_content = """A
-800
-TERRAIN
-
-LOAD_CENTER """ + str(sy) + " " + str(sx) + " " + str(dmt) + " " + str(mstr_photores) + """
-BASE_TEX_NOWRAP ../Textures/"""+str(cur_tile_y)+"_"+str(cur_tile_x)+".jpg"+"""
-NO_ALPHA"""
- with open(self._output + "\\Tiles\\"+str(bb_lat)+"_"+str(bb_lng)+"\\terrain\\"+str(cur_tile_y)+"_"+str(cur_tile_x)+".ter", 'w') as textfile:
- textfile.write(ter_content)
- mstr_msg("mstr_orthographic", "Wrote .ter file")
- '''
-
# Adjust longitude coordinates
cur_tile_x = cur_tile_x+1
bb_lng = bb_lng + mstr_zl_18
bb_lng_edge = bb_lng_edge + mstr_zl_18
mstr_msg("mstr_orthographic", "Adjustment of longitude performed")
+ # Adjust peak longitude tile number
+ if cur_tile_x > top_lng:
+ top_lng = cur_tile_x
# Clear out cache
if mstr_clear_cache == True:
ch = glob.glob(mstr_datafolder + "_cache\\*")
for f in ch:
- os.remove(f)
+ if os_platform == "nt":
+ if self._isFileAccessibleWin(f) == True:
+ os.remove(f)
+ if os_platform == "posix":
+ if self._isFileAccessiblePosix(f) == True:
+ os.remove(f)
mstr_msg("mstr_orthographic", "Cleared cache")
bb_lat = bb_lat + self._vstep
bb_lat_edge = bb_lat_edge + self._vstep
mstr_msg("mstr_orthographic", "Adjustment of latitude performed")
+ # Adjust peak latitude number
+ if cur_tile_y > top_lat:
+ top_lat = cur_tile_y
+
+ mstr_msg("mstr_orthographic", "Generation of all tiles completed!")
+
+ mstr_msg("mstr_orthographic", "Generating ZL16 tiles and keeping airport tiles")
+ tg = mstr_tilegen(self._lat, self._lng, self._vstep, top_lat, top_lng)
+ tg.genTiles()
+ mstr_msg("mstr_orthographic", "Final step completed.")
+ print("")
+ print("")
+ mstr_msg("mstr_orthographic", "Tile data in: " + mstr_datafolder + "\\Tiles\\" + str(self._lat) + "_" + self._lng)
+ mstr_msg("mstr_orthographic", "Orthos are in the Textures subfolder")
+ mstr_msg("mstr_orthographic", "X-Plane .ter's are in the terrain subfolder")
+ print("")
+ print("")
+ mstr_msg("mstr_orthographic", "Thanks for using Orthographic! -- Best, Marcus")
+ print("")
+
+
- Open the file defines.py
- Change mstr_datafolder to where you want the data of Orthographic to be stored and placed
-- Change mstr_airport_radius to an amount in kilometers. Areas around airports with ICAO code will get aerials with zoom level 18
+- Change mstr_airport_radius to an amount in zoom level 18 tiles. Areas around airports with ICAO code will get aerials with zoom level 18. I recommend to leave the default at 5
In this file you can also define how large you want the final products to be - you can choose between 4k resolution (2048px) or 16k resolution (4096). The choice you make here should be based on 1) how much detail you want, and 2) how much VRAM your GPU has. The larger a texture, the more VRAM is needed, and the more processing is required by the GPU. I personally go with 2048, as I have a RTX2060, so my VRAM is limited. When at altitudes of 10,000 feet and above, you will most definitely not notice the difference between 4096 and 2048.
+NOTE! 4096 not yet implemented, but planned.
+
Change the mstr_photores variable to 4096 if you want the maximum possible.
Just a note: 4096 also uses 4x more hard drive space. 4k uses about 2MB per image, where as 16k uses about 8-10MB per image. This may not seem much for one image, but keep in mind we are talking about quite a considerable number of images. To get an idea - if you have it, look into any folder of an Ortho4XP tile in your X-Plane folder, and check the size of the "textures" folder.
Very simple.
-[codebox]python main.py LATITUDE LONGITUDE[/codebox]
+[codebox]python og.py LATITUDE LONGITUDE[/codebox]
So for example
As I have hosted this on my private but publicly accessible gitweb (which is the same tool kernel.org uses btw), it should be clear that I am making the code available to everyone. Of course, you are free to improve (I am sure the code needs some optimisation). If you changed or improved something, and you publish it (no matter where), you MUST adhere to the license this software ships with, which is OSL 3.0.
As it is hosted on my private gitweb, and not in a public instance like GitHub, I need to be very careful and clear about this. In short it means, also legally, that you cannot download the current snapshot, post the content somewhere else, and claim you made it. Respect the time, work and energy I have invested into this project :)
+
rws = r.fetchall()
return rws
+ # Get all tiles with detected airports (ICAO codes)
+ def get_tiles_with_airports(self):
+ r = self._crs.execute("SELECT * FROM airports")
+ rws = r.fetchall
+ return rws
+
# Perform a custom query and retrieve results
def perform_query(self, qry):
from log import *
from functions import *
from tiledb import *
+import math
+import os
+import glob
class mstr_tilegen:
# We only need some values. Also sets up connection to DB
- def __init__(self, lat, lng, lngw, vstep):
- self._lat = lat
- self._lng = lng
- self._lngw = lngw
- self._vstep = vstep
+ def __init__(self, lat, lng, vstep, max_lat, max_lng):
+ self._lat = lat
+ self._lng = lng
+ self._vstep = vstep
+ self._maxlat = max_lat
+ self._maxlng = max_lng
# Connection to DB
self._tiledb = mstr_tiledb(lat, lng)
+ mstr_msg("mstr_tilegen", "Tilegen initialized")
- # Generates the ZL16 tiles and stores them
+ # To write down X-Plane .ter files, we will need to know the exact size
+ # of the particular longitude we are in, as this value varies depending
+ # on where you are on a sphere.
+ # Returned values is in meters.
+ # The current latitude is needed.
+ def _findWidthOfLongitude(self, lat):
+ dm = math.cos(math.radians(lat)) * 111.321 # <- 1 deg width at equator in km
+ return round(dm * 1000, 3)
+
+
+ # Generates the ZL16 tiles and stores them.
+ # We generate ZL16 tiles first, then we check which tiles to keep near airports
def genTiles(self):
- pass
\ No newline at end of file
+ # The current lat and lng tile numbers
+ cur_lat = 1
+ cur_lng = 1
+
+ # Actual starting coordinates for ZL16
+ a_lat = self._lat + self._vstep * 2
+ a_lng = self._lng + mstr_zl_18 * 2
+
+ # Scaled res
+ scaled_res = int(mstr_photores/4) # For example, 512 for a photo res of 2048
+
+ # Find out how many steps we can walk in every direction
+ steps_lat = int(math.ceil(self._maxlat/4))
+ steps_lng = int(math.ceil(self._maxlng/4))
+ mstr_msg("mstr_tilegen", "Latitude and longitude steps determined")
+
+ # OK... so. Let's finish this.
+ for lt in range(1, steps_lat):
+ for ln in range(1, steps_lng):
+ # Find out which tiles to process
+ tiles = findZL16tiles(cur_lat, cur_lng)
+
+ # Generate the ZL16 image
+ zl16 = Image.new("RGB", (mstr_photores, mstr_photores))
+
+ # Walk through this array
+ xpos = 0
+ ypos = int(scaled_res*3)
+ for i in range(0, 3):
+ for j in range(0, 3):
+ # We may run into situations where ask for tiles that don't exist...
+ # Let's make sure we can continue
+ fpath = mstr_datafolder + "Tiles\\" + str(self._lat) + "_" + str(self._lng) + "\\Textures\\" + str(tiles[i][j][0]) + "_" + str(tiles[i][j][1]) + ".jpg"
+ if os.path.isfile( fpath ):
+ tlimg = Image.open(fpath)
+ tlimg = tlimg.resize((scaled_res,scaled_res), Image.Resampling.BILINEAR)
+ zl16.paste(tlimg, (xpos, ypos))
+ xpos = xpos + scaled_res
+ xpos = 0
+ ypos = ypos - scaled_res
+
+ # Now save this image
+ zl16.save(mstr_datafolder + "Tiles\\" + str(self._lat) + "_" + str(self._lng) + "\\Textures\\" + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG16.jpg", format='JPEG', subsampling=0, quality=100)
+
+ # Store a terrain file
+ dmt = self._findWidthOfLongitude(a_lat) * mstr_zl_16
+ ter_content = """A
+800
+TERRAIN
+
+LOAD_CENTER """ + str(a_lat) + " " + str(a_lng) + " " + str(dmt) + " " + "../Textures/" + str(self._lat) + "-" + str(ln) + "_" + str(self._lng) + "-" + str(lt) + "_OG_16.jpg"+"""
+NO_ALPHA"""
+ with open(mstr_datafolder + "\\Tiles\\"+str(self._lat)+"_"+str(self._lng)+"\\terrain\\"+str(self._lat)+"_"+str(lt)+"-"+str(self._lng)+"-"+str(ln)+"_OG16.ter", 'w') as textfile:
+ textfile.write(ter_content)
+ mstr_msg("mstr_tilegen", "Wrote .ter file")
+
+
+ # Adjust
+ a_lng = a_lng + (mstr_zl_16 * 4)
+ cur_lng = cur_lng + 4
+ mstr_msg("mstr_tilegen", "Adjusted coordinate values")
+
+ # Adjust
+ a_lng = self._lat + (mstr_zl_16 * 2)
+ a_lat = a_lat + (self._vstep * 4)
+ cur_lat = cur_lat + 4
+ cur_lng = self._lng
+ mstr_msg("mstr_tilegen", "Adjusted coordinate values for next tile loop")
+
+ mstr_msg("mstr_tilegen", "Tile generation... completed (wow.jpg)")
+
+
+ # BUT! This is not the end. Yet.
+
+ # Make sure we keep tiles around airports.
+ airports = self._tiledb.get_tiles_with_airports()
+ mstr_msg("mstr_tilegen", "Filtering ZL18 tiles for airports")
+
+ # The ZL 18 tiles to keep in the end
+ tiles = []
+ mstr_msg("mstr_tilegen", "Finding ZL18 tiles to keep")
+ for a in airports:
+ tiles.append(findAirportTiles(int(a[1]), int(a[2])))
+ mstr_msg("mstr_tilegen", "Determined ZL18 tiles")
+
+ # Create a final array to make life easier
+ mstr_msg("mstr_tilegen", "Generating arrays for tiles to keep")
+ keeping = []
+ for t in tiles:
+ for i in t:
+ keeping.append(i)
+
+ # Perform the cleanup
+ mstr_msg("mstr_tilegen", "Cleaning up non-needed tiles")
+ for y in range(1, self._maxlat):
+ for x in range(1, self._maxlng):
+ fn = str(y) + "_" + str(x) + ".jpg"
+ found = False
+ for k in keeping:
+ kfn = str(k[0]) + "_" + str(k[1]) + ".jpg"
+ if fn == kfn:
+ found = True
+ break
+ if found == False:
+ os.remove(mstr_datafolder + "\\Tiles\\" + str(self._lat) + "_" + str(self._lng) + "\\Textures\\" + fn)
+ mstr_msg("mstr_tilegen", "Cleanup completed")
+
+
+ # And now for the final act of tonight's entertainment
+ mstr_msg("mstr_tilegen", "Writing .ter files for ZL18 tiles")
+
+ for k in keeping:
+ k_lat = self._lat + (k[0] * self._vstep) + (self._vstep * 0.5)
+ k_lng = self._lat + (k[1] * mstr_zl_18) + (mstr_zl_18 * 0.5)
+ k_dmt = self._findWidthOfLongitude(self._lng * mstr_zl_18)
+ k_fln = mstr_datafolder + "\\Tiles\\" + str(self._lat) + "_" + str(self._lng) + "\\terrain\\" + str(k[0]) + "_" + str(k[1]) + ".ter"
+ ter_content = """A
+800
+TERRAIN
+
+LOAD_CENTER """ + str(k_lat) + " " + str(k_lng) + " " + str(k_dmt) + " " + "../Textures/" + str(k[0]) + "_" + str(k[1]) + ".jpg"+"""
+NO_ALPHA"""
+ with open(k_fln, 'w') as textfile:
+ textfile.write(ter_content)
+ mstr_msg("mstr_tilegen", "Wrote all .ter files for ZL18 tiles.")
+
+ mstr_msg("mstr_tilegen", "Work complete.")
+
+
+
+'''
+ Did we fly to the moon too soon?
+ Did we squander the chance?
+ In the rush of the race
+ The reason we chase is lost in romance
+ And still we try
+ To justify the waste
+ For a taste of man's greatest adventure
+
+ I blame you for the moonlit sky
+ And the dream that died
+ With the eagles' flights
+ I blame you for the moonlit nights
+ When I wonder why
+ Are the seas still dry
+ Don't blame this sleeping satellite
+'''