def test_midpoint(self, fixture): points = fixture["in"] mid_point = midpoint(points[0], points[1]) assert truncate(distance(points[0], mid_point), 2) == truncate(distance(points[1], mid_point), 2)
def test_exception(self): wrong_units = "foo" with pytest.raises(Exception) as excinfo: distance(point([0, 0]), point([10, 10]), {"units": wrong_units}) assert excinfo.type == InvalidInput assert str( excinfo.value) == error_code_messages["InvalidUnits"](wrong_units)
def length(features, options=None): """ Calculates the total length of the input Feature / FeatureCollection in the specified units. :param features: a Feature / FeatureCollection of types LineString, MultiLineString, Polygon or MultiPolygon :param options: optional parameters [options["units"]=kilometers] can be degrees, radians, miles, or kilometers :return: the measured distance """ coords = get_coords_from_features(features, [ "LineString", "MultiLineString", "Polygon", "MultiPolygon", ]) if any( isinstance(inner_item, list) for item in coords for inner_item in item): distances = list( map(lambda sub_item: length(sub_item, options), coords)) return sum(distances) total_distance = reduce( lambda accum, coord: accum + distance(coord[0], coord[1], options), zip(coords, coords[1:]), 0, ) return total_distance
def test_distance(self, units): pt1 = fixture["features"][0] pt2 = fixture["features"][1] options = {"units": units} assert distance(pt1, pt2, options) == expected_results[units]
def square(bbox): """ Takes a bounding box and calculates the minimum square bounding box that would contain the input. :param bbox: bounding box extent in [minX, minY, maxX, maxY] order :return: a square surrounding bbox """ if not isinstance(bbox, list) or len(bbox) != 4: raise InvalidInput(error_code_messages["InvalidBoundingBox"]) west = float(bbox[0]) south = float(bbox[1]) east = float(bbox[2]) north = float(bbox[3]) horizontal_distance = distance([west, south], [east, south]) vertical_distance = distance([west, south], [west, north]) if horizontal_distance >= vertical_distance: vertical_midpoint = (south + north) / 2 bounding_box = [ west, vertical_midpoint - ((east - west) / 2), east, vertical_midpoint + ((east - west) / 2), ] else: horizontal_midpoint = (west + east) / 2 bounding_box = [ horizontal_midpoint - ((north - south) / 2), south, horizontal_midpoint + ((north - south) / 2), north, ] return bounding_box
def midpoint(point1, point2): """ Takes two point features and returns a point midway between them. The midpoint is calculated geodesically, meaning the curvature of the earth is taken into account. :param point1: first point :param point2: second point :return: a point midway between point 1 and point 2 """ dist = distance(point1, point2) heading = bearing(point1, point2) mid_point = destination(point1, dist / 2, heading) return mid_point
def test_rhumb_distance(self, fixture): pt1 = fixture["in"]["features"][0] pt2 = fixture["in"]["features"][1] distances = { "miles": round(rhumb_distance(pt1, pt2, {"units": "miles"}), 6), "nauticalmiles": round( rhumb_distance(pt1, pt2, {"units": "nautical_miles"}), 6 ), "kilometers": round(rhumb_distance(pt1, pt2, {"units": "kilometers"}), 6), "greatCircleDistance": round( distance(pt1, pt2, {"units": "kilometers"}), 6 ), "radians": round(rhumb_distance(pt1, pt2, {"units": "radians"}), 6), "degrees": round(rhumb_distance(pt1, pt2, {"units": "degrees"}), 6), } assert distances == fixture["out"]
def calculate_distance_by_method(point_1: List, point_2: List, method: str) -> float: """ Wrapper to calculate the distance between two points. Depending on the method, if calls rhumb distance or haversine distance :param point_1: Point coordinates :param point_2: Point coordinates :param method: wether to calculate the distance based on geodesic (spheroid) or planar (flat) method. Valid options are: 'geodesic' or 'planar :return: distance between point and both segments in radians """ if method == "planar": dist = rhumb_distance(point_1, point_2, {"units": "degrees"}) else: dist = distance(point_1, point_2, {"units": "degrees"}) return dist
def nearest_point(target: Union[Sequence, Dict, Feature], features: GeoJson) -> Point: """ Calculates the closest reference point from a feature collection towards a target point This calculation is geodesic. :param target: targetPoint the reference point :param features: points against input point set :return: the closest point in the features set to the reference point """ min_distance = float("inf") nearest_point = None feature_index = None features = explode(features) points = features.get("features") target = get_coords_from_features(target, ["Point"]) for i, point in enumerate(points): dist = distance(target, point) if dist < min_distance: min_distance = dist nearest_point = deepcopy(point) feature_index = i if "properties" in nearest_point: nearest_point["properties"].update({ "featureIndex": feature_index, "distanceToPoint": min_distance }) else: nearest_point.update({ "properties": { "featureIndex": feature_index, "distanceToPoint": min_distance, } }) return nearest_point
def along(line, dist, options=None): """ Takes a LineString and returns a Point at a specified distance along the line :param line: input LineString :param dist: distance along the line :param options: optional parameters [options["units"]="kilometers"] can be degrees, radians, miles, or kilometers :return: Point `dist` `units` along the line """ if not options or not isinstance(options, dict): options = {} if not isinstance(dist, (float, int)) or dist < 0: raise InvalidInput(error_code_messages["InvalidDistance"]) coords = get_coords_from_features(line, ["LineString"]) travelled = 0 for i in range(len(coords)): if dist >= travelled and i == len(coords) - 1: break elif travelled >= dist: overshot = dist - travelled if not overshot: return point([truncate(coord, 6) for coord in coords[i]]) else: direction = bearing(coords[i], coords[i - 1]) - 180 interpolated = destination(coords[i], overshot, direction, options) return interpolated else: travelled += distance(coords[i], coords[i + 1]) return point([truncate(coord, 6) for coord in coords[-1]])
def test_distance_particular_case(self): assert distance(point([-180, -90]), point([180, -90])) == 0
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)