# We will simply things to achieve good-looking results.
# You can, however, disable the shadow rendering layer here.
mstr_shadow_enabled = True
-mstr_shadow_strength = 0.65
+mstr_shadow_strength = 0.85
mstr_shadow_shift = 24
# The tags that cast shadows
mstr_shadow_casters = [
("landuse", "greenfield", 30),
("landuse", "orchard", 30),
("landuse", "meadow", 30),
- ("barrier", "hedge", 12),
- ("landuse", "recreation_ground", 30),
+ ("barrier", "hedge", 5),
+ ("landuse", "recreation_ground", 20),
("landuse", "vineyard", 30),
("natural", "grassland", 30),
("natural", "wetland", 30),
- ("natural", "scrub", 30),
- ("natural", "heath", 30),
+ ("natural", "scrub", 20),
+ ("natural", "heath", 20),
("leisure", "park", 30),
- ("leisure", "golf_course", 35),
+ ("leisure", "golf_course", 25),
("leisure", "dog_park", 35),
("leisure", "garden", 20),
("leisure", "sports_centre", 5),
("landuse", "military", 30),
# Z-Order 3
("natural", "bare_rock", 25),
- ("natural", "water", 20),
+ ("natural", "water", 4),
("natural", "bay", 30),
("natural", "beach", 30),
- ("water", "lake", 20),
- ("water", "pond", 20),
- ("water", "river", 20),
+ ("water", "lake", 10),
+ ("water", "pond", 10),
+ ("water", "river", 10),
("waterway", "river", 10),
("waterway", "stream", 10),
- ("amenity", "parking", 10),
+ ("amenity", "parking", 3),
("highway", "pedestrian", 12),
# Z-Order 4
("highway", "motorway", 5),
src = -1
if len(brd) == 1: src=1
if len(brd) >= 2:
- src = randrange(1, len(brd))
+ src = randrange(1, len(brd)+1)
ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png")
# Load in the sources to work with
imgid = 0
if len(ptc_src) == 1: imgid = 0
if len(ptc_src) >= 2:
- imgid = randrange(1, len(ptc_src)) - 1
+ imgid = randrange(1, len(ptc_src)+1) - 1
l = 0 - int(ptc_src[imgid].width / 2)
r = layer.width - int(ptc_src[imgid].width / 2)
t = 0 - int(ptc_src[imgid].height / 2)
if src == -1:
if len(brd) == 1: src=1
if len(brd) >= 2:
- src = randrange(1, len(brd))
+ src = randrange(1, len(brd)+1)
ptc = glob.glob(root_folder + "/ptc/b" + str(src) + "_p*.png")
imgid = 0
if len(ptc_src) == 1: imgid = 0
if len(ptc_src) >= 2:
- imgid = randrange(1, len(ptc_src)) - 1
+ imgid = randrange(1, len(ptc_src)+1) - 1
l = 0 - int(ptc_src[imgid].width / 2)
r = layer.width - int(ptc_src[imgid].width / 2)
t = 0 - int(ptc_src[imgid].height / 2)
# Here we need to do some magic to make some features look more natural
if (self._tag == "landuse" and self._value == "meadow") or (self._tag == "natural" and self._value == "grassland") or (self._tag == "natural" and self._value == "heath"):
amt = randrange(1,5)
- for i in range(1, amt):
+ for i in range(1, amt+1):
ptc = randrange(1, 14)
img = Image.open(mstr_datafolder + "Textures/tile/completion/p" + str(ptc)+".png")
lx = randrange( int(layer.width/20), layer.width - (int(layer.width/20)) - img.width )
layer_border = self.genborder(osm_edge, "landuse", "meadow")
layer_comp.alpha_composite(layer_border)
+ # Edges for waters
+ if self._tag == "natural" and self._value == "water":
+ osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES)
+ osm_edge = osm_edge.filter(ImageFilter.MaxFilter)
+ osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=2))
+ layer_comp.alpha_composite(osm_edge)
# Store layer
if self._is_completion == False:
# Find a color range
d = randrange(1,21)
+ # Bring in some variety by making the one or other pixel darker
+ dp = randrange(1, 3)
+ if dp == 2:
+ d = d + 20
# Adjust this pixel
c = (bld_clr[cidx][1]-d, bld_clr[cidx][2]-d, bld_clr[cidx][3]-d, 255)
# Set pixel
shf_x2 = x-randrange(1, 21)
shf_y2 = y-randrange(1, 21)
if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_y <= self._imgsize-1 and shf_y >= 0:
- st = random.uniform(0.85, 1.0)
+ st = random.uniform(0.65, 0.85)
ca = 255 * st
aa = int(ca)
d = randrange(1,26)
layer_comp = details
# New edge
osm_edge = osm_mask.filter(ImageFilter.FIND_EDGES)
- osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=1))
+ osm_edge = osm_edge.filter(ImageFilter.MaxFilter)
+ osm_edge = osm_edge.filter(ImageFilter.GaussianBlur(radius=2))
# Blur the image
layer_comp = layer_comp.filter(ImageFilter.GaussianBlur(radius=1))
osm_edge.alpha_composite(layer_comp)
if shf_x > 0 and shf_x < self._imgsize and shf_y > 0 and shf_y < self._imgsize:
# Pick a number of trees to place
numtrees = randrange(1, 16)
- for i in range(1, numtrees):
+ for i in range(1, numtrees+1):
# Pick some file
pick = str(randrange(1, 11))
tree = Image.open(mstr_datafolder + "Textures/building/area/p" + pick + ".png")
for y in range(self._imgsize-1):
for x in range(self._imgsize-1):
m = mask_pix[x,y]
- shf_x = x + randrange(1, mstr_shadow_shift)
- shf_x2 = x + randrange(1, mstr_shadow_shift)
+ shf_x = x + randrange(1, mstr_shadow_shift + 1)
+ shf_x2 = x + randrange(1, mstr_shadow_shift + 1)
if shf_x <= self._imgsize-1 and shf_x >= 0 and shf_x2 <= self._imgsize-1 and shf_x2 >= 0:
a = mask_pix[x,y][3]
- st = random.uniform(0.45, mstr_shadow_strength)
+ st = random.uniform(0.6, mstr_shadow_strength)
ca = a * st
aa = int(ca)
shadow_pix[shf_x, y] = (0,0,0,aa)
from log import *
from PIL import Image, ImageFilter, ImageDraw, ImagePath
from random import randrange
+from tiledb import *
import random
class mstr_maskgen:
self._vstep = vstep
self._scale = 1 / math.cos(math.radians(self._box[0]))
self._isline = isline
+ self._tiledb = mstr_tiledb(self._box[0], self._box[2])
+ if xml != None:
+ self._xml = xml
#mstr_msg("maskgen", "Intialized mask gen.")
if len(latlng) == 2:
# For some reason, sometimes the array is empty. Make sure we have two data points.
if len(latlng) == 2:
+ # Insert a WED data point should this be a forest:
+ if self._tag == "landuse" and self._value == "forest":
+ self._tiledb.insert_wed_datapoint(1, 1, latlng[0], latlng[1])
+
# Project the pixel, and add to the polygon shape.
p_lat = self.project_pixel(latlng[0], bbox[1])
p_lng = self.project_pixel(latlng[1], bbox[3])
# Inform
mstr_msg("maskgen", "Mask built.")
+ # Close the DB
+ self._tiledb.close_db()
+
os.makedirs(self._output + "/Tiles/"+str(self._lat)+"_"+str(self._long))
mstr_msg("orthographic", "Created Tiles sub folder: " +str(self._lat)+"_"+str(self._long))
- # Note down diameter of entire tile
- #tile_dm = round(self._findWidthOfLongitude(), 3)
- #mstr_msg("orthographic", "Tile diameter: " + str(tile_dm) + "m")
- #dm_of_18 = round(tile_dm * mstr_zl_18, 3)
- #mstr_msg("orthographic", "Diameter of ZL 18 tile: " + str(dm_of_18) + "m")
-
# The tile is constructed of many smaller parts. We walk through the
# smallest possible, from which the bigger ones are later built.
bb_lat = self._lat
mstr_msg("orthographic", "Processing layer " + str(curlyr) + " of " + str(len(layers)))
# Generate the mask
- mg = mstr_maskgen( [self._lat, cur_tile_y, self._long, cur_tile_x], self._vstep, layer[0], layer[1], layer[2] )
+ mg = mstr_maskgen( [self._lat, cur_tile_y, self._long, cur_tile_x], self._vstep, layer[0], layer[1], layer[2])
mg._build_mask()
# Generate the layer
# Acquire XMLs in chunks, then store them
- def acquire_osm(self, v, h):
+ def acquire_osm(self, v, h, asobject=False):
mstr_msg("osmxml", "Acquiring OSM data for " + str(self._lat)+","+str(self._lng)+" - "+str(self._curB_lat)+","+str(self._curB_lng))
# We will use our self-hosted API for this.
}
r = requests.post(mstr_osm_endpoint, json=data)
- xml = mstr_datafolder + "_cache/tile.xml"
- if os.path.isfile(xml):
- os.remove(xml)
- with open(xml, 'wb') as textfile:
+ xmlf = mstr_datafolder + "_cache/tile.xml"
+ if os.path.isfile(xmlf):
+ os.remove(xmlf)
+ with open(xmlf, 'wb') as textfile:
textfile.write(r.content)
+ # Provide the object directly
+ if asobject == True:
+ xml_doc = xml.dom.minidom.parse("_cache/tile.xml")
+ return xml_doc
+
# Get all nodes from the specified OSM file
def acquire_nodes(self, xmlfile):
icao.append(v)
# Return list of found airports
return icao
-
+
# Finds the surface type of a runway in the current data chunk.
# If no surface type is specified, the runway will be rendered similar
# Return the found surface type
return surface
-
+
# It turns out that some features hide themselves in the relations section.
# I figured this out during testing, and almost going insane over the
tgd.append(i.getAttribute("k"))
tgd.append(i.getAttribute("v"))
rls.append((rp.getAttribute("ref"), tgd))
- return rls
\ No newline at end of file
+ return rls
+
\ No newline at end of file
It is, at the end of the day, for a flight simulator. Orthographic can strike the right balance between accuracy of the part of the world you fly in, and looks.
+Prior to publishing, there have been many iterations and hours of testing the generation of buildings. While the current state is far from perfect, it is as best I can make it - and I think we can agree that it looks good enough... for flight simulators. With the right tools in place, buildings will most likely be placed on many of these locations anyways.
+
[section]Where it excels[/section]
- Current Python version
- Pillow for Python
+- SQLite must be available to Python. On Windows it is by default, you may need to install the sqlite3 developer libraries on your distribution, then compile Python to automatically avail of this built-in module.
+
[section]Configuration[/section]
- 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 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
+- You can turn console logging on or off by changing the variable mstr_show_log
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.
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.
+Also in defines.py, you will find the single layers, along with their corresponding mask blurring values. It is my strong recommendation NOT to change these values, as I have taken a lot of time to fine-tune these values.
+
[section]Running![/section]
self._conn.execute("CREATE TABLE IF NOT EXISTS tiledata (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);")
self._conn.execute("CREATE TABLE IF NOT EXISTS airports (icao TEXT, tile_v INTEGER, tile_h INTEGER, latitude REAL, longitude REAL);")
self._conn.execute("CREATE TABLE IF NOT EXISTS completion (tile_v INTEGER, tile_h INTEGER, tag TEXT, value TEXT, source INTEGER, adjacent TEXT);")
+ self._conn.execute("CREATE TABLE IF NOT EXISTS weddata (datatype INTEGER, datagroup INTEGER, latitude REAL, longitude REAL);")
#mstr_msg("tiledb", "Tables created")
def insert_icao(self, icao, tv, th, lat, lng):
self._conn.execute("INSERT INTO airports VALUES ('"+icao+"', "+str(tv)+", "+str(th)+", "+str(lat)+", "+str(lng)+");")
+ # Inserts a data point for WED generation
+ def insert_wed_datapoint(self, dtype, dgroup, lat, lng):
+ self._conn.execute("INSERT INTO weddata VALUES( "+str(dtype)+","+str(dgroup)+","+str(lat)+","+str(lng)+" );")
+ self.commit_query()
+
# Commit a query or a number of queries
def commit_query(self):
--- /dev/null
+
+# -------------------------------------------------------------------
+# ORTHOGRAPHIC
+# Your personal aerial satellite. Always on. At any altitude.*
+# Developed by MarStrMind
+# License: Open Software License 3.0
+# Up to date version always on marstr.online
+# -------------------------------------------------------------------
+# wedgen.py
+# Generates the XML for X-Plane's WED, which you need to use the
+# orthos in the flight simulator.
+# -------------------------------------------------------------------
+
+import xml.dom.minidom
+from defines import *
+from log import *
+from tiledb import *
+
+
+class mstr_wedgen:
+ def __init__(self, lat, lng):
+ self._lat = lat
+ self._lng = lng
+ self._curID = 2
+ self._tiledb = mstr_tiledb(lat, lng)
+
+
+ # Creates the XML file for WED in one go
+ def create_WED_XML(self):
+
+ # Generates a very basic skeleton for the World Editor
+ root = xml.dom.minidom.Document()
+ xmlobj = root.createElement("doc")
+ root.appendChild(xmlobj)
+
+ objs = root.createElement("objects")
+ xmlobj.appendChild(objs)
+
+ rtobj = root.createElement("object")
+ rtobj.setAttribute("class", "WED_Root")
+ rtobj.setAttribute("id", "1")
+ rtobj.setAttribute("parent_id", "0")
+
+ chdobj = root.createElement("children")
+ rtobj.appendChild(chdobj)
+
+ hiera = root.createElement("hierarchy")
+ hiera.setAttribute("name", "root")
+ rtobj.appendChild(hiera)
+
+ # We now open the DB and check for everything that needs to be added,
+ # predominantely forests
+
+
+ # I believe this needs to be in
+ prefs = root.createElement("prefs")
+ xmlobj.appendChild(prefs)
+
+ file_handle = open(mstr_datafolder + "Tiles/" + str(self._lat) + "_" + str(self._lng) + "/earth.wed.xml","w")
+ root.writexml(file_handle)
+ file_handle.close()
\ No newline at end of file