Exemplo n.º 1
0
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))
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
    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
Exemplo n.º 5
0
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)
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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)
Exemplo n.º 8
0
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
Exemplo n.º 9
0
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))
Exemplo n.º 10
0
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
Exemplo n.º 11
0
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
Exemplo n.º 12
0
    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"]
Exemplo n.º 13
0
    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]
Exemplo n.º 14
0
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))
Exemplo n.º 15
0
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
Exemplo n.º 16
0
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]
Exemplo n.º 17
0
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()
Exemplo n.º 18
0
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)
Exemplo n.º 19
0
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))
Exemplo n.º 20
0
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
Exemplo n.º 21
0
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
Exemplo n.º 22
0
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
Exemplo n.º 23
0
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
Exemplo n.º 24
0
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]])
Exemplo n.º 25
0
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
Exemplo n.º 26
0
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
Exemplo n.º 27
0
    def test_get_coords_from_features_objects(self, input_value, output_value):

        assert get_coords_from_features(*input_value) == output_value
Exemplo n.º 28
0
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)