Skip to main content

A lightweight implementation of shapes drawn across a geo-temporal plane.

Project description

Geostructures

Unit Tests

A lightweight implementation of shapes drawn across a geo-temporal plane.

Geostructures makes it easy to:

  • Define a variety of common shapes used for geospatial analysis
  • Perform common geospatial and geometric calculations
  • Convert to/from a variety of geospatial formats
  • Temporally bound data by instant or interval
  • Convert shapes and coordinates to/from geohashes
plotting

Installation

Geostructures is available on PYPI

$ pip install geostructures

Requirements

Geostructures only requires numpy to function.

Optional Requirements

  • pip install geostructures[df]

    • Add dataframe support for geopandas and pandas
  • pip install geostructures[h3]

    • Add support for geohashing using Uber's H3 algorithm
  • pip install geostructures[mgrs]

    • Add support for converting coordinates using the Military Grid Reference System (MGRS)
  • pip install geostructures[proj]

    • Add support for coordinate projection conversion

Overview

Geostructures provides a python interface for functionally defining various shapes drawn on a map. Unlike other libraries such as Shapely, these shapes retain their mathematical definitions rather than being simplified into N-sided polygons.

The shapes currently supported are:

  • Boxes
  • Circles
  • Ellipses
  • LineStrings
  • Points
  • Polygons
  • Rings/Wedges

All shapes may optionally be temporally-bound using a specific datetime or a datetime interval.

Additionally, geostructures provides convenience objects for representing chronologically-ordered (Track) and unordered (FeatureCollection) collections of the above shapes.

Quick Start

For an interactive introduction, please review our collection of Jupyter notebooks.

Creating GeoShapes

from geostructures import *

box = GeoBox(
    Coordinate(-0.154092, 51.539865),   # Northwest corner
    Coordinate(-0.140592, 51.505665),   # Southeast corner
)

circle = GeoCircle(
    Coordinate(-0.131092, 51.509865),   # centerpoint
    radius=500, 
)

ellipse = GeoEllipse(
    Coordinate(-0.093092, 51.529865),   # centerpoint
    semi_major=1_000,                   # The distance between the centerpoint and the furthest point along the circumference
    semi_minor=250,                     # The distance between the centerpoint and the closest point along the circumference
    rotation=45,                        # The angle of rotation (between 0 and 360)
)

ring = GeoRing(
    Coordinate(-0.116092, 51.519865),   # centerpoint
    inner_radius=800,
    outer_radius=1000,
    properties={"name": "ring"}
)

# Same as a ring, but with a min/max angle
wedge = GeoRing(
    Coordinate(-0.101092, 51.514865),   # centerpoint
    inner_radius=300,
    outer_radius=500,
    angle_min=60,                       # The minimum angle of the wedge
    angle_max=190,                      # The maximum angle of the wedge
)

linestring = GeoLineString(
    [
        Coordinate(-0.123092, 51.515865), Coordinate(-0.118092, 51.514665), Coordinate(-0.116092, 51.514865),
        Coordinate(-0.116092, 51.518865), Coordinate(-0.108092, 51.512865)
    ],
)

point = GeoPoint(
    Coordinate(-0.116092, 51.519865), 
)

polygon = GeoPolygon(
    [
        Coordinate(-0.116092, 51.509865), Coordinate(-0.111092, 51.509865), 
        Coordinate(-0.113092, 51.506865), Coordinate(-0.116092, 51.509865)  # Note that the last coordinate is the same as the first
    ],
)

Cutting Holes

Holes are defined using GeoShapes and can be cut from any individual shape (on its own or as a component of a multishape)

from geostructures import *

circle = GeoCircle(
    Coordinate(-0.131092, 51.509865), 
    radius=500, 
    holes=[
        GeoCircle(Coordinate(-0.131092, 51.509865), 250)
    ]
)

Defining Properties

You can attach whatever properties you want to any shape. Where supported (e.g. GeoJSON and shapefiles), these properties will remain with the shape when you convert it to a different format.

from geostructures import *

# You can define properties upon instantiation
point = GeoPoint(
    Coordinate(-0.116092, 51.519865), 
    properties={
        'example': 'property'
    }
)

# Or at any time afterwards (will mutate the shape)
point.set_property(
    'example',      # The property key
    2               # The property value
)

Creating MultiShapes

Multishapes are treated as lists of GeoShapes (of their corresponding type) and can be assigned properties/holes/time bounds in the same way.

from geostructures import *

# Multipolygons 
multipolygon = MultiGeoPolygon(
    [
        GeoCircle(Coordinate(-0.131092, 51.509865), 500),
        GeoBox(Coordinate(-0.154092, 51.539865), Coordinate(-0.140592, 51.505665))
    ],
    dt=...,
    properties=...,
)

multipoint = MultiGeoPoint(
    [
        GeoPoint(Coordinate(-0.154092, 51.539865)),
        GeoPoint(Coordinate(-0.140592, 51.505665))
    ],
    dt=...,
    properties=...,
)

multilinestring = MultiGeoLineString(
    [
        GeoLineString([Coordinate(-0.154092, 51.539865), Coordinate(-0.140592, 51.505665), ...]),
        GeoLineString([Coordinate(-0.116092, 51.518865), Coordinate(-0.108092, 51.512865), ...]),
    ],
    dt=...,
    properties=...,
)

Geometric Operations

Geostructures isn't a replacement for shapely (see conversion to shapely below), however supports many geometric operations.

from datetime import datetime
from geostructures import *

circle = GeoCircle(Coordinate(-0.131092, 51.509865), radius=500, dt=datetime(2020, 1, 1))
ellipse = GeoEllipse(Coordinate(-0.093092, 51.529865), semi_major=1_000, semi_minor=250, rotation=45,
                     dt=datetime(2020, 1, 1))

# True/False, do these shapes intersect?
circle.intersects(ellipse)  # Both temporally and spatially
circle.intersects_shape(ellipse)  # Only spatially

# True/False, does the circle fully contain the ellipse?
circle.contains(ellipse)  # Both temporally and spatially
circle.contains_shape(ellipse)  # Only spatially

# Return the rectangle that circumscribes this shape (as a GeoBox)
circle.circumscribing_rectangle()

# Return the circle that circumscribes this shape (as a GeoCircle)
ellipse.circumscribing_circle()

# Get the xmin, xmax, ymin, ymax of this shape
circle.bounds

# Get the area of this shape in meters squared (requires pyproj)
circle.area

# Get the volume of this shape in meters squared seconds (requires pyproj)
circle.volume

# Get the coordinates that define this shapes outer shell
circle.bounding_coords()  # default 36 bounding points
circle.bounding_coords(k=360)  # or define the number of points to increase/decrease precision

# Get a list of all the linear rings that comprise this shape (includes holes)
circle.linear_rings()  # Also accepts k

# Return the convex hull as a GeoPolygon (only for multi-shapes and collections)
multishape = MultiGeoPolygon([circle, ellipse])
multishape.convex_hull()  # Also accepts k

Converting Between Formats

All objects can be converted to/from most common geospatial formats.

from geostructures import *
from geostructures.collections import FeatureCollection

polygon = GeoPolygon(
    [
        Coordinate(-0.116092, 51.509865), Coordinate(-0.111092, 51.509865), 
        Coordinate(-0.113092, 51.506865), Coordinate(-0.116092, 51.509865)
    ]
)

# GeoJSON
polygon.to_geojson()
polygon.from_geojson( { a geojson object } )

# Well-Known Text (WKT)
polygon.to_wkt()
polygon.from_wkt( '<a wkt polygon string>' )

# Python Shapely
polygon.to_shapely()
polygon.from_shapely( a shapely polygon )

# Collections (and Tracks) of shapes have additional supported formats
collection = FeatureCollection([polygon])

# Creates a geopandas DataFrame
collection.to_geopandas()
collection.from_geopandas( a geopandas DataFrame )

# Creates a GeoJSON FeatureCollection
collection.to_geojson()
collection.from_geojson( { a geojson featurecollection } )

# Read/Write a FeatureCollection to an ESRI Shapefile
from zipfile import ZipFile
with ZipFile('shapefile_name.zip', 'w') as zfile:
    collection.to_shapefile(zfile)

collection.from_shapefile('shapefile_name.zip')

# Shapefiles may contain multiple feature layers, so you can control which ones get read 
collection.from_shapefile('shapefile_name.zip', read_layers=['layer1', 'layer2'])

Bounding Shapes by Time

All shapes can be bound by time instants or intervals

from datetime import datetime, timedelta
from geostructures import *
from geostructures.time import TimeInterval

# Shapes can be bounded by a datetime to represent an instant in time
point_in_time = GeoPoint(Coordinate(-0.154092, 51.539865), dt=datetime(2020, 5, 1, 12))

# Or they can be bounded by a time interval
span_of_time = GeoPoint(
    Coordinate(-0.155092, 51.540865), 
    dt=TimeInterval(
        datetime(2020, 5, 1, 13),
        datetime(2020, 5, 1, 14)
    )
)

# Time spans can also be set after instantiation
another_point = GeoPoint(Coordinate(-0.154092, 51.539865))
another_point.set_dt(TimeInterval(datetime(2020, 5, 1, 16), datetime(2020, 5, 1, 17)))

# You can buffer a time-bound shape with a timedelta
another_point.buffer_dt(timedelta(hours=6))

# Or strip shape's time bounds
another_point.strip_dt()

# Collections where all underlying shapes are time-bound can be represented as a Track, which
# supports additional features
track = Track([point_in_time, span_of_time])

# Slice by datetime
subset = track[datetime(2020, 5, 1, 12):datetime(2020, 5, 1, 13)]

# Get metrics between shapes
track.centroid_distances    # meters
track.speed_diffs           # meters per second
track.time_start_diffs      # timedeltas

Geohashing

Geostructures supports geohashing using both Uber's H3 and the original Niemeyer geohashing algorithm.

from geostructures import *
from geostructures.geohash import H3Hasher, NiemeyerHasher, h3_to_geopolygon, niemeyer_to_geobox

box = GeoBox(Coordinate(-0.154092, 51.539865), Coordinate(-0.140592, 51.505665))
circle = GeoCircle(Coordinate(-0.131092, 51.509865), radius=500)
collection = FeatureCollection([box, circle])

# Create a H3 hasher
hasher = H3Hasher(resolution=10)

# Hash a singular shape to return the list of geohashes
set_of_geohashes = hasher.hash_shape(box)

# Convert the geohash into its corresponding GeoShape
geopolygon = h3_to_geopolygon(set_of_geohashes.pop())

# Hash a collection of shapes to return a dictionary of { geohash: [ corresponding geoshapes ] }
# or supply a custom aggregation function
hashmap = hasher.hash_collection(collection)

# Alternatively, hash using the Niemeyer algorithm
hasher = NiemeyerHasher(length=8, base=16)
set_of_geohashes = hasher.hash_shape(box)
geobox = niemeyer_to_geobox(set_of_geohashes.pop(), base=16)
hashmap = hasher.hash_collection(collection)

Common Geospatial Calculations

from geostructures import Coordinate
from geostructures.calc import *

# The straight-line direction of travel to get from point A to point B
bearing_degrees(Coordinate(-0.154092, 51.539865), Coordinate(-0.140592, 51.505665))

# The great-sphere distance between two points
haversine_distance_meters(Coordinate(-0.154092, 51.539865), Coordinate(-0.140592, 51.505665))

# The resulting coordinate of traveling some distance from point A in a given direction
inverse_haversine_degrees(
    Coordinate(-0.154092, 51.539865),
    45,  # degrees clockwise from true north
    200  # the distance traveled (in meters)
)

# The same, except using radians for direction of travel
inverse_haversine_radians(Coordinate(-0.154092, 51.539865), 0.7853981633974483, 200)

# Rotate coordinates around a defined origin
rotate_coordinates(
    [
        Coordinate(-0.154092, 51.539865), 
        Coordinate(-0.140592, 51.505665)
    ],
    origin=Coordinate(-0.16, 50.24),
    degrees=45,
)

Projections

This library assumes that all geospatial terms and structures conform to the WGS84 standard (CRS 4326).

Sourcing

This library is designed to implement and extend the requirements of geospatial data laid out by:

Extensions / Related Projects

Geochron A companion package to geostructures enabling geo-spatial-temporal data structures

Reporting Issues / Requesting Features

The Geostructures team uses Github issues to track development goals. Please be as descriptive as possible so we can effectively triage your request.

Contributing

We welcome all contributors! Please review CONTRIBUTING.md for more information.

Developers

Carl Best (Sr. Data Scientist/Project Owner)
https://github.com/ccbest/

Eli Talbert (Sr. Data Scientist/PhD)
https://github.com/etalbert102

Jessica Moore (Sr. Data Scientist)
https://github.com/jessica-writes-code

Richard Marshall (Data Scientist/SME)
https://github.com/RichardMarshall13

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

geostructures-0.11.0.tar.gz (71.5 kB view details)

Uploaded Source

Built Distribution

geostructures-0.11.0-py3-none-any.whl (55.0 kB view details)

Uploaded Python 3

File details

Details for the file geostructures-0.11.0.tar.gz.

File metadata

  • Download URL: geostructures-0.11.0.tar.gz
  • Upload date:
  • Size: 71.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/5.1.1 CPython/3.9.19

File hashes

Hashes for geostructures-0.11.0.tar.gz
Algorithm Hash digest
SHA256 c2654102debf6f36a758b068a528d6d9a182f159a693f6afe6a376661c8a3c9c
MD5 a40b56d94fbd4aeeeeac14c7fa878477
BLAKE2b-256 8b0748ff94c1d996a9769b5499c779ad6950beeeb3abbf5d4271296d8803a1b8

See more details on using hashes here.

File details

Details for the file geostructures-0.11.0-py3-none-any.whl.

File metadata

File hashes

Hashes for geostructures-0.11.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4cd986ff493e627f7ba02a72c8709f1cf8a5319821f2000fd16b5c9aba2c3251
MD5 2ef043c61b2d6a16fe166216ada6551e
BLAKE2b-256 48e863c4ad0fe5e171b6540021231b240b49b55ba1b183c349287e8735df3347

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page