def setUp(self) -> None: super().setUp() self.hour = 17 self.serialise = False self.test_bound_coords = [-1.5, 50.87, -1.3, 51] # self.test_bound_coords = [-1.55, 50.745, -1.3, 51] self.resolution = 30 self.test_bounds = make_bounds_polygon( (self.test_bound_coords[0], self.test_bound_coords[2]), (self.test_bound_coords[1], self.test_bound_coords[3])) self._setup_aircraft() ps = PlotServer() ps.set_time(self.hour) self.raster_shape = ps._get_raster_dimensions(self.test_bounds, self.resolution) ps.data_layers = [TemporalPopulationEstimateLayer('tpe')] [ layer.preload_data() for layer in chain(ps.data_layers, ps.annotation_layers) ] ps.generate_layers(self.test_bounds, self.raster_shape) self.raster_grid = np.flipud( np.sum([ remove_raster_nans(res[1]) for res in ps._generated_data_layers.values() if res[1] is not None ], axis=0)) self.raster_shape = self.raster_grid.shape del ps
def make(min_lat, max_lat, min_lon, max_lon, start_lat, start_lon, end_lat, end_lon, aircraft, failure_prob, output_path, resolution, hour, altitude, airspeed, wind_direction, wind_speed, algo, algo_args): """ Path minimising the fatality risk posed. Generate a path minimising the fatality risk posed by a specified aircraft in the specified bounds. The path is output as a GeoJSON file in the form of a LineString of EPSG:4326 coordinates in (lon, lat) order from start to end. If an aircraft config json file is not specified, a default aircraft with reasonable parameters is used. All coordinates should be in decimal degrees and form a non degenerate polygon. """ bounds = make_bounds_polygon((min_lon, max_lon), (min_lat, max_lat)) pop_grid = _make_pop_grid(bounds, hour, resolution) if not aircraft: aircraft = _setup_default_aircraft() else: aircraft = _import_aircraft(aircraft) strike_grid, v_is = _make_strike_grid(aircraft, airspeed, altitude, failure_prob, pop_grid, resolution, wind_direction, wind_speed) res = _make_fatality_grid(aircraft, strike_grid, v_is) raster_shape = res.shape raster_indices = dict(Longitude=np.linspace(min_lon, max_lon, num=raster_shape[0]), Latitude=np.linspace(min_lat, max_lat, num=raster_shape[1])) start_x, start_y = snap_coords_to_grid(raster_indices, start_lon, start_lat) end_x, end_y = snap_coords_to_grid(raster_indices, end_lon, end_lat) env = GridEnvironment(res, diagonals=False) if algo == 'ra*': algo = RiskGridAStar() elif algo == 'ga': raise NotImplementedError("GA CLI not implemented yet") algo = GeneticAlgorithm([], **algo_args) path = algo.find_path(env, Node((start_x, start_y)), Node((end_x, end_y))) if not path: print('Path not found') return 1 snapped_path = [] for node in path: lat = raster_indices['Latitude'][node.position[1]] lon = raster_indices['Longitude'][node.position[0]] snapped_path.append((lon, lat)) snapped_path = sg.LineString(snapped_path) dataframe = gpd.GeoDataFrame({ 'geometry': [snapped_path] }).set_crs('EPSG:4326') dataframe.to_file(os.path.join(output_path, f'path.geojson'), driver='GeoJSON')
def fatality(min_lat, max_lat, min_lon, max_lon, aircraft, failure_prob, output_path, resolution, hour, altitude, airspeed, wind_direction, wind_speed): """ Fatality Risk map Generate a geotiff raster file of the probability of ground fatality with a specified aircraft in the specified bounds. If an aircraft config json file is not specified, a default aircraft with reasonable parameters is used. All coordinates should be in decimal degrees and form a non degenerate polygon. """ bounds = make_bounds_polygon((min_lon, max_lon), (min_lat, max_lat)) pop_grid = _make_pop_grid(bounds, hour, resolution) if not aircraft: aircraft = _setup_default_aircraft() else: aircraft = _import_aircraft(aircraft) strike_grid, v_is = _make_strike_grid(aircraft, airspeed, altitude, failure_prob, pop_grid, resolution, wind_direction, wind_speed) res = _make_fatality_grid(aircraft, strike_grid, v_is) out_name = f'fatality_{hour}h.tif' _write_geotiff(max_lat, max_lon, min_lat, min_lon, out_name, output_path, res)
def setUp(self) -> None: super().setUp() if not hasattr(self, 'layer'): return self.layer.preload_data() self.test_bounds = make_bounds_polygon((-1.5, -1.3), (50.87, 51)) self.raster_shape = (500, 500) if not hasattr(self, 'hour'): self.hour = 13
def setUp(self) -> None: super().setUp() if not hasattr(self, 'layer'): return import os os.chdir( os.sep.join( (os.path.dirname(os.path.realpath(__file__)), '..', '..'))) self.layer.preload_data() self.test_bounds = make_bounds_polygon((-1.5, -1.3), (50.87, 51)) self.raster_shape = (500, 500) if not hasattr(self, 'hour'): self.hour = 13
def pop_density(min_lat, max_lat, min_lon, max_lon, output_path, resolution, hour): """ Temporal Population Density map Generate a geotiff raster file of the population density in the specified bounds. All coordinates should be in decimal degrees and form a non degenerate polygon. """ bounds = make_bounds_polygon((min_lon, max_lon), (min_lat, max_lat)) raster_grid = _make_pop_grid(bounds, hour, resolution) out_name = f'pop_density_{hour}h.tif' _write_geotiff(max_lat, max_lon, min_lat, min_lon, out_name, output_path, raster_grid)
def make(min_lat, max_lat, min_lon, max_lon, start_lat, start_lon, end_lat, end_lon, aircraft, failure_prob, output_path, resolution, hour, altitude, airspeed, wind_direction, wind_speed, algo, algo_args): """ Path minimising the fatality risk posed. Generate a path minimising the fatality risk posed by a specified aircraft in the specified bounds. The path is output as a GeoJSON file in the form of a LineString of EPSG:4326 coordinates in (lon, lat) order from start to end. If an aircraft config json file is not specified, a default aircraft with reasonable parameters is used. All coordinates should be in decimal degrees and form a non degenerate polygon. """ import geopandas as gpd import os bounds = make_bounds_polygon((min_lon, max_lon), (min_lat, max_lat)) pop_grid = make_pop_grid(bounds, hour, resolution) if not aircraft: aircraft = _setup_default_aircraft() else: aircraft = _import_aircraft(aircraft) strike_grid, v_is = make_strike_grid(aircraft, airspeed, altitude, failure_prob, pop_grid, resolution, wind_direction, wind_speed) res = make_fatality_grid(aircraft, strike_grid, v_is) snapped_path = make_path(res, bounds, (start_lat, start_lon), (end_lat, end_lon), algo=algo) if snapped_path: dataframe = gpd.GeoDataFrame({ 'geometry': [snapped_path] }).set_crs('EPSG:4326') dataframe.to_file(os.path.join(output_path, f'path.geojson'), driver='GeoJSON') else: print('Path could not be found')
def compose_overlay_plot(self, x_range: Optional[Sequence[float]] = (-1.6, -1.2), y_range: Optional[Sequence[float]] = (50.8, 51.05)) \ -> Union[Overlay, Element]: """ Compose all generated HoloViews layers in self.data_layers into a single overlay plot. Overlaid in a first-on-the-bottom manner. If plot bounds has moved outside of data bounds, generate more as required. :param tuple x_range: (min, max) longitude range in EPSG:4326 coordinates :param tuple y_range: (min, max) latitude range in EPSG:4326 coordinates :returns: overlay plot of stored layers """ try: if not self._preload_complete: # If layers aren't preloaded yet just return the map tiles self._progress_callback('Still preloading layer data...') plot = self._base_tiles else: # Construct box around requested bounds bounds_poly = make_bounds_polygon(x_range, y_range) raster_shape = self._get_raster_dimensions( bounds_poly, self.raster_resolution_m) # Ensure bounds are small enough to render without OOM or heat death of universe if (raster_shape[0] * raster_shape[1]) < 7e5: from time import time t0 = time() self._progress_bar_callback(10) self.generate_layers(bounds_poly, raster_shape) self._progress_bar_callback(50) plot = Overlay([ res[0] for res in self._generated_data_layers.values() ]) print("Generated all layers in ", time() - t0) if self.annotation_layers: plot = Overlay([ res[0] for res in self._generated_data_layers.values() ]) raw_datas = [ res[2] for res in self._generated_data_layers.values() ] raster_indices = dict( Longitude=np.linspace(x_range[0], x_range[1], num=raster_shape[0]), Latitude=np.linspace(y_range[0], y_range[1], num=raster_shape[1])) raster_grid = np.sum([ remove_raster_nans(res[1]) for res in self._generated_data_layers.values() if res[1] is not None ], axis=0) raster_grid = np.flipud(raster_grid) raster_indices['Latitude'] = np.flip( raster_indices['Latitude']) self._progress_callback('Annotating Layers...') res = jl.Parallel( n_jobs=1, verbose=1, backend='threading')( jl.delayed(layer.annotate)(raw_datas, ( raster_indices, raster_grid)) for layer in self.annotation_layers) plot = Overlay([ self._base_tiles, plot, *[annot for annot in res if annot is not None] ]).collate() else: plot = Overlay([self._base_tiles, plot]).collate() self._progress_bar_callback(90) else: self._progress_callback('Area too large to render!') if not self._generated_data_layers: plot = self._base_tiles else: plot = Overlay([ self._base_tiles, *list(self._generated_data_layers.values()) ]) self._update_layer_list() self._progress_callback("Rendering new map...") except Exception as e: # Catch-all to prevent plot blanking out and/or crashing app # Just display map tiles in case this was transient import traceback traceback.print_exc() print(e) plot = self._base_tiles return plot.opts(width=self.plot_size[0], height=self.plot_size[1], tools=self.tools, active_tools=self.active_tools)
def test_bounds_polygon_2arg(self): res = make_bounds_polygon((1, 5), (1, 5)) poly = sg.Polygon([(1, 1), (1, 5), (5, 5), (5, 1)]) # __eq__ and .equals are not the same for sg objects! self.assertTrue(res.equals(poly))
def compose_overlay_plot(self, x_range: Optional[Sequence[float]] = (-1.6, -1.2), y_range: Optional[Sequence[float]] = (50.8, 51.05)) \ -> Union[Overlay, Element]: """ Compose all generated HoloViews layers in self.data_layers into a single overlay plot. Overlaid in a first-on-the-bottom manner. If plot bounds has moved outside of data bounds, generate more as required. :param tuple x_range: (min, max) longitude range in EPSG:4326 coordinates :param tuple y_range: (min, max) latitude range in EPSG:4326 coordinates :returns: overlay plot of stored layers """ try: if not self._preload_complete: # If layers aren't preloaded yet just return the map tiles self._progress_callback('Still preloading layer data...') plot = self._base_tiles else: # Construct box around requested bounds bounds_poly = make_bounds_polygon(x_range, y_range) raster_shape = self._get_raster_dimensions( bounds_poly, self.raster_resolution_m) # Ensure bounds are small enough to render without OOM or heat death of universe if (raster_shape[0] * raster_shape[1]) < 7e5: from time import time t0 = time() self._progress_bar_callback(10) # TODO: This will give multiple data layers, these need to be able to fed into their relevent pathfinding layers for annlayer in self.annotation_layers: new_layer = FatalityRiskLayer( 'Fatality Risk', ac=annlayer.aircraft['name']) self.add_layer(new_layer) self.remove_duplicate_layers() self._progress_bar_callback(20) self.generate_layers(bounds_poly, raster_shape) self._progress_bar_callback(50) plt_lyr = list(self._generated_data_layers)[0] plot = Overlay([self._generated_data_layers[plt_lyr][0]]) print("Generated all layers in ", time() - t0) if self.annotation_layers: plot = Overlay( [self._generated_data_layers[plt_lyr][0]]) res = [] prog_bar = 50 for dlayer in self.data_layers: raster_indices = dict( Longitude=np.linspace(x_range[0], x_range[1], num=raster_shape[0]), Latitude=np.linspace(y_range[0], y_range[1], num=raster_shape[1])) raw_data = [ self._generated_data_layers[dlayer.key][2] ] raster_grid = np.sum([ remove_raster_nans( self._generated_data_layers[dlayer.key][1]) ], axis=0) raster_grid = np.flipud(raster_grid) raster_indices['Latitude'] = np.flip( raster_indices['Latitude']) for alayer in self.annotation_layers: if alayer.aircraft == dlayer.ac_dict: self._progress_bar_callback(prog_bar) prog_bar += 40 / len( self.annotation_layers) self._progress_callback( f'Finding a path for {alayer.aircraft["name"]}' ) res.append( alayer.annotate( raw_data, (raster_indices, raster_grid))) self._progress_callback('Plotting paths') self._progress_bar_callback(90) # res = jl.Parallel(n_jobs=1, verbose=1, backend='threading')( # jl.delayed(layer.annotate)(raw_datas, (raster_indices, raster_grid)) for layer in # self.annotation_layers ) plot = Overlay([ self._base_tiles, plot, *[annot for annot in res if annot is not None] ]).collate() else: plot = Overlay([self._base_tiles, plot]).collate() self._progress_bar_callback(90) else: self._progress_callback('Area too large to render!') if not self._generated_data_layers: plot = self._base_tiles else: plot = Overlay([ self._base_tiles, *list(self._generated_data_layers.values()) ]) self._update_layer_list() self._progress_callback("Rendering new map...") except Exception as e: # Catch-all to prevent plot blanking out and/or crashing app # Just display map tiles in case this was transient import traceback traceback.print_exc() self._progress_callback( f'Plotting failed with the following error: {e}. Please attempt to re-generate the plot' ) print(e) plot = self._base_tiles return plot.opts(width=self.plot_size[0], height=self.plot_size[1], tools=self.tools, active_tools=self.active_tools)