def bearing(start, end, options=None): """ Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees) :param start: starting point [lng, lat] or Point feature :param end: ending point [lng, lat] or Point feature :param options: dictionary with options: [options["final"]] - calculates the final bearing if true :return: bearing in decimal degrees, between -180 and 180 (positive clockwise) """ if not options: options = {} if isinstance(options, dict) and "final" in options: return calculate_final_bearing(start, end) start = get_coords_from_features(start, ["Point"]) end = get_coords_from_features(end, ["Point"]) lon1 = degrees_to_radians(start[0]) lon2 = degrees_to_radians(end[0]) lat1 = degrees_to_radians(start[1]) lat2 = degrees_to_radians(end[1]) a = np.sin(lon2 - lon1) * np.cos(lat2) b = np.cos(lat1) * np.sin(lat2) - np.sin(lat1) * np.cos(lat2) * np.cos(lon2 - lon1) return radians_to_degrees(np.arctan2(a, b))
def rhumb_bearing( origin: Union[Sequence, Dict, Feature], destination: Union[Sequence, Dict, Feature], options: Dict = None, ) -> float: """ Takes two {Point|points} and finds the bearing angle between them along a Rhumb line * i.e. the angle measured in degrees start the north line (0 degrees) https://en.wikipedia.org/wiki/Rhumb_line :param start: starting point [lng, lat] or Point feature :param end: ending point [lng, lat] or Point feature :param options: Optional parameters [options["final"]]: Calculates the final bearing if True :return: bearing from north in decimal degrees """ if not isinstance(options, dict): options = {} origin = get_coords_from_features(origin, ["Point"]) destination = get_coords_from_features(destination, ["Point"]) final = options.get("final", False) if final: bearing = calculate_rhumb_bearing(destination, origin) else: bearing = calculate_rhumb_bearing(origin, destination) return bearing
def rhumb_distance(origin, destination, options: Dict = None) -> float: """ Calculates the rhumb distance between two Points. Units are defined in helpers._units # https://en.wikipedia.org/wiki/Rhumb_line :param start: starting point [lng, lat] or Point feature :param end: ending point [lng, lat] or Point feature :param options: dictionary with units as an attribute. Units are defined in helpers._units :return: distance between the 2 points """ if not isinstance(options, dict): options = {} origin = get_coords_from_features(origin, ["Point"]) destination = get_coords_from_features(destination, ["Point"]) # compensate the crossing of the 180th meridian (https://macwright.org/2016/09/26/the-180th-meridian.html) # solution from https://github.com/mapbox/mapbox-gl-js/issues/3250#issuecomment-294887678 if (destination[0] - origin[0]) > 180: destination[0] -= 360 elif (origin[0] - destination[0]) > 180: destination[0] += 360 distance_in_meters = calculate_rhumb_distance(origin, destination) distance = convert_length( distance_in_meters, "meters", options.get("units", "kilometers") ) return distance
def test_exception(self, input_value, exception_value): with pytest.raises(Exception) as excinfo: get_coords_from_features(*input_value) assert excinfo.type == InvalidInput assert str(excinfo.value) == exception_value
def distance(start, end, options=None): """ Calculates the distance between two Points in degrees, radians, miles, or kilometers. This uses the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) to account for global curvature. :param start: starting point [lng, lat] or Point feature :param end: ending point [lng, lat] or Point feature :param options: dictionary with units as an attribute. Can be degrees, radians, miles, or kilometers :return: distance between the 2 points """ kwargs = {} if isinstance(options, dict) and "units" in options: kwargs.update(options) coordinates1 = get_coords_from_features(start, ["Point"]) coordinates2 = get_coords_from_features(end, ["Point"]) d_lat = degrees_to_radians(coordinates2[1] - coordinates1[1]) d_lon = degrees_to_radians(coordinates2[0] - coordinates1[0]) lat1 = degrees_to_radians(coordinates1[1]) lat2 = degrees_to_radians(coordinates2[1]) distance_rad = calculate_radians_distance(d_lon, d_lat, lat1, lat2) return radians_to_length(distance_rad, **kwargs)
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 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 get_line_segments(line: LinePolyFeature) -> Sequence: """ Gets segments from a line feature :param line: any LineString or Polygon :return: sequence of segmetns """ segments = [] geometry_type = get_geometry_type(line) if isinstance(geometry_type, str): geometry_type = [geometry_type] for line_geo in geometry_type: if line_geo in ["MultiPolygon", "Polygon"]: line = polygon_to_line(line) line_geo = line["geometry"]["type"] line_coords = get_coords_from_features( line, ("LineString", "MultiLineString")) if line_geo in ["LineString" ] and get_input_dimensions(line_coords) == 2: line_coords = [line_coords] for line_coord in line_coords: segments.extend(list(zip(line_coord, line_coord[1:]))) return segments
def centroid(features, options=None): """ Takes one or more features and calculates the centroid using the mean of all vertices. This lessens the effect of small islands and artifacts when calculating the centroid of a set of polygons. :param features: GeoJSON features to be centered :param options: optional parameters [options["properties"]={}] Translate GeoJSON Properties to Point :return: a Point feature corresponding to the centroid of the input features """ if not options: options = {} coords = get_coords_from_features(features) if get_input_dimensions(coords) == 1: coords = [coords] x_sum = 0 y_sum = 0 length = 0 x_sum, y_sum, length = reduce(reduce_coords, coords, [x_sum, y_sum, length]) return point([x_sum / length, y_sum / length], options.get("properties", None))
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 boolean_point_on_line( point: PointFeature, line: LineFeature, options: Dict = {} ) -> bool: """ Returns True if a point is on a line else False. Accepts a optional parameter to ignore the start and end vertices of the linestring. :param point: {Point} GeoJSON Point :param line: {LineString} GeoJSON LineString :param options: Optional parameters [options["ignoreEndVertices"]=False] whether to ignore the start and end vertices :return: boolean True/False if point is on line """ if not isinstance(options, dict): options = {} point_on_line = False ignore_end_vertices = options.get("ignoreEndVertices", False) point_coord = get_coords_from_features(point, ("Point",)) line_coords = get_coords_from_features(line, ("LineString",)) for i in range(len(line_coords) - 1): if ignore_end_vertices: # ignore if point_coord are the line start if (i == 0) and (point_coord == line_coords[i]): continue # ignore if point_coord are the line end if ((i + 1) == (len(line_coords) - 1)) and ( point_coord == line_coords[i + 1] ): continue point_on_line = point_on_segment( point_coord, line_coords[i], line_coords[i + 1] ) if point_on_line: break return point_on_line
def test_get_coords_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_coords_from_features(fixture["in"], allowed_types) == fixture["out"]
def test_bbox_polygon(self, fixture): polygon = bbox_polygon(fixture["in"]) assert polygon == fixture["out"] coordinates = get_coords_from_features(polygon) assert len(coordinates[0]) == 5 assert coordinates[0][0][0] == coordinates[0][len(coordinates) - 1][0] assert coordinates[0][0][1] == coordinates[0][len(coordinates) - 1][1]
def rhumb_destination(features: Dict, options: Dict = None) -> Point: """ Returns the destination {Point} having travelled the given distance along a Rhumb line from the origin Point with the (varant) given bearing. # https://en.wikipedia.org/wiki/Rhumb_line :param features: any GeoJSON feature or feature collection :param properties: specification to calculate the rhumb line [options["distance"]=100] distance from the starting point [options["bearing"]=180] varant bearing angle ranging from -180 to 180 degrees from north [options["units"]=kilometers] units: specifies distance (can be degrees, radians, miles, or kilometers) :param options: optional parameters also be part of features["properties"] [options["units"]={}] can be degrees, radians, miles, or kilometers [options["properties"]={}] Translate GeoJSON Properties to Point [options["id"]={}] Translate GeoJSON Id to Point :return: a FeatureDestination point. """ if not options: options = features.get("properties", {}) coords = get_coords_from_features(features, ["Point"]) bearing = options.get("bearing", 180) distance = options.get("dist", 100) units = options.get("units", "kilometers") distance_in_meters = convert_length(abs(distance), original_unit=units, final_unit="meters") if distance < 0: distance_in_meters *= -1 destination = calculate_rhumb_destination(coords, distance_in_meters, bearing) # compensate the crossing of the 180th meridian: # (https://macwright.org/2016/09/26/the-180th-meridian.html) # solution from: # https://github.com/mapbox/mapbox-gl-js/issues/3250#issuecomment-294887678 if (destination[0] - coords[0]) > 180: destination[0] -= 360 elif (coords[0] - destination[0]) > 180: destination[0] += 360 return point(destination, options.get("properties", None))
def point_to_line_distance( point: Union[Sequence, Dict, Feature], line: Union[Sequence, Dict, Feature], options: Dict = None, ) -> float: """ Returns the minimum distance between a {Point} and a {LineString}, being the distance from a line the minimum distance between the point and any segment of the `LineString` http://geomalgorithms.com/a02-_lines.html :param point: Point GeoJSON Feature or Geometry :param line: LineString GeoJSON Feature or Geometry :param options: Optional parameters [options["units"]]: any supported unit (e.g. degrees, radians, miles...) [options["method"]]: geodesic or 'planar for distance calculation :return: distance between point and line """ dist = [] if not isinstance(options, dict): options = {"method": "geodesic", "units": "kilometers"} point = get_coords_from_features(point, ["Point"]) line = get_coords_from_features(line, ["LineString"]) for i in range(1, len(line)): dist.append( get_distance_to_segment(point, line[i - 1], line[i], options.get("method", "geodesic"))) dist = convert_length(min(dist), "degrees", options.get("units", "kilometers")) return dist
def get_features(feature: Any) -> List[Union[str, Sequence]]: """ Takes any feature and returns the geometry type with coordinates. :param feature: {GeoJSON} feature any Feature or Geometry :return: List of geometry type and coordinate sequence """ feature_coords = get_coords_from_features(feature) feature_geometry = get_geometry_type(feature) if isinstance(feature_geometry, (list, tuple)): feature_geometry = feature_geometry[0] return [feature_geometry, feature_coords]
def great_circle(start, end, options=None): """ Returns the great circle route as LineString :param start: source point feature :param end: destination point feature :param options: Optional parameters [options["properties"]={}] line feature properties [options.npoints=100] number of points :return: great circle line feature """ if not options or not isinstance(options, dict): options = {} start = get_coords_from_features(start, ["Point"]) end = get_coords_from_features(end, ["Point"]) properties = options.get("properties", {}) npoints = options.get("npoints", 100) properties["npoints"] = npoints gc = GreatCircle(start, end, properties) return gc.to_geojson()
def bbox(features): """ Takes a set of features and returns a bounding box containing of all input features. :param features: any GeoJSON feature or feature collection :return: bounding box extent in [minX, minY, maxX, maxY] order """ bounding_box = [np.inf, np.inf, -np.inf, -np.inf] coords = get_coords_from_features(features) if get_input_dimensions(coords) == 1: coords = [coords] return reduce(reduce_coords, coords, bounding_box)
def destination(origin, distance, bearing, options=None): """ Takes a Point and calculates the location of a destination point given a distance in degrees, radians, miles, or kilometers; and bearing in degrees. This uses the [Haversine formula](http://en.wikipedia.org/wiki/Haversine_formula) to account for global curvature. :param origin: starting point :param distance: distance from the origin point :param bearing: bearing ranging from -180 to 180 :param options: optional parameters [options["units"]='kilometers'] miles, kilometers, degrees, or radians [options["properties"]={}] Translate properties to Point :return: destination GeoJSON Point feature """ if not options: options = {} kwargs = {} if "units" in options: kwargs["units"] = options.get("units") coords = get_coords_from_features(origin, ["Point"]) longitude1 = degrees_to_radians(coords[0]) latitude1 = degrees_to_radians(coords[1]) bearing_rads = degrees_to_radians(bearing) radians = length_to_radians(distance, **kwargs) latitude2 = asin( sin(latitude1) * cos(radians) + cos(latitude1) * sin(radians) * cos(bearing_rads)) longitude2 = longitude1 + atan2( sin(bearing_rads) * sin(radians) * cos(latitude1), cos(radians) - sin(latitude1) * sin(latitude2), ) lng = truncate(radians_to_degrees(longitude2), 6) lat = truncate(radians_to_degrees(latitude2), 6) return point([lng, lat], options.get("properties", None))
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
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 flatten_feature(feature: Any) -> List[Union[str, Sequence]]: """ Takes any feature and return the simple geometry type with coordinates. A MultiPoint will be flatten to Point, MultiLineString to LineString and MultiPolygon to Polygon :param feature: {GeoJSON} feature any Feature or Geometry :return: List of geometry type and coordinate sequence """ feature_coords = get_coords_from_features(feature) feature_geometry = get_geometry_type(feature) if isinstance(feature_geometry, (list, tuple)): feature_geometry = feature_geometry[0] if feature_geometry in ["MultiPoint", "MultiLineString", "MultiPolygon"]: if feature_geometry == "MultiPoint": feature_geometry = "Point" elif feature_geometry == "MultiLineString": feature_geometry = "LineString" elif feature_geometry == "MultiPolygon": feature_geometry = "Polygon" flat_feature = [[feature_geometry, coords] for coords in feature_coords] else: flat_feature = [[feature_geometry, feature_coords]] return flat_feature
def is_poly_in_poly(feature_1: Sequence, feature_2: Sequence) -> bool: """ Checks if feature_1 polygon feature is in feature_2 polygon :param feature_1: Coordinates of polygon feature 1 :param feature_2: Coordinates of polygon feature 2 :return: boolean True/False if feature 1 is within feature 2 """ poly_bbox_1 = bbox(feature_1) poly_bbox_2 = bbox(feature_2) if not bbox_overlap(poly_bbox_2, poly_bbox_1): return False feature_1 = polygon_to_line(polygon(feature_1)) line_coords = get_coords_from_features(feature_1) for coords in line_coords: if not boolean_point_in_polygon(coords, feature_2): return False return True
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 boolean_point_in_polygon( point: Union[Sequence, Dict, Feature], polygon: Union[Dict, Feature], options: Dict = None, ): """ Takes a {@link Point} and a Polygon or MultiPolygon and determines if the point resides inside the polygon. The polygon can be convex or concave. The function accounts for holes. reference: http://en.wikipedia.org/wiki/Even%E2%80%93odd_rule modified from: https://github.com/substack/point-in-polygon/blob/master/index.js which was modified from http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html :param point: input Point Feature :param polygon: input Polygon or MultiPolygon Feature :param options: optional parameters [options["ignoreBoundary"]] True if polygon boundary should be ignored when determining if the point is inside the polygon otherwise False. :return: True if the Point is inside the Polygon; False otherwise """ if not isinstance(options, dict): options = {} ignore_boundary = options.get("ignoreBoundary", False) point_coords = get_coords_from_features(point, ["Point"]) polygon_coords = get_coords_from_features(polygon, valid_polygons) geometry_type = get_geometry_type(polygon, valid_polygons) bbox = bounding_box(polygon) if not in_bbox(point_coords, bbox): return False if isinstance(geometry_type, str): geometry_type = [geometry_type] polygon_coords = [polygon_coords] inside_polygon = False for geo_type, poly_coords in zip(geometry_type, polygon_coords): if geo_type == "Polygon": poly_coords = [poly_coords] for polygon in poly_coords: # check if it is in the outer ring first if in_ring(point_coords, polygon[0], ignore_boundary): in_hole = False for ring in polygon[1:]: if in_ring(point_coords, ring, not ignore_boundary): in_hole = True if not in_hole: inside_polygon = True if inside_polygon: break return inside_polygon
def point_on_feature(features: GeoJSON) -> Point: """ Takes a Feature or FeatureCollection and returns a {Point} guaranteed to be on the surface of the feature. Given a {Polygon}, the point will be in the area of the polygon Given a {LineString}, the point will be along the string Given a {Point}, the point will the same as the input :param features: any GeoJSON feature or feature collection :return: Point GeoJSON Feature on the surface of `input` """ feature_collection = normalize_to_feature_collection(features) center_point = center(feature_collection) center_coords = center_point.get("geometry").get("coordinates") # check to see if centroid is on surface center_on_surface = False geometry_type = get_geometry_type(feature_collection) geometry_coords = get_coords_from_features(feature_collection) if isinstance(geometry_type, str): geometry_type = [geometry_type] for geo_type, geo_coords in zip(geometry_type, geometry_coords): if geo_type in ["Point", "MultiPoint"]: if geo_type == "Point": geo_coords = [geo_coords] for point_coords in geo_coords: if (center_coords[0] == point_coords[0]) and (center_coords[1] == point_coords[1]): center_on_surface = True break elif geo_type in ["LineString", "MultiLineString"]: if geo_type == "LineString": geo_coords = [geo_coords] for line_coords in geo_coords: if boolean_point_on_line(center_coords, line_coords): center_on_surface = True break elif geo_type in ["Polygon", "MultiPolygon"]: if geo_type == "Polygon": geo_coords = polygon(geo_coords) else: geo_coords = multi_polygon(geo_coords) if boolean_point_in_polygon(center_point, geo_coords): center_on_surface = True break if center_on_surface: point_on_surface = center_point else: point_on_surface = nearest_point(center_point, feature_collection) return point_on_surface
def test_get_coords_from_features_objects(self, input_value, output_value): assert get_coords_from_features(*input_value) == output_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)