diff --git a/flight/odlc_search_grid/main.py b/flight/odlc_search_grid/main.py new file mode 100644 index 0000000..62bd8da --- /dev/null +++ b/flight/odlc_search_grid/main.py @@ -0,0 +1,41 @@ +""" +Sample runner to test the search paths feature +""" + +import plotter +import search_path + + +search_area = [ + { + "altitudeMin": 100.0, + "altitudeMax": 400.0, + "boundaryPoints": [ + {"latitude": 37.95120673695754, "longitude": -91.78693406657685}, + {"latitude": 37.951248325955994, "longitude": -91.78207299965484}, + {"latitude": 37.948226725559984, "longitude": -91.78191292975686}, + {"latitude": 37.94711526778687, "longitude": -91.78116415681103}, + {"latitude": 37.94623824627026, "longitude": -91.78490802154013}, + {"latitude": 37.94797658615526, "longitude": -91.78697801835733}, + {"latitude": 37.95120673695754, "longitude": -91.78693406657685}, + ], + } +][0] + +if __name__ == "__main__": + search_area_points = search_area["boundaryPoints"] + + # Add utm coordinates to all + search_area_points = search_path.all_latlon_to_utm(search_area_points) + + # Generate search path + BUFFER_DISTANCE = -50 # use height/2 of camera image as buffer distance + search_paths = search_path.generate_search_paths(search_area_points, BUFFER_DISTANCE) + + # fly all points in search_paths[0], + # when you reach your starting point, + # fly all points in search_paths[1], + # etc... until all search paths have been flown + + # Plot data + plotter.plot_data(search_paths) diff --git a/flight/odlc_search_grid/plotter.py b/flight/odlc_search_grid/plotter.py new file mode 100644 index 0000000..ba85b9b --- /dev/null +++ b/flight/odlc_search_grid/plotter.py @@ -0,0 +1,26 @@ +""" +Provides plotting functionality for visualizing coordinate data +""" + +from typing import List, Tuple +import matplotlib.pyplot as plt + + +def plot_data(search_paths: List[List[Tuple[float, float]]]) -> None: + """Simple plotter function to plot the search paths + + Parameters + ---------- + search_paths : List[Tuple[float, float]] + A list of search paths to be plotted + """ + + for path in search_paths: + x, y = [], [] + for point in path: + x.append(point[0]) + y.append(point[1]) + plt.plot(x, y, "ro-") + + plt.gca().set_aspect(1) + plt.show() diff --git a/flight/odlc_search_grid/search_path.py b/flight/odlc_search_grid/search_path.py new file mode 100644 index 0000000..b239e91 --- /dev/null +++ b/flight/odlc_search_grid/search_path.py @@ -0,0 +1,86 @@ +""" +Functions for generating search paths to cover an area for finding the standard odlc objects +""" + +from typing import List, Dict, Tuple +from shapely.geometry import Polygon +import utm + + +def latlon_to_utm(coords: Dict[str, float]) -> Dict[str, float]: + """Converts latlon coordinates to utm coordinates and adds the data to the dictionary + + Parameters + ---------- + coords : Dict[str, float] + A dictionary containing lat long coordinates + + Returns + ------- + Dict[str, float] + An updated dictionary with additional keys and values with utm data + """ + + utm_coords = utm.from_latlon(coords["latitude"], coords["longitude"]) + coords["utm_x"] = utm_coords[0] + coords["utm_y"] = utm_coords[1] + coords["utm_zone_number"] = utm_coords[2] + coords["utm_zone_letter"] = utm_coords[3] + return coords + + +def all_latlon_to_utm(list_of_coords: List[Dict[str, float]]) -> List[Dict[str, float]]: + """Converts a list of dictionaries with latlon data to add utm data + + Parameters + ---------- + list_of_coords : List[Dict[str, float]] + A list of dictionaries that contain lat long data + + Returns + ------- + List[Dict[str, float]] + list[dict]: An updated list of dictionaries with added utm data + """ + + for i, _ in enumerate(list_of_coords): + list_of_coords[i] = latlon_to_utm(list_of_coords[i]) + return list_of_coords + + +def generate_search_paths( + search_area_points: List[Dict[str, float]], buffer_distance: int +) -> List[Tuple[float, float]]: + """Generates a list of search paths of increasingly smaller sizes until the whole area + of the original shape has been covered + + Parameters + ---------- + search_area_points : Dict[str, float] + A list of coordinates in dictionary form that contain utm coordinate data + buffer_distance : int + The distance that each search path will be apart from the previous one. + For complete photographic coverage of the area, this should be equal to half the height + of the area the camera covers on the ground given the current altitude. + + Returns + ------- + List[Tuple[float, float]] + A list of concentric search paths that cover the area of the polygon + """ + + # convert to shapely polygon for buffer operations + poly_points = [(point["utm_x"], point["utm_y"]) for point in search_area_points] + boundary_shape = Polygon(poly_points) + + search_paths = [] + + # shrink boundary by a fixed amount until the area it covers is 0 + # add the smaller boundary to our list of search paths on each iteration + while boundary_shape.area > 0: + search_paths.append( + tuple(zip(*boundary_shape.exterior.coords.xy)) # pylint: disable=maybe-no-member + ) + boundary_shape = boundary_shape.buffer(buffer_distance, single_sided=True) + + return search_paths