def explode(features: GeoJson) -> FeatureCollection: """ Takes a feature or set of features and returns all positions as {Point|points}. :param features: any GeoJSON feature or feature collection :return: {FeatureCollection} points representing the exploded input features """ points = [] try: geojson_type = features.get("type") except AttributeError: raise InvalidInput(error_code_messages["InvalidGeometry"](all_geometry_types)) if geojson_type in ["FeatureCollection", "GeometryCollection"]: key = "features" if geojson_type == "FeatureCollection" else "geometries" for feature in features[key]: properties = feature.get("properties", {}) coords = get_coords_from_features(feature) points.extend(reduce_coordinates_to_points(coords, properties)) else: properties = features.get("properties", {}) coords = get_coords_from_geometry(features) points.extend(reduce_coordinates_to_points(coords, properties)) return feature_collection(points)
def line_intersect(input_1: LinePolyFeature, input_2: LinePolyFeature) -> Feature: """ Takes any LineString or Polygon GeoJSON and returns the intersecting point(s). :param line_1: {GeoJSON} line1 any LineString or Polygon :param line_2:{GeoJSON} line2 any LineString or Polygon :returns: {FeatureCollection<Point>} point(s) that intersect both """ line_1_segments = get_line_segments(input_1) line_2_segments = get_line_segments(input_2) possible_intersects = [] intersects = [] possible_intersects.extend( spatial_filtering(line_1_segments, line_2_segments)) for i in possible_intersects: pnt = calculate_intersect(*i) if pnt: intersects.append(pnt) return feature_collection(intersects)
def normalize_to_feature_collection(geojson: GeoJSON) -> FeatureCollection: """ Normalizes any GeoJSON to a FeatureCollection :param geojson: any GeoJSON :return: FeatureCollection """ geojson_type = geojson.get("type") if geojson_type == "FeatureCollection": pass elif geojson_type == "Feature": geojson = feature_collection([geojson]) else: geojson = feature_collection([feature(geojson)]) return geojson
def prepare_response(nearest_point, target_point, fixture_in): nearest_point["properties"].update( {"marker-color": "#F00", "marker-symbol": "star"} ) target_point["properties"].update( {"marker-color": "#00F", "marker-symbol": "circle"} ) result = feature_collection([*fixture_in["features"], target_point, nearest_point]) return result
def test_input_mutation(self): point_1 = point([40, 50], {"featureIndex": "foo"}) point_2 = point([20, -10], {"distanceToPoint": "bar"}) points = feature_collection([point_1, point_2]) result = nearest_point([0, 0], points) # Check if featureIndex properties was added to properties assert result["properties"]["featureIndex"] == 1 # Check if previous input points have been modified assert point_1["properties"] == {"featureIndex": "foo"} assert point_2["properties"] == {"distanceToPoint": "bar"}
def prepare_response(destination_point, fixture_in): coords = get_coords_from_features(fixture_in) coords = [round(coord, 6) for coord in coords] dest_coords = get_coords_from_features(destination_point) dest_coords = [round(coord, 6) for coord in dest_coords] line = line_string([coords, dest_coords], {"stroke": "#F00", "stroke-width": 4}) fixture_in["properties"]["marker-color"] = "#F00" result = feature_collection([line, fixture_in, destination_point]) return result
def polygon_to_line(polygon: PolygonFeature, options: Dict = {}) -> LineFeature: """ Converts a {Polygon} to a {LineString} or a {MultiPolygon} to a {FeatureCollection} or {MultiLineString}. :param polygon: Feature to convert :param options: Optional parameters :return: {Feature Collection|LineString|MultiLineString} of converted (Multi)Polygon to (Multi)LineString """ if not options: properties = polygon.get("properties", {}) else: properties = options.get("properties", {}) geometry_type = get_geometry_type(polygon, ("Polygon", "MultiPolygon")) polygon_coords = get_coords_from_features(polygon, ("Polygon", "MultiPolygon")) if isinstance(geometry_type, str): geometry_type = [geometry_type] polygon_coords = [polygon_coords] for geo_type, poly_coords in zip(geometry_type, polygon_coords): if geo_type == "MultiPolygon": line_coords = [] for poly_coord in poly_coords: line_coords.append(coords_to_line(poly_coord, properties)) line_feature = feature_collection(line_coords) else: line_feature = coords_to_line(poly_coords, properties) return line_feature
class TestLineIntersectss: @pytest.mark.parametrize( "fixture", [ pytest.param(fixture, id=fixture_name) for fixture_name, fixture in fixtures.items() ], ) def test_line_intersect(self, fixture): line_1 = fixture["in"]["features"][0] line_2 = fixture["in"]["features"][1] result = line_intersect(line_1, line_2) assert result.keys() == fixture["out"].keys() assert len(result["features"]) == len(fixture["out"]["features"]) assert (all([ i in result["features"] for i in fixture["out"]["features"] ]) == True) assert (all([ i in fixture["out"]["features"] for i in result["features"] ]) == True) def test_input_mutation_prevention(self): line_1 = line_string([[7, 50], [8, 50], [9, 50]]) line_2 = line_string([[8, 49], [8, 50], [8, 51]]) line_1_cpy = deepcopy(line_1) line_2_cpy = deepcopy(line_2) _ = line_intersect(line_1, line_2) assert line_1 == line_1_cpy assert line_2 == line_2_cpy @pytest.mark.parametrize( "input_value,expected_value", [ pytest.param( ( line_string([[7, 50], [8, 50], [9, 50]])["geometry"], line_string([[8, 49], [8, 50], [8, 51]])["geometry"], ), [8, 50], id="ListHandling", ), pytest.param( ( feature_collection( [line_string([[7, 50], [8, 50], [9, 50], [7, 50]])]), feature_collection( [line_string([[8, 49], [8, 50], [8, 51], [8, 49]])]), ), [8, 50], id="FeatureCollectionHandling", ), pytest.param( ( polygon([[[7, 50], [8, 50], [9, 50], [7, 50]]]), polygon([[[8, 49], [8, 50], [8, 51], [8, 49]]]), ), [8, 50], id="PolygonHandling", ), ], ) def test_geometric_objects(self, input_value, expected_value): result = line_intersect(*input_value) assert result["features"][0]["geometry"][ "coordinates"] == expected_value
def point_grid( bbox: List[float], n_cells: Union[int, float], options: Dict = {}, ) -> FeatureCollection: """ Creates a square of rectangles from a bounding box, Feature or FeatureCollection. :param bbox: Array extent in [minX, minY, maxX, maxY] order :param n_cells: number of each cell, in units :param options: Optional parameters [options["units"]]: units ("degrees", "radians", "miles", "kilometers") of the given cell_width and cell_height [options["mask"]]: if passed a Polygon or MultiPolygon here, the grid Points will be created only inside it [options["properties"]]: passed to each point of the grid :returns: FeatureCollection of a grid of polygons """ if not isinstance(options, dict): options = {} results = [] west = bbox[0] south = bbox[1] east = bbox[2] north = bbox[3] x_fraction = n_cells / (distance([west, south], [east, south], options)) cell_width_deg = x_fraction * (east - west) y_fraction = n_cells / (distance([west, south], [west, north], options)) cell_height_deg = y_fraction * (north - south) # rows & columns bbox_width = east - west bbox_height = north - south columns = int(bbox_width // cell_width_deg) rows = int(bbox_height // cell_height_deg) # if the grid does not fill the bbox perfectly, center it. delta_x = (bbox_width - columns * cell_width_deg) / 2 delta_y = (bbox_height - rows * cell_height_deg) / 2 # iterate over columns & rows current_x = west + delta_x while current_x <= east: current_y = south + delta_y while current_y <= north: cell_point = point([current_x, current_y], options.get("properties", {})) if "mask" in options: if boolean_within(cell_point, options["mask"]): results.append(cell_point) else: results.append(cell_point) current_y += cell_height_deg current_x += cell_width_deg return feature_collection(results)
def triangle_grid( bbox: List[float], cell_side: Union[int, float], options: Dict = {}, ) -> FeatureCollection: """ Creates a square of rectangles from a bounding box, Feature or FeatureCollection. :param bbox: Array extent in [minX, minY, maxX, maxY] order :param cell_side: dimension of each cell :param options: Optional parameters [options["units"]]: units ("degrees", "radians", "miles", "kilometers") of the given cell_width and cell_height [options["mask"]]: if passed a Polygon or MultiPolygon here, the grid Points will be created only inside it [options["properties"]]: passed to each point of the grid :returns: FeatureCollection of a grid of polygons """ if not isinstance(options, dict): options = {} results = [] west = bbox[0] south = bbox[1] east = bbox[2] north = bbox[3] x_fraction = cell_side / (distance([west, south], [east, south], options)) cell_width_deg = x_fraction * (east - west) y_fraction = cell_side / (distance([west, south], [west, north], options)) cell_height_deg = y_fraction * (north - south) # if the grid does not fill the bbox perfectly, center it. xi = 0 current_x = west while current_x <= east: yi = 0 current_y = south while current_y <= north: cell_triangle1 = None cell_triangle2 = None if (xi % 2 == 0) and (yi % 2 == 0): cell_triangle1 = polygon( [[ [current_x, current_y], [current_x, current_y + cell_height_deg], [current_x + cell_width_deg, current_y], [current_x, current_y], ]], options.get("properties", {}), ) cell_triangle2 = polygon( [[ [current_x, current_y + cell_height_deg], [ current_x + cell_width_deg, current_y + cell_height_deg ], [current_x + cell_width_deg, current_y], [current_x, current_y + cell_height_deg], ]], options.get("properties", {}), ) elif (xi % 2 == 0) and (yi % 2 == 1): cell_triangle1 = polygon( [[ [current_x, current_y], [ current_x + cell_width_deg, current_y + cell_height_deg ], [current_x + cell_width_deg, current_y], [current_x, current_y], ]], options.get("properties", {}), ) cell_triangle2 = polygon( [[ [current_x, current_y], [current_x, current_y + cell_height_deg], [ current_x + cell_width_deg, current_y + cell_height_deg ], [current_x, current_y], ]], options.get("properties", {}), ) elif (yi % 2 == 0) and (xi % 2 == 1): cell_triangle1 = polygon( [[ [current_x, current_y], [current_x, current_y + cell_height_deg], [ current_x + cell_width_deg, current_y + cell_height_deg ], [current_x, current_y], ]], options.get("properties", {}), ) cell_triangle2 = polygon( [[ [current_x, current_y], [ current_x + cell_width_deg, current_y + cell_height_deg ], [current_x + cell_width_deg, current_y], [current_x, current_y], ]], options.get("properties", {}), ) elif (yi % 2 == 1) and (xi % 2 == 1): cell_triangle1 = polygon( [[ [current_x, current_y], [current_x, current_y + cell_height_deg], [current_x + cell_width_deg, current_y], [current_x, current_y], ]], options.get("properties", {}), ) cell_triangle2 = polygon( [[ [current_x, current_y + cell_height_deg], [ current_x + cell_width_deg, current_y + cell_height_deg ], [current_x + cell_width_deg, current_y], [current_x, current_y + cell_height_deg], ]], options.get("properties", {}), ) if "mask" in options: if boolean_intersects(options["mask"], cell_triangle1): results.append(cell_triangle1) if boolean_intersects(options["mask"], cell_triangle2): results.append(cell_triangle2) else: results.append(cell_triangle1) results.append(cell_triangle2) current_y += cell_height_deg yi += 1 current_x += cell_width_deg xi += 1 return feature_collection(results)
def hex_grid( bbox: List[float], cell_side: Union[int, float], options: Dict = {}, ) -> FeatureCollection: """ Takes a bounding box and the diameter of the cell and returns a FeatureCollection of flat-topped hexagons or triangles aligned in an "odd-q" vertical grid as described in [Hexagonal Grids](http://www.redblobgames.com/grids/hexagons/). :param bbox: Array extent in [minX, minY, maxX, maxY] order :param n_cells: length of the side of the the hexagons or triangles, in units. It will also coincide with the radius of the circumcircle of the hexagons :param options: Optional parameters [options["units"]]: units ("degrees", "radians", "miles", "kilometers") of the given cell_width and cell_height [options["mask"]]: if passed a Polygon or MultiPolygon here, the grid Points will be created only inside it [options["properties"]]: passed to each point of the grid [options["triangles"]]: whether to return as triangles instead of hexagons :returns: FeatureCollection of a grid of polygons """ if not isinstance(options, dict): options = {} has_triangles = options.get("triangles", None) results = [] west = bbox[0] south = bbox[1] east = bbox[2] north = bbox[3] center_y = (south + north) / 2 center_x = (west + east) / 2 x_fraction = (cell_side * 2) / (distance([west, center_y], [east, center_y], options)) cell_width_deg = x_fraction * (east - west) y_fraction = (cell_side * 2 / (distance([center_x, south], [center_x, north], options))) cell_height_deg = y_fraction * (north - south) radius = cell_width_deg / 2 hex_width = radius * 2 hex_height = math.sqrt(3) / 2 * cell_height_deg # rows & columns bbox_width = east - west bbox_height = north - south x_interval = 3 / 4 * hex_width y_interval = hex_height x_span = (bbox_width - hex_width) / (hex_width - radius / 2) x_count = int(x_span) x_adjust = (((x_count * x_interval - radius / 2) - bbox_width) / 2 - radius / 2 + x_interval / 2) y_count = int((bbox_height - hex_height) / hex_height) y_adjust = (bbox_height - y_count * hex_height) / 2 has_offset_y = (y_count * hex_height - bbox_height) > (hex_height / 2) if has_offset_y: y_adjust -= hex_height / 4 cosines = [] sines = [] for i in range(6): angle = 2 * math.pi / 6 * i cosines.append(math.cos(angle)) sines.append(math.sin(angle)) results = [] for x in range(x_count + 1): for y in range(y_count + 1): is_odd = x % 2 == 1 if (y == 0) and is_odd: continue if (y == 0) and has_offset_y: continue center_x = x * x_interval + west - x_adjust center_y = y * y_interval + south + y_adjust if is_odd: center_y -= hex_height / 2 if has_triangles: triangles = hex_triangles( [center_x, center_y], cell_width_deg / 2, cell_height_deg / 2, options.get("properties", {}).copy(), cosines, sines, ) for triangle in triangles: if "mask" in options: if boolean_intersects(options["mask"], triangle): results.append(triangle) else: results.append(triangle) else: hex = hexagon( [center_x, center_y], cell_width_deg / 2, cell_height_deg / 2, options.get("properties", {}).copy(), cosines, sines, ) if "mask" in options: if boolean_intersects(options["mask"], hex): results.append(hex) else: results.append(hex) return feature_collection(results)
class TestGeometryTypeFromFeatures: @pytest.mark.parametrize( "fixture", [ pytest.param(fixture, id=fixture_name) for fixture_name, fixture in fixtures.items() ], ) def test_get_geometry_type_from_features_geojson(self, fixture): result = get_geometry_type(fixture["in"]) if isinstance(result, tuple): assert result == tuple(fixture["out"]) else: assert result == fixture["out"][0] @pytest.mark.parametrize( "input_value,output_value", [ pytest.param( point([4.83, 45.75], as_geojson=False), "Point", id="point_feature_object", ), pytest.param( line_string([[4.86, 45.76], [4.85, 45.74]], as_geojson=False), "LineString", id="line_string_feature_object", ), pytest.param( polygon( [[ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ]], as_geojson=False, ), "Polygon", id="polygon_feature_object", ), pytest.param( feature_collection( [ polygon( [[ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ]], as_geojson=False, ), line_string([[4.86, 45.76], [4.85, 45.74]], as_geojson=False), point([4.83, 45.75]), ], as_geojson=False, ), ("Polygon", "LineString", "Point"), id="feature_collection_object", ), pytest.param( Point([4.83, 45.75]), "Point", id="Point_object", ), pytest.param( LineString([[4.86, 45.76], [4.85, 45.74]]), "LineString", id="LineString_object", ), ], ) def test_get_geometry_from_features_objects(self, input_value, output_value): assert get_geometry_type(input_value) == output_value @pytest.mark.parametrize( "input_value,output_value", [ pytest.param( ([4.83, 45.75], ["Point"]), "Point", id="point_feature_object", ), pytest.param( ([[4.86, 45.76], [4.85, 45.74]], ["LineString"]), "LineString", id="line_string_feature_object", ), pytest.param( ( [[ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ]], ["Polygon"], ), "Polygon", id="polygon_feature_object", ), ], ) def test_get_geometry_from_lists(self, input_value, output_value): assert get_geometry_type(*input_value) == output_value @pytest.mark.parametrize( "input_value, exception_value", [ pytest.param( (4.83), error_code_messages["InvalidGeometry"](allowed_types_default), id="InvalidFloat", ), pytest.param( (4), error_code_messages["InvalidGeometry"](allowed_types_default), id="InvalidInt", ), pytest.param( ("4.83"), error_code_messages["InvalidGeometry"](allowed_types_default), id="InvalidString", ), pytest.param( (None), error_code_messages["InvalidGeometry"](allowed_types_default), id="InvalidNone", ), pytest.param( ({}), error_code_messages["InvalidGeometry"](allowed_types_default), id="InvalidFeaturesInput", ), ], ) def test_general_exception(self, input_value, exception_value): with pytest.raises(Exception) as excinfo: get_geometry_type(input_value) assert excinfo.type == InvalidInput assert str(excinfo.value) == exception_value @pytest.mark.parametrize( "input_value,exception_value", [ pytest.param( (point([4.83, 45.75], as_geojson=False), ("LineString", )), error_code_messages["InvalidGeometry"](["LineString"]), id="point_vs_linestring_feature_object", ), pytest.param( ( line_string([[4.86, 45.76], [4.85, 45.74]], as_geojson=False), ["Point", "MultiPoint"], ), error_code_messages["InvalidGeometry"](["Point", "MultiPoint" ]), id="point_vs_line_string_feature_object", ), pytest.param( ( polygon( [[ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ]], as_geojson=False, ), ["Point"], ), error_code_messages["InvalidGeometry"](["Point"]), id="point_vs_polygon_feature_object", ), pytest.param( ( feature_collection( [ polygon( [[ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ]], as_geojson=False, ), line_string([[4.86, 45.76], [4.85, 45.74]], as_geojson=False), point([4.83, 45.75]), ], as_geojson=False, ), ["Polygon", "Point"], ), error_code_messages["InvalidGeometry"](["Polygon", "Point"]), id="one_wrong_feature_collection_object", ), pytest.param( (Point([4.83, 45.75]), ["MultiPoint"]), error_code_messages["InvalidGeometry"](["MultiPoint"]), id="No_Point_object", ), pytest.param( (LineString([[4.86, 45.76], [4.85, 45.74] ]), ["MultiLineString"]), error_code_messages["InvalidGeometry"](["MultiLineString"]), id="No_LineString_object", ), ], ) def test_specifid_exception(self, input_value, exception_value): with pytest.raises(Exception) as excinfo: get_geometry_type(*input_value) assert excinfo.type == InvalidInput assert str(excinfo.value) == exception_value
def polygon_tangents( start_point: PointFeature, polygon: PolygonFeature ) -> FeatureCollection: """ Finds the tangents of a {Polygon or(MultiPolygon} from a {Point}. more: http://geomalgorithms.com/a15-_tangents.html :param point: point [lng, lat] or Point feature to calculate the tangent points from :param polygon: polygon to get tangents from :return: Feature Collection containing the two tangent points """ point_features = [] point_coord = get_coords_from_features(start_point, ("Point",)) polygon_coords = get_coords_from_features(polygon, ("Polygon", "MultiPolygon")) geometry_type = get_geometry_type(polygon) if isinstance(geometry_type, str): geometry_type = [geometry_type] polygon_coords = [polygon_coords] box = bbox(polygon) near_point_index = 0 near_point = False # If the point lies inside the polygon bbox then it's a bit more complicated # points lying inside a polygon can reflex angles on concave polygons if ( (point_coord[0] > box[0]) and (point_coord[0] < box[2]) and (point_coord[1] > box[1]) and (point_coord[1] < box[3]) ): near_point = nearest_point(start_point, explode(polygon)) near_point_index = near_point["properties"]["featureIndex"] for geo_type, poly_coords in zip(geometry_type, polygon_coords): if geo_type == "Polygon": tangents = process_polygon( poly_coords, point_coord, near_point, near_point_index ) # bruteforce approach # calculate both tangents for each polygon # define all tangents as a new polygon and calculate tangetns out of those coordinates elif geo_type == "MultiPolygon": multi_tangents = [] for poly_coord in poly_coords: tangents = process_polygon( poly_coord, point_coord, near_point, near_point_index ) multi_tangents.extend(tangents) tangents = process_polygon( [multi_tangents], point_coord, near_point, near_point_index ) r_tangents = tangents[0] l_tangents = tangents[1] point_features.extend([point(r_tangents), point(l_tangents)]) return feature_collection(point_features)
class TestGeometryFromFeatures: @pytest.mark.parametrize( "fixture", [ pytest.param(fixture, id=fixture_name) for fixture_name, fixture in fixtures.items() ], ) def test_get_geometry_from_features_geojson(self, fixture): try: allowed_types = fixture["in"]["properties"]["allowed_types"] except TypeError: allowed_types = fixture["in"][0] fixture["in"] = fixture["in"][1] assert ( get_geometry_from_features(fixture["in"], allowed_types) == fixture["out"] ) @pytest.mark.parametrize( "input_value,output_value", [ pytest.param( (point([4.83, 45.75], as_geojson=False), ["Point"]), Point([4.83, 45.75]), id="point_feature_object", ), pytest.param( ( line_string([[4.86, 45.76], [4.85, 45.74]], as_geojson=False), ["LineString"], ), LineString([[4.86, 45.76], [4.85, 45.74]]), id="line_string_feature_object", ), pytest.param( ( polygon( [ [ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ] ], as_geojson=False, ), ["Polygon"], ), Polygon( [ [ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ] ] ), id="polygon_feature_object", ), pytest.param( ( feature_collection( [ polygon( [ [ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ] ], as_geojson=False, ), line_string( [[4.86, 45.76], [4.85, 45.74]], as_geojson=False ), point([4.83, 45.75]), ], as_geojson=False, ), ["LineString", "Polygon", "Point"], ), [ Polygon( [ [ [4.82, 45.79], [4.88, 45.79], [4.91, 45.76], [4.89, 45.72], [4.82, 45.71], [4.77, 45.74], [4.77, 45.77], [4.82, 45.79], ] ] ), LineString([[4.86, 45.76], [4.85, 45.74]]), Point([4.83, 45.75]), ], id="feature_collection_object", ), pytest.param( (Point([4.83, 45.75]), ["Point"]), Point([4.83, 45.75]), id="Point_object", ), pytest.param( (LineString([[4.86, 45.76], [4.85, 45.74]]), ["LineString"],), LineString([[4.86, 45.76], [4.85, 45.74]]), id="LineString_object", ), ], ) def test_get_geometry_from_features_objects(self, input_value, output_value): assert get_geometry_from_features(*input_value) == output_value @pytest.mark.parametrize( "input_value, exception_value", [ pytest.param( (point([4.83, 45.75], as_geojson=False), ["LineString"]), error_code_messages["InvalidGeometry"](["LineString"]), id="InvalidGeometry-geojson", ), pytest.param( ([4.83, 45.75], ["LineString"]), error_code_messages["InvalidGeometry"](["LineString"]), id="InvalidGeometry-list", ), pytest.param( ([4.83], ["Point"]), error_code_messages["InvalidPointInput"], id="InvalidPoint", ), pytest.param( ({}, ["Point"]), error_code_messages["InvalidGeometry"](["Point"]), id="InvalidFeaturesInput", ), ], ) def test_exception(self, input_value, exception_value): with pytest.raises(Exception) as excinfo: get_geometry_from_features(*input_value) assert excinfo.type == InvalidInput assert str(excinfo.value) == exception_value