# ------------------------------------------------------------------- # 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 # ------------------------------------------------------------------- # orthographic.py # Main class which handles the generation of the ortho tile. # ------------------------------------------------------------------- import math import os import glob import threading from multiprocessing import Process from defines import * from log import * from osmxml import * from maskgen import * from layergen import * from photogen import * from tileprep import * from xp_scenery import * # The main class which handles the rest class mstr_orthographic: # Constructor of class. Takes longitude and latitude. def __init__(self, lat, lng, outfolder, pwd, prep=False): self._lat = lat self._long = lng self._output = outfolder self._pwd = pwd self._vstep = self._findVerticalStepping() self._latlngfld = self.latlng_folder([lat,lng]) self._prep = prep mstr_msg("orthographic", "Initiated with LAT: " + str(lat) + ", LNG: " + str(lng)) # 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): a = True if os.access(src, os.W_OK) == False: a = False return a # 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 _findVerticalStepping(self): scale = 1 / math.cos(math.radians(self._lat)) maxlat = (1 / scale) * mstr_zl_18 return maxlat # 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) # In this case we only want to acquire PBF for a latitude and longitude. Normally # not needed for standard ortho generation. def _generateData(self): # 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 bb_lng = self._long bb_lat_edge = self._lat+self._vstep bb_lng_edge = self._long+mstr_zl_18 cur_tile_x = 1 cur_tile_y = 1 osmxml = mstr_osmxml(0,0) mstr_msg("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 # We need to know the highest possible latitude and longitude tile numbers, # in case we render at the edge mlat = 1 mlng = 1 while bb_lat < self._lat + 1: bb_lat = bb_lat + self._vstep mlat = mlat+1 while bb_lng < self._long + 1: bb_lng = bb_lng + mstr_zl_18 mlng = mlng+1 mstr_msg("orthographic", "Max lat tile: " + str(mlat) + " - max lng tile: " + str(mlng)) maxlatlng = [ mlat, mlng ] # Reset these two bb_lat = self._lat bb_lng = self._long # 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. for lat_grid in range(1, maxlatlng[0]+1): for lng_grid in range(1, maxlatlng[1]+1): # Adjust bounding box osmxml.adjust_bbox(bb_lat, bb_lng, bb_lat_edge, bb_lng_edge) osmxml.generate_osm(cur_tile_y, cur_tile_x) # <- This acquires current OSM info # 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("orthographic", "Adjustment of longitude performed") # Adjust peak longitude tile number if cur_tile_x > top_lng: top_lng = cur_tile_x # Adjust latitude and all other values when we get here cur_tile_y = cur_tile_y+1 cur_tile_x = 1 bb_lng = self._long bb_lng_edge = self._long + mstr_zl_18 bb_lat = bb_lat + self._vstep bb_lat_edge = bb_lat_edge + self._vstep mstr_msg("orthographic", "Adjustment of latitude performed") # Adjust peak latitude number if cur_tile_y > top_lat: top_lat = cur_tile_y # Start the multi-threaded build of all orthos # amtsmt = AmountSimultaneous - so how many orthos you want to # generate at the same time. You may need to fine tune this value # so that you don't overload your machine. def _generateOrthos_mt(self, amtsmt): # Need to know maximum values first bb_lat = self._lat bb_lng = self._long bb_lat_edge = self._lat+self._vstep bb_lng_edge = self._long+mstr_zl_18 mlat = 1 mlng = 1 while bb_lat < self._lat + 1: bb_lat = bb_lat + self._vstep mlat = mlat+1 while bb_lng < self._long + 1: bb_lng = bb_lng + mstr_zl_18 mlng = mlng+1 mstr_msg("orthographic", "Max lat tile: " + str(mlat) + " - max lng tile: " + str(mlng)) maxlatlng = [ mlat, mlng ] procs = [] for p in range(1, amtsmt+1): proc = Process(target=self._buildOrtho, args=[1, p, amtsmt]) procs.append(proc) proc.start() mstr_msg("orthographic", "Ortho threads started") # Starts a threading loop to build orthos, with the defined starting point in # the lat-lng grid. You will also need to provide the horizontal stepping so # that the thread keeps running. def _buildOrtho(self, v, h, step): # Starting point grid_lat = v grid_lng = h # 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 bb_lng = self._long bb_lat_edge = self._lat+self._vstep bb_lng_edge = self._long+mstr_zl_18 # We need to know the highest possible latitude and longitude tile numbers, # in case we render at the edge mlat = 1 mlng = 1 while bb_lat < self._lat + 1: bb_lat = bb_lat + self._vstep mlat = mlat+1 while bb_lng < self._long + 1: bb_lng = bb_lng + mstr_zl_18 mlng = mlng+1 mstr_msg("orthographic", "Max lat tile: " + str(mlat) + " - max lng tile: " + str(mlng)) maxlatlng = [ mlat, mlng ] while grid_lat <= maxlatlng[0]: # Reset these two bb_lat = self._lat + ((grid_lat-1)*self._vstep) bb_lng = self._long + ((grid_lng-1)*mstr_zl_18) bb_lat_edge = self._lat + ((grid_lat-1)*self._vstep) + self._vstep bb_lng_edge = self._long + ((grid_lng-1)*mstr_zl_18) + mstr_zl_18 osmxml = mstr_osmxml() osmxml.adjust_bbox(bb_lat, bb_lng, bb_lat_edge, bb_lng_edge) osmxml.acquire_osm(grid_lat, grid_lng) # Let the user know mstr_msg("orthographic", "Generating orthophoto " + str(grid_lat) + "-" + str(grid_lng)) # Check for work to be done layers = self.determineLayerWork(osmxml) # We need to walk through the array of layers, # in their z-order. # For each layer, we will generate the mask, the layer image # itself, and finally, compose the ortho photo. mstr_msg("orthographic", "Beginning generation of layers") # In here we store the layers photolayers = [] # The masks are handed to layergen in sequence. The layers are then # in turn handed to photogen. curlyr = 1 for layer in layers: # Let the user know mstr_msg("orthographic", "Processing layer " + str(curlyr) + " of " + str(len(layers))) # Generate the mask mg = mstr_maskgen( [self._lat, grid_lat, self._long, grid_lng], self._vstep, layer[0], layer[1], layer[2]) if layer[0] == "building": mg.set_tile_width(self._findWidthOfLongitude(bb_lat)) mg.set_latlng_numbers(self._lat, grid_lat, self._long, grid_lng) mask = mg._build_mask(osmxml) # Generate the layer lg = mstr_layergen(layer[0], layer[1], self._lat, grid_lat, self._long, grid_lng, layer[2]) lg.set_max_latlng_tile(maxlatlng) lg.set_latlng_folder(self._latlngfld) #lg.open_db() lg.open_tile_info() photolayers.append(lg.genlayer(mask, osmxml)) curlyr = curlyr+1 mstr_msg("orthographic", "All layers created") # We should have all layers now. # Snap a photo with our satellite :) mstr_msg("orthographic", "Generating ortho photo") pg = mstr_photogen(self._lat, self._long, grid_lat, grid_lng, maxlatlng[0], maxlatlng[1]) pg.genphoto(photolayers) mstr_msg("orthographic", " -- Ortho photo generated -- ") print("") print("") # Perform adjustment of grid position n_lng = grid_lng + step if n_lng > maxlatlng[1]: np = n_lng - maxlatlng[1] grid_lng = np grid_lat = grid_lat+1 else: grid_lng = n_lng # Prepares the entire tile def _prepareTile(self): mstr_msg("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"): os.makedirs(self._output + "/_cache") mstr_msg("orthographic", "Created _cache folder.") # Generate the Tiles/lat-lng folder for the finished tile if not os.path.exists(self._output + "/z_orthographic"): os.makedirs(self._output + "/z_orthographic") mstr_msg("orthographic", "Created z_orthographic folder") # Generate the orthos folder if not os.path.exists(self._output + "/z_orthographic/orthos"): os.makedirs(self._output + "/z_orthographic/orthos") mstr_msg("orthographic", "Created tile orthos folder") if not os.path.exists(self._output + "/z_orthographic/orthos" + self._latlngfld): os.makedirs(self._output + "/z_orthographic/orthos/" + self._latlngfld, exist_ok=True) # Generate the database folder if not os.path.exists(self._output + "/z_orthographic/data"): os.makedirs(self._output + "/z_orthographic/data") mstr_msg("orthographic", "Created tile database folder") if not os.path.exists(self._output + "/z_orthographic/data/" + self._latlngfld): os.makedirs(self._output + "/z_orthographic/data/" + self._latlngfld) # X-Plane specific if mstr_xp_genscenery == True: btnum = self.find_earthnavdata_number() btstr = self.latlng_folder(btnum) if not os.path.exists(self._output + "/z_orthographic/terrain"): os.makedirs(self._output + "/z_orthographic/terrain") mstr_msg("orthographic", "[X-Plane] Created terrain files folder") if not os.path.exists(self._output + "/z_orthographic/terrain/" + self._latlngfld): os.makedirs(self._output + "/z_orthographic/terrain/" + self._latlngfld) if not os.path.exists(self._output + "/z_orthographic/Earth nav data"): os.makedirs(self._output + "/z_orthographic/Earth nav data") mstr_msg("orthographic", "[X-Plane] Created Earth nav folder") if not os.path.exists(self._output + "/z_orthographic/Earth nav data/" + btstr): os.makedirs(self._output + "/z_orthographic/Earth nav data/" + btstr) if mstr_xp_scn_normalmaps == True: if not os.path.exists(self._output + "/z_orthographic/normals"): os.makedirs(self._output + "/z_orthographic/normals") mstr_msg("orthographic", "[X-Plane] created tile normal maps folder") if not os.path.exists(self._output + "/z_orthographic/normals/" + self._latlngfld): os.makedirs(self._output + "/z_orthographic/normals/" + self._latlngfld) # 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 bb_lng = self._long bb_lat_edge = self._lat+self._vstep bb_lng_edge = self._long+mstr_zl_18 cur_tile_x = 1 cur_tile_y = 1 #osmxml = mstr_osmxml(0,0) mstr_msg("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 # We need to know the highest possible latitude and longitude tile numbers, # in case we render at the edge mlat = 1 mlng = 1 while bb_lat < self._lat + 1: bb_lat = bb_lat + self._vstep mlat = mlat+1 while bb_lng < self._long + 1: bb_lng = bb_lng + mstr_zl_18 mlng = mlng+1 mstr_msg("orthographic", "Max lat tile: " + str(mlat) + " - max lng tile: " + str(mlng)) maxlatlng = [ mlat, mlng ] # Reset these two bb_lat = self._lat bb_lng = self._long # We will now prepare the graphic tile generation. We do this by only generating # the masks and determine which sources to use in the actual images. # 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. for lat_grid in range(1, maxlatlng[0]+1): for lng_grid in range(1, maxlatlng[1]+1): # Adjust bounding box osmxml = mstr_osmxml() osmxml.adjust_bbox(bb_lat, bb_lng, bb_lat_edge, bb_lng_edge) osmxml.acquire_osm(lat_grid, lng_grid) mstr_msg("orthographic", "Adjusted bounding box for XML object") # Check for work to be done layers = self.determineLayerWork(osmxml) curlyr = 1 for layer in layers: if layer[2] == False and layer[0] != "building": # Let the user know 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]) mask = mg._build_mask(osmxml, is_prep=True) # We need an object here tp = mstr_tileprep(self._lat, self._long, lat_grid, lng_grid, layer[0], layer[1], mask, False) tp._prepareTile() curlyr = curlyr+1 # 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("orthographic", "Adjustment of longitude performed") # Adjust peak longitude tile number if cur_tile_x > top_lng: top_lng = cur_tile_x # Adjust latitude and all other values when we get here cur_tile_y = cur_tile_y+1 cur_tile_x = 1 bb_lng = self._long bb_lng_edge = self._long + mstr_zl_18 bb_lat = bb_lat + self._vstep bb_lat_edge = bb_lat_edge + self._vstep mstr_msg("orthographic", "Adjustment of latitude performed") # Adjust peak latitude number if cur_tile_y > top_lat: top_lat = cur_tile_y # Checks which layers need to be generated, and what kind of layer it is def determineLayerWork(self, xmlobj): mstr_msg("orthographic", "Checking for work to be performed") layers = [] #tilexml = mstr_datafolder + "_cache/tile.xml" #xml = mstr_osmxml(0,0) way = xmlobj.acquire_waypoint_data() rls = xmlobj.acquire_relations() for l in mstr_ortho_layers: # Check if there is anything to render has_way = False has_rls = False for w in way: if w[2] == l[0] and w[3] == l[1]: has_way = True break for r in rls: if l[0] in r[1] and l[1] in r[1]: has_rls = True break if has_way == True or has_rls == True: mstr_msg("orthographic", "Adding: " + l[0]+":"+l[1]) is_line = False for s in mstr_ortho_layers: if s[0] == l[0] and s[1] == l[1]: if isinstance(s[2], int) == False: is_line = False break if isinstance(s[2], int) == True: is_line = True break ly = (l[0], l[1], is_line) layers.append(ly) mstr_msg("orthographic", "A total of " + str(len(layers)) + " layers were found") return layers # Construct a folder name for latitude and longitude def latlng_folder(self, numbers): fstr = "" if numbers[0] >= 0: fstr = "+" if numbers[0] < 0: fstr = "-" if abs(numbers[0]) < 10: fstr = fstr + "0" + str(numbers[0]) if abs(numbers[0]) >= 10 and numbers[0] <= 90: fstr = fstr + str(numbers[0]) if numbers[1] >= 0: fstr = fstr + "+" if numbers[1] < 0: fstr = fstr + "-" if abs(numbers[1]) < 10: fstr = fstr + "00" + str(numbers[1]) if abs(numbers[1]) >= 10 and numbers[0] <= 99: fstr = fstr + "0" + str(numbers[1]) if abs(numbers[1]) >= 100 : fstr = fstr + str(numbers[1]) return fstr # Find the next "by-ten" numbers for the current latitude and longitude def find_earthnavdata_number(self): earthnavdata = [] lat = abs(int(self._lat / 10) * 10) lng = abs(int(self._long / 10) * 10) earthnavdata.append(lat) earthnavdata.append(lng) return earthnavdata