def calculate_hydrology_map(self, map, edaphic_map, soil_ids_map, image_insolation_map, biom): """ Calculates the available water at every point on the terrain. It uses the average rainfall and groundwater from the biom. The water absorption and depth of the soil is also considered and the evaporation by the sun is calculated. :param map: Object of the map class. :param edaphic_map: Object of the Edaphology class. Used to get the soil depth. :param soil_ids_map: Map of the soil ids. Used to get the water absorption of the soil. :param image_insolation_map: Result of the insolation calculation. Used for the calculation of the evaporation. :param biom: Object of the biom class. Used to get the groundwater and rainfall values. :return: hydrology_map: Result of water calculations. """ hydrology_map = Image(size=edaphic_map.size, dtype=np.float) for y in range(edaphic_map.size): print("Calculating hydrology: Row: " + str(y)) for x in range(edaphic_map.size): depth = edaphic_map.image[y][x] soil_id = soil_ids_map.image[y][x] soil = self.controller.search_soil(soil_id) if depth >= 100: depth_coefficient = 1.0 else: depth_coefficient = depth / 100 water_supply = (biom.groundwater + biom.avg_rainfall_per_day ) * depth_coefficient * soil.water_absorption evaporated_water = ( image_insolation_map.image[y][x] * K_CALORIES_NEEDED_TO_EVAPORATE_1_G_WATER) / 1000 if evaporated_water > water_supply: evaporated_water = water_supply water_supply -= evaporated_water hydrology_map.image[y][ x] = water_supply # * (map.pixel_size ** 2) return hydrology_map
def draw_height_and_soil_map(self, maps): self.controller.load_height_and_soil_map(maps) self.subplot_height_map.imshow(self.controller.image_height_map.image, cmap='gray') if self.height_map_colorbar: self.height_map_colorbar.remove() plot = self.subplot_height_map.pcolor( self.controller.image_height_map.image, cmap='gray') self.height_map_colorbar = self.figure_height_map.colorbar(plot) self.subplot_texture_map.imshow(self.controller.soil_ids_map.image) self.canvas_height_map.draw() self.canvas_texture_map.draw() map_name = self.maps.get() map = self.controller.maps[map_name] insolation_map_file = Path("resources/results/" + map_name + "/" + map_name + "_" + self.daylight_hours.get("1.0", 'end-1c') + "daylight_hours_insolation_image.png") self.controller.image_insolation_map = Image(size=3, fill_color=0xFF) if insolation_map_file.is_file(): self.controller.image_insolation_map.load_image( insolation_map_file) self.draw_insolation_image(self.controller.image_insolation_map) orographic_file = Path("resources/results/" + map_name + "/" + map_name + "_orographic_normals") self.controller.image_orographic_map = [[[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]], [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]] if orographic_file.is_file(): self.controller.image_orographic_map = self.controller.load_3d_list( orographic_file) self.draw_orographic_image(self.controller.image_orographic_map) edaphic_map_file = Path("resources/results/" + map_name + "/" + map_name + "_edaphic_image.png") self.controller.image_edaphic_map = Image(size=3, fill_color=0xFF) if edaphic_map_file.is_file(): self.controller.image_edaphic_map.load_image(edaphic_map_file) self.draw_edaphic_image(self.controller.image_edaphic_map) water_map_file = Path("resources/results/" + map_name + "/" + map_name + "_water_image.png") self.controller.image_water_map = Image(dtype=np.float, size=3, fill_color=0xFF) if water_map_file.is_file(): self.controller.image_water_map.load_image(water_map_file) self.draw_hydrology_image(self.controller.image_water_map) self.controller.image_probabilities = Image(dtype=np.float, size=3, fill_color=0xFF) self.draw_probability_image(self.controller.image_probabilities)
def load_texture_map(self): rep = askopenfilenames(parent=self, initialdir='/', initialfile='tmp', filetypes=[("Pictures", "*.png")]) if len(rep) > 0: self.texture_map_path = rep[0] soil_ids_map = Image() soil_ids_map.load_image(self.texture_map_path) self.subplot_texture_map.imshow(soil_ids_map.image) self.canvas_texture_map.draw()
def load_height_map(self): rep = askopenfilenames(parent=self, initialdir='/', initialfile='tmp', filetypes=[("Pictures", "*.png")]) if len(rep) > 0: self.height_map_path = rep[0] image_height_map = Image() image_height_map.load_image(self.height_map_path) self.subplot_height_map.imshow(image_height_map.image, cmap='gray') self.canvas_height_map.draw()
def load_height_and_soil_map(self, map_name): """ It creates two images and loads the height- and soil- map into it. :param map_name: String of the map name. It is used to find the images on disk. """ map = self.maps[map_name] self.image_height_map = Image() self.image_height_map.load_image(map.height_map_path) self.soil_ids_map = Image() self.soil_ids_map.load_image(map.texture_map_path)
def test_CalculateInsolationDarkImage(self): # given image_size = 4 input_image = Image(size=image_size, fill_color=100) insolation = Insolation(input_image) expected_image = Image(size=image_size, fill_color=0) # completely dark sun_position = [2, 2, 10] # when insolation.calculate_insolation_cpu(sun_position) # then self.assertEqual(expected_image, insolation.insolation_image)
def test_CalculateInsolationWhiteImage(self): # given image_size = 4 input_image = Image(size=image_size, fill_color=100) insolation = Insolation(input_image) expected_image = Image( size=image_size, fill_color=CAL_PER_HOUR_PER_PIXEL) # every pixel receives calories sun_position = [2, 2, 200] # when insolation.calculate_insolation_cpu(sun_position) # then self.assertEqual(expected_image, insolation.insolation_image)
def test_CalculateInsolationForDaylightHours(self): # given image_size = 10 input_image = Image(size=image_size, fill_color=0) insolation = Insolation(input_image) daylight_hours = 7 # every pixel receives calories per hour expected_image = Image(size=image_size, fill_color=CAL_PER_HOUR_PER_PIXEL * daylight_hours) # when insolation.calculate_insolation_for_daylight_hours(daylight_hours) # then self.assertEqual(expected_image, insolation.insolation_image)
def calculate_soil_depth(map, size, angles): """ It calculates the soil depth at every point of the terrain. It uses the previously calculated angles (steepness). :param map: Object of Map class. Used to get the max soil depth. :param size: Integer. Size of the terrain. Used to initialize the image. :param angles: 3D-List of all normal vectors. :return: angles: List all angles on the terrain. """ soil_depths = Image(size=size) x = 0 y = 0 for rows in angles: print("Calculating edaphology: Row: " + str(y)) for angle in rows: depth = (1 - angle / 90) * map.max_soil_depth soil_depths.image[y][x] = depth x += 1 y += 1 x = 0 return soil_depths
def test_ConstructorShouldInitializeCorrectly(self): # given fill_color = 10 # when image = Image(size=2, fill_color=fill_color) # then self.assertEqual(fill_color, image.image[0][0]) self.assertEqual(fill_color, image.image[0][1]) self.assertEqual(fill_color, image.image[1][0]) self.assertEqual(fill_color, image.image[1][1])
def prepare_insolation_calculation(self, map_name, daylight_hours, sun_start_elevation, sun_start_azimuth, sun_max_elevation, reflection_coefficient): """ It finds the correct map obect and creates an image and an object of insolation class. Then the heightmap gets loaded and the calculation of the insolation gets started. The results will be shown in the UI and saved. :param map_name: String of the current map name :param daylight_hours: Integer of the number of daylight hours :param sun_start_elevation: Float of the start elevation of the sun :param sun_start_azimuth: Float of the start azimuth of the sun :param sun_max_elevation: Float of the maximal sun elevation (noon) :param reflection_coefficient: Float of the reflection coeficient. It states how much light of the neighbour pixel will be reflected. """ map = self.maps[map_name] self.image_insolation_map = Image(size=self.image_height_map.size) insolation = Insolation(self) self.image_height_map.load_image(map.height_map_path) self.image_insolation_map = insolation.calculate_actual_insolation(map, daylight_hours, sun_start_elevation, sun_start_azimuth, sun_max_elevation, reflection_coefficient) self.main_window.frames['ProbabilityCloudWindow'].draw_insolation_image(self.image_insolation_map) save_path = "resources/results/" + map_name + "/" + map_name + "_" + str(daylight_hours) + "daylight_hours_insolation_image.png" self.image_insolation_map.save_image(save_path)
def calculate_probabilities(self): """ Calculates the final probability by selecting the lowest probability at every pixel. :return: final_probabilities: List of final probabilities at each pixel. """ all_probabilities = [ self.calculate_insolation_probabilities(), self.calculate_soil_demand_probabilities(), self.calculate_soil_depth_probabilities(), self.calculate_water_demand_probabilities() ] final_probabilities = Image(size=self.controller.image_height_map.size, dtype=np.float) reasons_for_not_growing = [0, 0, 0, 0] for y in range(self.controller.image_height_map.size): for x in range(self.controller.image_height_map.size): probability = 1.0 for i in range(len(all_probabilities)): if all_probabilities[i][y][x] < probability: probability = all_probabilities[i][y][x] if probability == 0.0: reasons_for_not_growing[i] += 1 final_probabilities.image[y][x] = probability location_factor_with_max_reasons_for_not_growing = 0 for j in range(len(reasons_for_not_growing)): if j >= 2: # soil demand should be skipped because it is a obvious reason if reasons_for_not_growing[j] > reasons_for_not_growing[ location_factor_with_max_reasons_for_not_growing]: location_factor_with_max_reasons_for_not_growing = j location_factors = [ "insolation", "soil demand", "soil depth", "water demand" ] print( "Main reason for not growing (except soil demand): " + location_factors[location_factor_with_max_reasons_for_not_growing]) return final_probabilities
class Controller: """ The Controller class controls the flow of the application. It starts the UI, loads all data and controlls the calculation of the maps (insolation, orography, edaphology and hydrology) by starting the other logic classes. It also starts the calculation of the probability map. All results will be saved and can be loaded later on. """ def __init__(self): self.image_height_map = None self.soil_ids_map = None self.image_insolation_map = None self.image_orographic_map = None self.image_edaphic_map = None self.image_water_map = None self.image_probabilities = None self.bioms = {} self.load_bioms() self.soils = {} self.load_soils() self.vegetations = {} self.load_vegetations() self.maps = {} self.load_maps() self.main_window = MainWindow(self) def load_height_and_soil_map(self, map_name): """ It creates two images and loads the height- and soil- map into it. :param map_name: String of the map name. It is used to find the images on disk. """ map = self.maps[map_name] self.image_height_map = Image() self.image_height_map.load_image(map.height_map_path) self.soil_ids_map = Image() self.soil_ids_map.load_image(map.texture_map_path) # self.transform_and_save_soil_id_map(map.texture_map_path) # self.save_image_as_csv(self.image_height_map.image) def transform_and_save_soil_id_map(self, path): """ This function is used to transform all occuring IDs in a soil map to wanted IDs (usually the IDs of the created soils). To transform the IDs you have to change the transformation list. This function is used as a workaround if you can't find a valid soil-map. :param path: String of the path to the map that shall be transformed. """ self.soil_ids_map.filter_unique_numbers_from_2d_array() transformation_list = {0: 40, 9362: 80, 18724: 120, 28086: 160, 37449: 200, 46811: 240} self.soil_ids_map.transform_image_to_valid_soils(transformation_list) self.soil_ids_map.filter_unique_numbers_from_2d_array() # check if the transformation was successfull self.soil_ids_map.save_image(path) def prepare_insolation_calculation(self, map_name, daylight_hours, sun_start_elevation, sun_start_azimuth, sun_max_elevation, reflection_coefficient): """ It finds the correct map obect and creates an image and an object of insolation class. Then the heightmap gets loaded and the calculation of the insolation gets started. The results will be shown in the UI and saved. :param map_name: String of the current map name :param daylight_hours: Integer of the number of daylight hours :param sun_start_elevation: Float of the start elevation of the sun :param sun_start_azimuth: Float of the start azimuth of the sun :param sun_max_elevation: Float of the maximal sun elevation (noon) :param reflection_coefficient: Float of the reflection coeficient. It states how much light of the neighbour pixel will be reflected. """ map = self.maps[map_name] self.image_insolation_map = Image(size=self.image_height_map.size) insolation = Insolation(self) self.image_height_map.load_image(map.height_map_path) self.image_insolation_map = insolation.calculate_actual_insolation(map, daylight_hours, sun_start_elevation, sun_start_azimuth, sun_max_elevation, reflection_coefficient) self.main_window.frames['ProbabilityCloudWindow'].draw_insolation_image(self.image_insolation_map) save_path = "resources/results/" + map_name + "/" + map_name + "_" + str(daylight_hours) + "daylight_hours_insolation_image.png" self.image_insolation_map.save_image(save_path) def prepare_orographic_calculation(self, map_name): """ It loads the correct map object, starts the orograhic calculation, displays the result in the UI and saves it as an image. :param map_name: name of the current map """ map = self.maps[map_name] self.image_orographic_map = Orography.calculate_normal_map(map, self.image_height_map) self.main_window.frames['ProbabilityCloudWindow'].draw_orographic_image(self.image_orographic_map) self.save_3d_list(self.image_orographic_map, "resources/results/" + map_name + "/" + map_name + "_orographic_normals") def prepare_edaphic_calculation(self, map_name): """ It loads the correct map object, calculates all angles on the map (between the normal vector and the z-vector), starts the edaphic calculation, displays the result in the UI and saves it as an image. :param map_name: name of the current map """ map = self.maps[map_name] angles = Edaphology.calculate_angles(self.image_orographic_map) self.image_edaphic_map = Edaphology.calculate_soil_depth(map, self.image_height_map.size, angles) self.main_window.frames['ProbabilityCloudWindow'].draw_edaphic_image(self.image_edaphic_map) self.image_edaphic_map.save_image("resources/results/" + map_name + "/" + map_name + "_edaphic_image.png") def prepare_water_calculation(self, map_name): """ It loads the correct map and biom object, starts the hydrologic calculation, displays the result in the UI and saves it as an image. :param map_name: name of the current map """ map = self.maps[map_name] biom = map.biom hydrology = Hydrology(self) self.image_water_map = hydrology.calculate_hydrology_map(map, self.image_edaphic_map, self.soil_ids_map, self.image_insolation_map, biom) self.main_window.frames['ProbabilityCloudWindow'].draw_hydrology_image(self.image_water_map) self.image_water_map.save_image("resources/results/" + map_name + "/" + map_name + "_water_image.png") def calculate_all(self, map_name, daylight_hours, sun_start_elevation, sun_start_azimuth, sun_max_elevation, reflection_coefficient): """ Starts all calculation. This function is used for very large maps. So the user does not have to check the state of the application all the time. You can start all calculations and come back a few hours later. :param map_name: String of the current map name. :param daylight_hours: Integer of the number of daylight hours. :param sun_start_elevation: Float of the start elevation of the sun. :param sun_start_azimuth: Float of the start azimuth of the sun. :param sun_max_elevation: Float of the maximal sun elevation (noon). :param reflection_coefficient: Float of the reflection coeficient. It states how much light of the neighbour pixel will be reflected. """ self.prepare_insolation_calculation(map_name, daylight_hours, sun_start_elevation, sun_start_azimuth, sun_max_elevation, reflection_coefficient) self.prepare_orographic_calculation(map_name) self.prepare_edaphic_calculation(map_name) self.prepare_water_calculation(map_name) def prepare_probabilites_calculation(self, vegetation_name, map_name): """ It creates a probability object, starts the calculation of the probabilites and displays the result in the UI. :param vegetation_name: String. Name of the vegetation for which the probabilites will be calculated. :param map_name: String. Name of the current map. """ probability_calculator = Probabilities(self, vegetation_name, map_name) self.image_probabilities = probability_calculator.calculate_probabilities() self.main_window.frames['ProbabilityCloudWindow'].draw_probability_image(self.image_probabilities) def search_soil(self, soil_id): """ Searches for the soil object in the soil list with the help of the soil ID. :param soil_id: ID of the soil for which the object from the list shall be found. :return: soil_value: Soil object from the soil list. """ for soil_name, soil_value in self.soils.items(): if soil_value.id == soil_id: return soil_value print('Soil id (' + str(soil_id) + ') could not be found!') return self.soils['NotFound'] # raise Exception('Soil id could not be found!') def load_bioms(self): """ Loads all bioms from the bioms.yml into a list. """ bioms = {} bioms_file = Path("resources/data/bioms.yml") if bioms_file.is_file(): with open(bioms_file, 'r') as stream: try: bioms_dict = yaml.safe_load(stream) if bioms_dict is not None: for biom_name, biom_values in bioms_dict.items(): bioms[biom_name] = Biom(biom_name, float(biom_values['atmospheric_diffusion']), float(biom_values['atmospheric_absorption']), float(biom_values['cloud_reflection']), float(biom_values['avg_rainfall_per_day']), float(biom_values['groundwater'])) except yaml.YAMLError as exc: print(exc) self.bioms = bioms def load_soils(self): """ Loads all soils from the soils.yml into a list. """ soils = {} soils_file = Path("resources/data/soil_types.yml") if soils_file.is_file(): with open(soils_file, 'r') as stream: try: soils_dict = yaml.safe_load(stream) if soils_dict is not None: for soil_name, soil_values in soils_dict.items(): soils[soil_name] = Soil(int(soil_values['id']), soil_name, float(soil_values['albedo']), float(soil_values['water_absorption'])) except yaml.YAMLError as exc: print(exc) self.soils = soils def load_vegetations(self): """ Loads all vegetations from the vegetation_types.yml into a list. """ vegetations = {} vegetations_file = Path("resources/data/vegetation_types.yml") if vegetations_file.is_file(): with open(vegetations_file, 'r') as stream: try: vegetations_dict = yaml.safe_load(stream) if vegetations_dict is not None: for vegetation_name, vegetation_values in vegetations_dict.items(): vegetations[vegetation_name] = Vegetation(vegetation_name, float(vegetation_values['energy_demand']), float(vegetation_values['water_demand']), self.soils[vegetation_values['soil_demand']], float(vegetation_values['soil_depth_demand'])) except yaml.YAMLError as exc: print(exc) self.vegetations = vegetations def load_maps(self): """ Loads all maps from the maps.yml into a list. """ maps = {} maps_file = Path("resources/data/maps.yml") if maps_file.is_file(): with open(maps_file, 'r') as stream: try: maps_dict = yaml.safe_load(stream) if maps_dict is not None: for map_name, map_values in maps_dict.items(): maps[map_name] = Map(map_name, self.bioms[map_values['biom']], map_values['height_map_path'], map_values['texture_map_path'], map_values['height_conversion'], map_values['max_soil_depth'], map_values['pixel_size']) except yaml.YAMLError as exc: print(exc) self.maps = maps @staticmethod def save_3d_list(list, path): """ It saves the result of the orographic class (normal map) as a file. :param list: List of the normal vectors of the current map. :param path: String of the path where the file should be saved. """ if not os.path.exists(os.path.dirname(path)): os.makedirs(os.path.dirname(path)) output = open(path, 'wb') pickle.dump(list, output) output.close() @staticmethod def load_3d_list(path): """ Loads the normal vectors from a file. :param path: String of the path of the file that will be loaded. :return: list: List of the loaded normal vectors (normal map). """ pkl_file = open(path, 'rb') list = pickle.load(pkl_file) pkl_file.close() return list @staticmethod def save_image_as_csv(image): np.savetxt("resources/height_map.csv", image, delimiter=',', fmt='%s')
def __init__(self, parent, main_window, controller): tk.Frame.__init__(self, parent) self.controller = controller row = 0 column = 0 self.canvas = tk.Canvas(self, width=main_window.window_width-20, height=main_window.window_height) self.canvas.grid(row=0, column=0, sticky="nsew") scrollbar = tk.Scrollbar(self, command=self.canvas.yview) scrollbar.grid(row=0, column=1, sticky='ns') self.canvas.configure(yscrollcommand=scrollbar.set) # update scrollregion after starting 'mainloop' # when all widgets are in canvas self.canvas.bind('<Configure>', self.on_configure) self.frame = tk.Frame(self.canvas) self.canvas.create_window((0, 0), window=self.frame, anchor='nw') tk.Button(self.frame, text="< Back to menu", command=lambda: main_window.show_frame("MenuWindow")).grid(row=row, column=column, pady=10) row += 1 tk.Label(self.frame, text="Maps:", font='Helvetica 18 bold').grid(row=row, column=column, pady=10) row += 1 tk.Button(self.frame, text="Create new map >", command=lambda: main_window.show_frame("NewMapWindow")).grid(row=row, column=column, pady=10, padx=10) column += 1 for map in controller.maps.values(): row += 1 tk.Label(self.frame, text=map.name, font='Helvetica 16').grid(row=row, column=column, pady=(50, 0), columnspan=4, sticky='S') row += 1 tk.Label(self.frame, text="Biom:").grid(row=row, column=column + 1, pady=5, sticky='E') tk.Label(self.frame, text=map.biom.name).grid(row=row, column=column + 2, pady=5, sticky='W') row += 1 tk.Label(self.frame, text="Height conversion:").grid(row=row, column=column + 1, pady=5, sticky='E') tk.Label(self.frame, text=str(map.height_conversion) + " meter/unit").grid(row=row, column=column + 2, pady=5, sticky='W') row += 1 tk.Label(self.frame, text="Maximum soil depth:").grid(row=row, column=column + 1, pady=5, sticky='E') tk.Label(self.frame, text=str(map.max_soil_depth) + " cm").grid(row=row, column=column + 2, pady=5, sticky='W') row += 1 tk.Label(self.frame, text="Pixel size:").grid(row=row, column=column + 1, pady=5, sticky='E') tk.Label(self.frame, text=str(map.pixel_size) + " m").grid(row=row, column=column + 2, pady=5, sticky='W') row += 1 figure_height_map = Figure(figsize=(3, 3)) subplot_height_map = figure_height_map.add_subplot() subplot_height_map.title.set_text('Height map') canvas_height_map = FigureCanvasTkAgg(figure_height_map, master=self.frame) canvas_height_map.get_tk_widget().grid(row=row, column=column + 1) height_map = Image() height_map.load_image(map.height_map_path) subplot_height_map.imshow(height_map.image, cmap='gray') figure_texture_map = Figure(figsize=(3, 3)) subplot_texture_map = figure_texture_map.add_subplot() subplot_texture_map.title.set_text('Soil-IDs map') canvas_texture_map = FigureCanvasTkAgg(figure_texture_map, master=self.frame) canvas_texture_map.get_tk_widget().grid(row=row, column=column + 2) texture_map = Image() texture_map.load_image(map.texture_map_path) subplot_texture_map.imshow(texture_map.image)