├── mogeo ├── serialization │ ├── geoarrow.mojo │ ├── __init__.mojo │ ├── json.mojo │ ├── wkt.mojo │ └── traits.mojo ├── test │ ├── algo │ │ └── __init__.mojo │ ├── __init__.mojo │ ├── fixtures │ │ ├── wkt │ │ │ ├── point │ │ │ │ └── point.wkt │ │ │ ├── multi_point │ │ │ │ ├── point.wkt │ │ │ │ └── point_z.wkt │ │ │ └── line_string │ │ │ │ └── curved.wkt │ │ └── geojson │ │ │ ├── multi_point │ │ │ ├── multi_point.geojson │ │ │ └── multi_point_z.geojson │ │ │ └── line_string │ │ │ ├── curved.geojson │ │ │ ├── straight.geojson │ │ │ └── zigzag.geojson │ ├── geog │ │ └── __init__.mojo │ ├── geom │ │ ├── __init__.mojo │ │ ├── test_empty.mojo │ │ ├── test_layout.mojo │ │ ├── test_enums_coorddims.mojo │ │ ├── test_multi_point.mojo │ │ ├── test_envelope.mojo │ │ ├── test_line_string.mojo │ │ └── test_point.mojo │ ├── constants.mojo │ ├── helpers.mojo │ └── pytest.mojo ├── geog │ ├── crs.mojo │ ├── __init__.mojo │ ├── feature.mojo │ └── feature_collection.mojo ├── algo │ └── __init__.mojo ├── geom │ ├── __init__.mojo │ ├── polygon.mojo │ ├── multi_polygon.mojo │ ├── multi_line_string.mojo │ ├── geometry_collection.mojo │ ├── empty.mojo │ ├── traits.mojo │ ├── enums.mojo │ ├── layout.mojo │ ├── envelope.mojo │ ├── line_string.mojo │ ├── multi_point.mojo │ └── point.mojo ├── bench │ └── __init__.mojo └── __init__.mojo ├── docs └── docstrings.monopic ├── .gitmodules ├── environment.yml ├── scripts └── setup-mojo-conda-env-macos.sh ├── Makefile ├── LICENSE ├── .github └── workflows │ └── tests.yaml ├── .gitignore └── README.md /mogeo/serialization/geoarrow.mojo: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mogeo/test/algo/__init__.mojo: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mogeo/geog/crs.mojo: -------------------------------------------------------------------------------- 1 | struct CRS: 2 | pass 3 | -------------------------------------------------------------------------------- /mogeo/test/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Test module. 3 | """ -------------------------------------------------------------------------------- /mogeo/algo/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Algorithms module. 3 | """ -------------------------------------------------------------------------------- /mogeo/geog/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Geographic module. 3 | """ -------------------------------------------------------------------------------- /mogeo/geog/feature.mojo: -------------------------------------------------------------------------------- 1 | struct Feature: 2 | pass 3 | -------------------------------------------------------------------------------- /mogeo/geom/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Geometric module. 3 | """ -------------------------------------------------------------------------------- /mogeo/geom/polygon.mojo: -------------------------------------------------------------------------------- 1 | struct Polygon: 2 | pass 3 | -------------------------------------------------------------------------------- /mogeo/bench/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Benchmarks module. 3 | """ 4 | -------------------------------------------------------------------------------- /mogeo/geom/multi_polygon.mojo: -------------------------------------------------------------------------------- 1 | struct MultiPolygon: 2 | pass 3 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/wkt/point/point.wkt: -------------------------------------------------------------------------------- 1 | POINT(-108.680 38.974) 2 | -------------------------------------------------------------------------------- /mogeo/geog/feature_collection.mojo: -------------------------------------------------------------------------------- 1 | struct FeatureCollection: 2 | pass 3 | -------------------------------------------------------------------------------- /mogeo/geom/multi_line_string.mojo: -------------------------------------------------------------------------------- 1 | struct MultiLineString: 2 | pass 3 | -------------------------------------------------------------------------------- /mogeo/test/geog/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for mogeo/geog module. 3 | """ 4 | -------------------------------------------------------------------------------- /mogeo/test/geom/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for mogeo/geom module. 3 | """ 4 | -------------------------------------------------------------------------------- /mogeo/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | MoGeo: Mojo Geospatial/Geometric Package 3 | """ 4 | -------------------------------------------------------------------------------- /mogeo/geom/geometry_collection.mojo: -------------------------------------------------------------------------------- 1 | struct GeometryCollection: 2 | pass 3 | -------------------------------------------------------------------------------- /docs/docstrings.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guidorice/mogeo/HEAD/docs/docstrings.monopic -------------------------------------------------------------------------------- /mogeo/test/constants.mojo: -------------------------------------------------------------------------------- 1 | let lon = -108.680 2 | let lat = 38.974 3 | let height = 8.0 4 | let measure = 42.0 5 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/wkt/multi_point/point.wkt: -------------------------------------------------------------------------------- 1 | MULTIPOINT (-108.68 38.974, -109.68 38.974, -108.68 39.974) 2 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/wkt/multi_point/point_z.wkt: -------------------------------------------------------------------------------- 1 | MULTIPOINT (-108.68 38.974 42, -109.68 38.974 43, -108.68 39.974 44) 2 | -------------------------------------------------------------------------------- /mogeo/serialization/__init__.mojo: -------------------------------------------------------------------------------- 1 | """ 2 | Serialization module. 3 | """ 4 | 5 | from .json import * 6 | from .wkt import * 7 | from .geoarrow import * 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "mogeo/test/fixtures/geoarrow/geoarrow-data"] 2 | path = mogeo/test/fixtures/geoarrow/geoarrow-data 3 | url = https://github.com/geoarrow/geoarrow-data.git 4 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/wkt/line_string/curved.wkt: -------------------------------------------------------------------------------- 1 | LINESTRING (-122.500 37.500, -122.588 37.469, -122.669 37.438, -122.741 37.408, -122.806 37.377, -122.863 37.346, -122.908 37.315, -122.941 37.283, -122.963 37.252, -122.973 37.220) -------------------------------------------------------------------------------- /mogeo/test/fixtures/geojson/multi_point/multi_point.geojson: -------------------------------------------------------------------------------- 1 | {"type":"MultiPoint","coordinates":[[-117.2,32.7],[-115.8,33.9],[-114.6,35.1],[-116.5,36.2],[-118.4,34.3],[-119.8,35.6],[-121.7,37.3],[-120.1,38.1],[-118.7,39.5],[-117.4,40.2]]} 2 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/geojson/multi_point/multi_point_z.geojson: -------------------------------------------------------------------------------- 1 | {"type":"MultiPoint","coordinates":[[-117.2,32.7,853],[-115.8,33.9,412],[-114.6,35.1,224],[-116.5,36.2,731],[-118.4,34.3,66],[-119.8,35.6,924],[-121.7,37.3,102],[-120.1,38.1,569],[-118.7,39.5,888],[-117.4,40.2,632]]} 2 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: mogeo 2 | channels: 3 | - defaults 4 | dependencies: 5 | - python=3.11 6 | - ipykernel 7 | - matplotlib 8 | - numpy 9 | - orjson 10 | - pip 11 | - pytest 12 | - shapely 13 | - pip: 14 | - geoarrow-pyarrow 15 | - geoarrow-pandas 16 | - git+https://github.com/guidorice/mojo-pytest.git@v0.6.0 17 | -------------------------------------------------------------------------------- /scripts/setup-mojo-conda-env-macos.sh: -------------------------------------------------------------------------------- 1 | # Setup conda virtualenv (mac) 2 | 3 | # create conda environment named `venv` in pwd 4 | conda env create -y -p venv --file environment.yml 5 | 6 | # activate conda environment 7 | conda activate ./venv 8 | 9 | # export env var for mojo to use the correct libpython 10 | export MOJO_PYTHON_LIBRARY="$(pwd)/venv/lib/libpython3.11.dylib" 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: package test format clean install-py-packages 2 | 3 | install-py-packages: 4 | conda env create -p venv --file environment.yml 5 | 6 | clean: 7 | rm -rf ~/.modular/.mojo_cache build/mogeo.mojopkg 8 | 9 | test: 10 | pytest -W error 11 | 12 | format: 13 | mojo format . 14 | 15 | package: 16 | mkdir -p build/ 17 | mojo package mogeo/ -o build/mogeo.mojopkg 18 | -------------------------------------------------------------------------------- /mogeo/serialization/json.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | 4 | 5 | struct JSONParser: 6 | @staticmethod 7 | fn parse(json_str: String) raises -> PythonObject: 8 | """ 9 | Wraps json parser implementation. 10 | """ 11 | let orjson = Python.import_module("orjson") 12 | return orjson.loads(json_str) 13 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/geojson/line_string/curved.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "LineString", 3 | "coordinates": [ 4 | [-122.500, 37.500], 5 | [-122.588, 37.469], 6 | [-122.669, 37.438], 7 | [-122.741, 37.408], 8 | [-122.806, 37.377], 9 | [-122.863, 37.346], 10 | [-122.908, 37.315], 11 | [-122.941, 37.283], 12 | [-122.963, 37.252], 13 | [-122.973, 37.220] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/geojson/line_string/straight.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "LineString", 3 | "coordinates": [ 4 | [-122.456, 37.789], 5 | [-122.457, 37.790], 6 | [-122.458, 37.791], 7 | [-122.459, 37.792], 8 | [-122.460, 37.793], 9 | [-122.461, 37.794], 10 | [-122.462, 37.795], 11 | [-122.463, 37.796], 12 | [-122.464, 37.797], 13 | [-122.465, 37.798] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /mogeo/test/fixtures/geojson/line_string/zigzag.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "LineString", 3 | "coordinates": [ 4 | [-122.231, 37.002], 5 | [-122.321, 37.098], 6 | [-122.543, 37.234], 7 | [-122.654, 37.543], 8 | [-122.765, 37.664], 9 | [-122.876, 37.789], 10 | [-122.987, 37.876], 11 | [-122.765, 37.951], 12 | [-122.456, 38.022], 13 | [-122.123, 38.107] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /mogeo/serialization/wkt.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | 4 | 5 | struct WKTParser: 6 | @staticmethod 7 | fn parse(wkt: String) raises -> PythonObject: 8 | """ 9 | Wraps shapely.from_wkt to convert WKT string to a Shapely object. 10 | """ 11 | let shapely = Python.import_module("shapely") 12 | return shapely.from_wkt(wkt) 13 | -------------------------------------------------------------------------------- /mogeo/test/helpers.mojo: -------------------------------------------------------------------------------- 1 | import testing 2 | from pathlib import Path 3 | from python import Python 4 | 5 | 6 | fn load_geoarrow_test_fixture(path: Path) raises -> PythonObject: 7 | """ 8 | Reads the geoarrow test data fixture at path. 9 | 10 | Returns 11 | ------- 12 | table : pyarrow.Table 13 | The contents of the Feather file as a pyarrow.Table 14 | """ 15 | let feather = Python.import_module("pyarrow.feather") 16 | let table = feather.read_table(PythonObject(path.__str__())) 17 | return table 18 | -------------------------------------------------------------------------------- /mogeo/test/pytest.mojo: -------------------------------------------------------------------------------- 1 | import testing 2 | 3 | 4 | @value 5 | struct MojoTest: 6 | """ 7 | A utility struct for testing. 8 | """ 9 | 10 | var test_name: String 11 | 12 | fn __init__(inout self, test_name: String): 13 | self.test_name = test_name 14 | print("# " + test_name) 15 | 16 | fn assert_true(self, cond: Bool, message: String): 17 | """ 18 | Wraps testing.assert_true. 19 | """ 20 | try: 21 | testing.assert_true(cond, message) 22 | except e: 23 | print(e) 24 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_empty.mojo: -------------------------------------------------------------------------------- 1 | from mogeo.geom.empty import empty_value, is_empty 2 | from mogeo.test.pytest import MojoTest 3 | 4 | 5 | fn main() raises: 6 | let test = MojoTest("empty_value") 7 | 8 | let empty_f64 = empty_value[DType.float64]() 9 | let empty_f32 = empty_value[DType.float32]() 10 | let empty_f16 = empty_value[DType.float16]() 11 | let empty_int = empty_value[DType.int32]() 12 | let empty_uint = empty_value[DType.uint32]() 13 | 14 | test.assert_true(is_empty(empty_f64), "empty_f64") 15 | test.assert_true(is_empty(empty_f32), "empty_f32") 16 | test.assert_true(is_empty(empty_f16), "empty_f16") 17 | test.assert_true(is_empty(empty_int), "empty_int") 18 | test.assert_true(is_empty(empty_uint), "empty_uint") 19 | 20 | test.assert_true(not is_empty[DType.float64, 1](42), "not empty") 21 | test.assert_true(not is_empty[DType.uint16, 1](42), "not empty") 22 | -------------------------------------------------------------------------------- /mogeo/geom/empty.mojo: -------------------------------------------------------------------------------- 1 | from math import nan, isnan 2 | from math.limit import max_finite 3 | 4 | 5 | @always_inline 6 | fn empty_value[dtype: DType]() -> SIMD[dtype, 1]: 7 | """ 8 | Define a special value to mark empty slots or dimensions in structs. Required because SIMD must be power of two. 9 | """ 10 | 11 | @parameter 12 | if dtype.is_floating_point(): 13 | return nan[dtype]() 14 | else: 15 | return max_finite[dtype]() 16 | 17 | 18 | @always_inline 19 | fn is_empty[dtype: DType, simd_width: Int](value: SIMD[dtype, simd_width]) -> Bool: 20 | """ 21 | Check for empty value. Note: NaN cannot be compared by equality. This helper function calls isnan() if the dtype 22 | is floating point. 23 | """ 24 | 25 | @parameter 26 | if dtype.is_floating_point(): 27 | return isnan[dtype, simd_width](value) 28 | else: 29 | return value == max_finite[dtype]() 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alex G Rice 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | pull_request: 7 | types: [opened, reopened, edited] 8 | 9 | push: 10 | branches: 11 | - 'main' 12 | 13 | jobs: 14 | 15 | tests: 16 | runs-on: ubuntu-latest 17 | 18 | defaults: 19 | run: 20 | shell: bash -el {0} 21 | 22 | steps: 23 | - name: Check out repository code 24 | uses: actions/checkout@v4 25 | with: 26 | submodules: "recursive" 27 | 28 | - name: "Setup conda env (base)" 29 | uses: conda-incubator/setup-miniconda@v2 30 | with: 31 | python-version: 3.11 32 | auto-activate-base: true 33 | 34 | - name: "Install mojo" 35 | run: | 36 | curl https://get.modular.com | sh - && \ 37 | modular auth ${{secrets.MODULAR_AUTH}} && \ 38 | modular install --install-version 0.6.0 mojo 39 | 40 | - name: "Setup conda env (mogeo)" 41 | uses: conda-incubator/setup-miniconda@v2 42 | with: 43 | python-version: 3.11 44 | activate-environment: mogeo 45 | environment-file: environment.yml 46 | 47 | - name: "Run mojo-pytest" 48 | run: | 49 | export MODULAR_HOME="/home/runner/.modular" 50 | export PATH="/home/runner/.modular/pkg/packages.modular.com_mojo/bin:$PATH" 51 | export MOJO_PYTHON_LIBRARY="$(find $CONDA_PREFIX/lib -iname 'libpython*.[s,d]*' | sort -r | head -n 1)" 52 | pytest -W error 53 | 54 | 55 | -------------------------------------------------------------------------------- /mogeo/geom/traits.mojo: -------------------------------------------------------------------------------- 1 | from .enums import CoordDims 2 | from .envelope import Envelope 3 | 4 | 5 | trait Dimensionable: 6 | fn dims(self) -> Int: 7 | ... 8 | 9 | fn has_height(self) -> Bool: 10 | ... 11 | 12 | fn has_measure(self) -> Bool: 13 | ... 14 | 15 | fn set_ogc_dims(inout self, ogc_dims: CoordDims): 16 | """ 17 | Setter for ogc_dims enum. May be useful if the Point constructor with variadic list of coordinate values. 18 | (Point Z vs Point M is ambiguous). 19 | """ 20 | ... 21 | 22 | 23 | trait Emptyable: 24 | @staticmethod 25 | fn empty(dims: CoordDims = CoordDims.Point) -> Self: 26 | ... 27 | 28 | fn is_empty(self) -> Bool: 29 | ... 30 | 31 | 32 | trait Geometric(Dimensionable): 33 | ... 34 | # TODO: Geometric trait seems to require parameter support on Traits (TBD mojo version?) 35 | 36 | # fn envelope(self) -> Envelope[dtype]: 37 | # fn contains(self, other: Self) -> Bool 38 | # fn contains(self, other: Self) -> Bool 39 | # fn intersects(self, other: Self) -> Bool 40 | # fn overlaps(self, other: Self) -> Bool 41 | # fn disjoint(self, other: Self) -> Bool 42 | # fn touches(self, other: Self) -> Bool 43 | # fn intersection(self, other: Self) -> Self 44 | # fn union(self, other: Self) -> Self 45 | # fn difference(self, other: Self) -> Self 46 | # fn buffer(self, size: SIMD[dtype, 1]) -> Self 47 | # fn convex_hull(self) -> Polygon[dtype] 48 | # fn simplify(self) -> Self 49 | # fn centroid(self) -> SIMD[dtype, 1] 50 | # fn area(self) -> SIMD[dtype, 1] 51 | # fn length(self) -> SIMD[dtype, 1] 52 | # fn translate(self, SIMD[dtype, simd_dims]) -> Self 53 | # fn rotate(self, degrees: SIMD[dtype, 1]) -> Self 54 | -------------------------------------------------------------------------------- /mogeo/serialization/traits.mojo: -------------------------------------------------------------------------------- 1 | trait WKTable: 2 | """ 3 | Serializable to and from Well Known Text (WKT). 4 | 5 | ### Specs 6 | 7 | - https://libgeos.org/specifications/wkt 8 | - https://www.ogc.org/standard/sfa/ 9 | - https://www.ogc.org/standard/sfs/ 10 | """ 11 | 12 | @staticmethod 13 | fn from_wkt(wkt: String) raises -> Self: 14 | ... 15 | 16 | fn wkt(self) -> String: 17 | ... 18 | 19 | 20 | trait JSONable: 21 | """ 22 | Serializable to and from GeoJSON representation of Point. Point coordinates are in x, y order (easting, northing for 23 | projected coordinates, longitude, and latitude for geographic coordinates). 24 | 25 | ### Specs 26 | 27 | - https://geojson.org 28 | - https://datatracker.ietf.org/doc/html/rfc7946 29 | """ 30 | 31 | @staticmethod 32 | fn from_json(json: PythonObject) raises -> Self: 33 | ... 34 | 35 | @staticmethod 36 | fn from_json(json_str: String) raises -> Self: 37 | ... 38 | 39 | fn json(self) raises -> String: 40 | """ 41 | Serialize to GeoJSON format. 42 | 43 | ### Raises Error 44 | 45 | Error is raised for PointM and PointZM, because measure and other higher dimensions are not part of the GeoJSON 46 | spec. 47 | 48 | > An OPTIONAL third-position element SHALL be the height in meters above or below the WGS 84 reference 49 | > ellipsoid. (RFC 7946) 50 | """ 51 | ... 52 | 53 | 54 | trait Geoarrowable: 55 | """ 56 | Serializable to and from GeoArrow representation of a Point. 57 | 58 | ### Spec 59 | 60 | - https://geoarrow.org/ 61 | """ 62 | 63 | @staticmethod 64 | fn from_geoarrow(table: PythonObject) raises -> Self: 65 | """ 66 | Create Point from geoarrow / pyarrow table with geometry column. 67 | """ 68 | ... 69 | 70 | # TODO: to geoarrow 71 | # fn geoarrow(self) -> PythonObject: 72 | # ... 73 | -------------------------------------------------------------------------------- /mogeo/geom/enums.mojo: -------------------------------------------------------------------------------- 1 | @value 2 | @register_passable("trivial") 3 | struct CoordDims(Stringable, Sized): 4 | """ 5 | Enum for encoding the OGC/WKT variants of Points. 6 | """ 7 | 8 | # TODO: use a real enum here, when mojo supports. 9 | 10 | var value: SIMD[DType.uint8, 1] 11 | 12 | alias Point = CoordDims(100) 13 | """ 14 | 2 dimensional Point. 15 | """ 16 | alias PointZ = CoordDims(101) 17 | """ 18 | 3 dimensional Point, has height or altitude (Z). 19 | """ 20 | alias PointM = CoordDims(102) 21 | """ 22 | 3 dimensional Point, has measure (M). 23 | """ 24 | alias PointZM = CoordDims(103) 25 | """ 26 | 4 dimensional Point, has height and measure (ZM) 27 | """ 28 | 29 | alias PointND = CoordDims(104) 30 | """ 31 | N-dimensional Point, number of dimensions from constructor. 32 | """ 33 | 34 | fn __eq__(self, other: Self) -> Bool: 35 | return self.value == other.value 36 | 37 | fn __ne__(self, other: Self) -> Bool: 38 | return not self.__eq__(other) 39 | 40 | fn __str__(self) -> String: 41 | """ 42 | Convert to string, using WKT point variants. 43 | """ 44 | if self == CoordDims.Point: 45 | return "Point" 46 | elif self == CoordDims.PointZ: 47 | return "Point Z" 48 | elif self == CoordDims.PointM: 49 | return "Point M" 50 | elif self == CoordDims.PointZM: 51 | return "Point ZM" 52 | else: 53 | return "Point ND" 54 | 55 | fn __len__(self) -> Int: 56 | if self == CoordDims.Point: 57 | return 2 58 | elif self == CoordDims.PointM or self == CoordDims.PointZ: 59 | return 3 60 | elif self == CoordDims.PointZM: 61 | return 4 62 | else: 63 | return self.value.to_int() 64 | 65 | fn has_height(self) -> Bool: 66 | return (self == CoordDims.PointZ) or (self == CoordDims.PointZM) 67 | 68 | fn has_measure(self) -> Bool: 69 | return (self == CoordDims.PointM) or (self == CoordDims.PointZM) 70 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_layout.mojo: -------------------------------------------------------------------------------- 1 | from tensor import Tensor, TensorSpec, TensorShape 2 | from utils.index import Index 3 | 4 | from mogeo.test.pytest import MojoTest 5 | from mogeo.geom.layout import Layout 6 | from mogeo.test.constants import lat, lon, height, measure 7 | from mogeo.geom.enums import CoordDims 8 | 9 | 10 | fn main() raises: 11 | test_constructors() 12 | test_equality_ops() 13 | test_len() 14 | test_dims() 15 | 16 | 17 | fn test_constructors() raises: 18 | let test = MojoTest("constructors") 19 | 20 | var n = 10 21 | 22 | # 2x10 (default of 2 dims) 23 | let layout_a = Layout(coords_size=n) 24 | var shape = layout_a.coordinates.shape() 25 | test.assert_true(shape[0] == 2, "2x10 constructor") 26 | test.assert_true(shape[1] == n, "2x10 constructor") 27 | 28 | # 3x15 29 | n = 15 30 | let layout_b = Layout(ogc_dims=CoordDims.PointZ, coords_size=n) 31 | shape = layout_b.coordinates.shape() 32 | test.assert_true(shape[0] == 3, "3x15 constructor") 33 | test.assert_true(shape[1] == n, "3x15 constructor") 34 | 35 | # 4x20 36 | n = 20 37 | let layout_c = Layout(ogc_dims=CoordDims.PointZM, coords_size=n) 38 | shape = layout_c.coordinates.shape() 39 | test.assert_true(shape[0] == 4, "4x20 constructor") 40 | test.assert_true(shape[1] == n, "4x20 constructor") 41 | 42 | 43 | fn test_equality_ops() raises: 44 | let test = MojoTest("equality ops") 45 | 46 | let n = 20 47 | var ga2 = Layout(coords_size=n, geoms_size=0, parts_size=0, rings_size=0) 48 | var ga2b = Layout(coords_size=n, geoms_size=0, parts_size=0, rings_size=0) 49 | for dim in range(2): 50 | for coord in range(n): 51 | let idx = Index(dim, coord) 52 | ga2.coordinates[idx] = 42.0 53 | ga2b.coordinates[idx] = 42.0 54 | test.assert_true(ga2 == ga2b, "__eq__") 55 | 56 | ga2.coordinates[Index(0, n - 1)] = 3.14 57 | test.assert_true(ga2 != ga2b, "__ne__") 58 | 59 | 60 | fn test_len() raises: 61 | let test = MojoTest("__len__") 62 | 63 | let n = 50 64 | let l = Layout(coords_size=n) 65 | test.assert_true(len(l) == 50, "__len__") 66 | 67 | 68 | fn test_dims() raises: 69 | let test = MojoTest("dims") 70 | let l = Layout(coords_size=10) 71 | let expect_dims = len(CoordDims.Point) 72 | test.assert_true(l.dims() == expect_dims, "dims") 73 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_enums_coorddims.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | from pathlib import Path 4 | 5 | from mogeo.geom.empty import empty_value, is_empty 6 | from mogeo.test.pytest import MojoTest 7 | from mogeo.geom.enums import CoordDims 8 | 9 | 10 | fn main() raises: 11 | test_coord_dims() 12 | 13 | 14 | fn test_coord_dims() raises: 15 | test_constructors() 16 | test_str() 17 | test_eq() 18 | test_getters() 19 | test_len() 20 | 21 | 22 | fn test_constructors(): 23 | let test = MojoTest("constructors") 24 | _ = CoordDims(42) 25 | 26 | 27 | fn test_len(): 28 | let test = MojoTest("len") 29 | 30 | let n = 42 31 | let pt = CoordDims(n) 32 | test.assert_true(len(pt) == n, "dims()") 33 | 34 | 35 | fn test_getters(): 36 | let test = MojoTest("getters") 37 | let pt = CoordDims.Point 38 | test.assert_true(not pt.has_height(), "has_height") 39 | test.assert_true(not pt.has_measure(), "has_measure") 40 | 41 | let pt_z = CoordDims.PointZ 42 | test.assert_true(pt_z.has_height(), "has_height") 43 | test.assert_true(not pt_z.has_measure(), "has_measure") 44 | 45 | let pt_m = CoordDims.PointM 46 | test.assert_true(pt_m.has_measure(), "has_height") 47 | test.assert_true(not pt_m.has_height(), "has_measure") 48 | 49 | let pt_zm = CoordDims.PointZM 50 | test.assert_true(pt_zm.has_measure(), "has_height") 51 | test.assert_true(pt_zm.has_height(), "has_measure") 52 | 53 | 54 | fn test_str() raises: 55 | let test = MojoTest("__str__") 56 | 57 | let pt = CoordDims.Point 58 | test.assert_true(str(pt) == "Point", "__str__") 59 | 60 | let pt_z = CoordDims.PointZ 61 | test.assert_true(str(pt_z) == "Point Z", "__str__") 62 | 63 | let pt_m = CoordDims.PointM 64 | test.assert_true(str(pt_m) == "Point M", "__str__") 65 | 66 | let pt_zm = CoordDims.PointZM 67 | test.assert_true(str(pt_zm) == "Point ZM", "__str__") 68 | 69 | let pt_nd = CoordDims.PointND 70 | test.assert_true(str(pt_nd) == "Point ND", "__str__") 71 | 72 | 73 | fn test_eq() raises: 74 | let test = MojoTest("__eq__, __ne__") 75 | 76 | let pt = CoordDims.Point 77 | let pt_z = CoordDims.PointZ 78 | test.assert_true(pt != pt_z, "__ne__") 79 | 80 | let n = 42 81 | let pt_nd_a = CoordDims(n) 82 | let pt_nd_b = CoordDims(n) 83 | test.assert_true(pt_nd_a == pt_nd_b, "__eq__") 84 | -------------------------------------------------------------------------------- /mogeo/geom/layout.mojo: -------------------------------------------------------------------------------- 1 | from math.limit import max_finite 2 | from tensor import Tensor 3 | 4 | from .traits import Dimensionable 5 | from .enums import CoordDims 6 | 7 | 8 | @value 9 | struct Layout[dtype: DType = DType.float64, offset_dtype: DType = DType.uint32]( 10 | Sized, Dimensionable 11 | ): 12 | """ 13 | Memory layout inspired by, but not exactly following, the GeoArrow format. 14 | 15 | ### Spec 16 | 17 | https://geoarrow.org 18 | """ 19 | 20 | alias dimensions_idx = 0 21 | alias features_idx = 1 22 | 23 | var coordinates: Tensor[dtype] 24 | var geometry_offsets: Tensor[offset_dtype] 25 | var part_offsets: Tensor[offset_dtype] 26 | var ring_offsets: Tensor[offset_dtype] 27 | var ogc_dims: CoordDims 28 | 29 | fn __init__( 30 | inout self, 31 | ogc_dims: CoordDims = CoordDims.Point, 32 | coords_size: Int = 0, 33 | geoms_size: Int = 0, 34 | parts_size: Int = 0, 35 | rings_size: Int = 0, 36 | ): 37 | """ 38 | Create column-oriented tensor: rows (dims) x cols (coords), plus offsets vectors. 39 | """ 40 | if max_finite[offset_dtype]() < coords_size: 41 | print( 42 | "Warning: offset_dtype parameter not large enough for coords_size arg.", 43 | offset_dtype, 44 | coords_size, 45 | ) 46 | self.ogc_dims = ogc_dims 47 | self.coordinates = Tensor[dtype](len(ogc_dims), coords_size) 48 | self.geometry_offsets = Tensor[offset_dtype](geoms_size) 49 | self.part_offsets = Tensor[offset_dtype](parts_size) 50 | self.ring_offsets = Tensor[offset_dtype](rings_size) 51 | 52 | fn __eq__(self, other: Self) -> Bool: 53 | """ 54 | Check equality of coordinates and offsets vs other. 55 | """ 56 | if ( 57 | self.coordinates == other.coordinates 58 | and self.geometry_offsets == other.geometry_offsets 59 | and self.part_offsets == other.part_offsets 60 | and self.ring_offsets == other.ring_offsets 61 | ): 62 | return True 63 | return False 64 | 65 | fn __ne__(self, other: Self) -> Bool: 66 | """ 67 | Check in-equality of coordinates and offsets vs other. 68 | """ 69 | return not self == other 70 | 71 | fn __len__(self) -> Int: 72 | """ 73 | Length is the number of coordinates (constructor's `coords_size` argument) 74 | """ 75 | return self.coordinates.shape()[self.features_idx] 76 | 77 | fn dims(self) -> Int: 78 | """ 79 | Num dimensions (X, Y, Z, M, etc). (constructor's `dims` argument). 80 | """ 81 | return self.coordinates.shape()[self.dimensions_idx] 82 | 83 | fn has_height(self) -> Bool: 84 | return self.ogc_dims == CoordDims.PointZ or self.ogc_dims == CoordDims.PointZM 85 | 86 | fn has_measure(self) -> Bool: 87 | return self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZM 88 | 89 | fn set_ogc_dims(inout self, ogc_dims: CoordDims): 90 | """ 91 | Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. 92 | (ex: when Point Z vs Point M is ambiguous. 93 | """ 94 | debug_assert( 95 | len(self.ogc_dims) == len(ogc_dims), "Unsafe change of dimension number" 96 | ) 97 | self.ogc_dims = ogc_dims 98 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode/ 3 | scratch/ 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # poetry 102 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 103 | # This is especially recommended for binary packages to ensure reproducibility, and is more 104 | # commonly ignored for libraries. 105 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 106 | #poetry.lock 107 | 108 | # pdm 109 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 110 | #pdm.lock 111 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 112 | # in version control. 113 | # https://pdm.fming.dev/#use-with-ide 114 | .pdm.toml 115 | 116 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 117 | __pypackages__/ 118 | 119 | # Celery stuff 120 | celerybeat-schedule 121 | celerybeat.pid 122 | 123 | # SageMath parsed files 124 | *.sage.py 125 | 126 | # Environments 127 | .env 128 | .venv 129 | env/ 130 | venv/ 131 | ENV/ 132 | env.bak/ 133 | venv.bak/ 134 | 135 | # Spyder project settings 136 | .spyderproject 137 | .spyproject 138 | 139 | # Rope project settings 140 | .ropeproject 141 | 142 | # mkdocs documentation 143 | /site 144 | 145 | # mypy 146 | .mypy_cache/ 147 | .dmypy.json 148 | dmypy.json 149 | 150 | # Pyre type checker 151 | .pyre/ 152 | 153 | # pytype static type analyzer 154 | .pytype/ 155 | 156 | # Cython debug symbols 157 | cython_debug/ 158 | 159 | # PyCharm 160 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 161 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 162 | # and can be added to the global gitignore or merged into this file. For a more nuclear 163 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 164 | #.idea/ 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MoGeo: Mojo geographic and geometric vector features 2 | 3 | [![Run Tests](https://github.com/guidorice/mogeo/actions/workflows/tests.yaml/badge.svg)](https://github.com/guidorice/mogeo/actions/workflows/tests.yaml) 4 | 5 | [Mojo🔥](https://github.com/modularml/mojo) package for geographic and geometric 6 | vector features and analytics, such as location data or earth observation data. 7 | 8 | ## status 2024-01-05 9 | 10 | | :construction: pre-alpha, not yet usable! | 11 | |-------------------------------------------| 12 | 13 | In 2023, this package (formerly `geo-features`) served for learning Mojo 0.x 14 | and to experiment with the memory layout advocated by 15 | [GeoArrow](https://geoarrow.org). GeoArrow seems quite promising, but is 16 | already well served by it's C, Rust and Python implementations. In Mojo, there 17 | is not yet support for zero-copy shared memory buffers, so I created a [feature 18 | request for the python buffer 19 | protocol](https://github.com/modularml/mojo/issues/1515). Until that is 20 | supported, I not see any practical use for a GeoArrow implementation in pure 21 | Mojo. 22 | 23 | In 2024 and beyond I'm exploring an alternative backend and memory layout using 24 | dual quaternions. Dual quaternions can represent rotations and translations, 25 | and should be useful in solving the [antimeridian crossing 26 | problem](https://macwright.com/2016/09/26/the-180th-meridian.html). Dual 27 | quaternions have been successfully used in robotics, physics simulations, game 28 | dev, and graphics. To this end I added "Be useful for many application 29 | domains..." to the [project goals](#project-goals). 30 | 31 | ## project goals 32 | 33 | - Apply Mojo's systems programming features to create a native geo package with strong 34 | type safety and high performance. 35 | - Be useful for many application domains, not only Geographic Information 36 | Systems (GIS). Additionally: planetary information systems, oceanography, 37 | robotics, gamedev, graphics, embedded systems. 38 | - Promote [cloud native geospatial](https://cloudnativegeo.org/) computing and 39 | open geospatial standards. 40 | - Leverage the vast Python ecosystem, wherever possible, to enable rapid 41 | development and development and interoperability. 42 | 43 | ## requirements 44 | 45 | - [Mojo](https://github.com/modularml/mojo) >= 0.6 46 | - [Python](https://www.python.org/) >= 3.9 47 | - [Conda](https://docs.conda.io/en/latest/) 48 | 49 | ## roadmap 50 | 51 | ### core structs 52 | 53 | - [ ] Envelope 54 | - [ ] Feature 55 | - [ ] FeatureCollection 56 | - [ ] GeometryCollection 57 | - [ ] LinearRing 58 | - [ ] LineString 59 | - [ ] Memory Layout 60 | - [ ] MultiLineString 61 | - [ ] MultiPoint 62 | - [ ] MultiPolygon 63 | - [ ] Point 64 | - [ ] Polygon 65 | 66 | ### serialization and interchange formats 67 | 68 | - [ ] GeoArrow 69 | - [ ] GeoJSON 70 | - [ ] GeoParquet 71 | - [ ] TopoJSON 72 | - [ ] WKT 73 | 74 | ### methods and algorithms 75 | 76 | - [ ] area 77 | - [ ] perimeter 78 | - [ ] centroid 79 | - [ ] intersection 80 | - [ ] union 81 | - [ ] difference 82 | - [ ] parallelized+vectorized spatial join 83 | - [ ] rasterize from vector 84 | - [ ] vectorize from raster 85 | - [ ] re-projection and CRS support 86 | - [ ] simplify or decimate 87 | - [ ] stratified sampling? 88 | - [ ] zonal stats? 89 | - [ ] smart antimeridian crossing mode (quaternions?) 90 | 91 | ## architectural decisions 92 | 93 | - ~~Implement a columnar memory layout similar to [GeoArrow](https://geoarrow.org/), for 94 | efficient representation of coordinates, features and attributes.~~ See [status 2024-01-05](#status-2024-01-05). 95 | 96 | ## related software 97 | 98 | - [GEOS](https://libgeos.org/) - Geometry Engine, Open Source. 99 | - [GDAL/OGR](https://gdal.org) - Geospatial Data Abstraction Library. 100 | - [Shapely](https://shapely.readthedocs.io) - Python package for computational geometry. 101 | - [JTS Topology Suite](https://github.com/locationtech/jts) - Java library for creating and manipulating vector geometry. 102 | - [TG](https://github.com/tidwall/tg) - Geometry library for C that is small, fast, and easy to use. 103 | - [Turf.js](https://turfjs.org) - Advanced geospatial analysis for browsers and Node.js. 104 | - [TurfPy](https://turfpy.readthedocs.io/en/latest/) - Python library for performing geospatial data analysis which reimplements turf.js. 105 | 106 | ## specs 107 | 108 | - [ISO/OGC Simple Features](https://en.wikipedia.org/wiki/Simple_Features) - Set of standards that specify a common 109 | storage and access model of geographic features. 110 | - [GeoJSON](https://geojson.org) - Geospatial data interchange format based on JavaScript Object Notation (JSON). 111 | - [GeoArrow](https://geoarrow.org) - Specification for storing geospatial data in Apache Arrow and Arrow-compatible data 112 | structures and formats. 113 | - [GeoParquet](https://geoparquet.org) - Specification for storing geospatial vector data (point, line, polygon) in 114 | Parquet. 115 | 116 | ## setup dev environment 117 | 118 | 1. Clone this repo, including submodules: 119 | 120 | ```shell 121 | git clone --recurse-submodules https://github.com/guidorice/mogeo 122 | ``` 123 | 124 | 2. Create a Python environment using [environment.yml](./environment.yml). This is required for supporting packages used 125 | by `mogeo`, for example for interchange, serialization, and unit testing. 126 | [Conda](https://docs.conda.io/projects/miniconda/en/latest/) is recommended because it puts a copy of libpython into 127 | each conda env. 128 | 129 | ```text 130 | conda env create -n mogeo --file environment.yml 131 | ``` 132 | 133 | 3. Set `MOJO_PYTHON_LIBRARY` environment variable to your libpython. An example of doing 134 | this on MacOS is the [scripts](./scripts/setup-mojo-conda-env-macos.sh) 135 | directory. Help: [Using Mojo with Python](https://www.modular.com/blog/using-mojo-with-python) . 136 | 137 | 4. Run targets in [Makefile](./Makefile), ex: `make test`, `make package`, `make format`. 138 | 139 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_multi_point.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | from utils.vector import DynamicVector 4 | from utils.index import Index 5 | from pathlib import Path 6 | 7 | from mogeo.test.constants import lat, lon, height, measure 8 | from mogeo.test.pytest import MojoTest 9 | from mogeo.geom.point import Point, Point64 10 | from mogeo.geom.multi_point import MultiPoint 11 | from mogeo.serialization.json import JSONParser 12 | from mogeo.geom.enums import CoordDims 13 | 14 | 15 | fn main() raises: 16 | test_multi_point() 17 | 18 | 19 | fn test_multi_point() raises: 20 | test_constructors() 21 | test_mem_layout() 22 | test_get_item() 23 | test_equality_ops() 24 | test_is_empty() 25 | test_repr() 26 | test_stringable() 27 | test_wktable() 28 | test_jsonable() 29 | 30 | 31 | fn test_constructors() raises: 32 | var test = MojoTest("variadic list constructor") 33 | 34 | let mpt = MultiPoint(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 35 | test.assert_true(mpt[0] == Point(lon, lat), "variadic list constructor") 36 | test.assert_true(mpt[1] == Point(lon, lat), "variadic list constructor") 37 | test.assert_true(mpt[2] == Point(lon, lat + 1), "variadic list constructor") 38 | test.assert_true(len(mpt) == 3, "variadic list constructor") 39 | 40 | test = MojoTest("vector constructor") 41 | 42 | var points_vec = DynamicVector[Point64](10) 43 | for n in range(10): 44 | points_vec.push_back(Point(lon + n, lat - n)) 45 | _ = MultiPoint(points_vec) 46 | 47 | 48 | fn test_mem_layout() raises: 49 | """ 50 | Test if MultiPoint fills the Layout struct correctly. 51 | """ 52 | let test = MojoTest("mem layout") 53 | 54 | # equality check each point by indexing into the MultiPoint. 55 | var points_vec = DynamicVector[Point64](10) 56 | for n in range(10): 57 | points_vec.push_back(Point(lon + n, lat - n)) 58 | let mpt2 = MultiPoint(points_vec) 59 | for n in range(10): 60 | let expect_pt = Point(lon + n, lat - n) 61 | test.assert_true(mpt2[n] == expect_pt, "test_mem_layout") 62 | 63 | let layout = mpt2.data 64 | 65 | # offsets fields are empty in MultiPoint because of using geo_arrows "struct coordinate representation" 66 | test.assert_true( 67 | layout.geometry_offsets.num_elements() == 0, "geo_arrow geometry_offsets" 68 | ) 69 | test.assert_true(layout.part_offsets.num_elements() == 0, "geo_arrow part_offsets") 70 | test.assert_true(layout.ring_offsets.num_elements() == 0, "geo_arrow ring_offsets") 71 | 72 | 73 | fn test_get_item() raises: 74 | let test = MojoTest("get_item") 75 | var points_vec = DynamicVector[Point64](10) 76 | for n in range(10): 77 | points_vec.push_back(Point(lon + n, lat - n)) 78 | let mpt = MultiPoint(points_vec) 79 | for n in range(10): 80 | let expect_pt = Point(lon + n, lat - n) 81 | let got_pt = mpt[n] 82 | test.assert_true(got_pt == expect_pt, "get_item") 83 | 84 | 85 | fn test_equality_ops() raises: 86 | let test = MojoTest("equality operators") 87 | 88 | # partial simd_load (n - i < nelts) 89 | let mpt1 = MultiPoint( 90 | Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8), Point(9, 10) 91 | ) 92 | let mpt2 = MultiPoint( 93 | Point(1.1, 2.1), 94 | Point(3.1, 4.1), 95 | Point(5.1, 6.1), 96 | Point(7.1, 8.1), 97 | Point(9.1, 10.1), 98 | ) 99 | test.assert_true(mpt1 != mpt2, "partial simd_load (n - i < nelts)") 100 | 101 | # partial simd_load (n - i < nelts) 102 | alias Point2F32 = Point[DType.float32] 103 | let mpt5 = MultiPoint( 104 | Point2F32(1, 2), 105 | Point2F32(5, 6), 106 | Point2F32(10, 11), 107 | ) 108 | let mpt6 = MultiPoint( 109 | Point2F32(1, 2), 110 | Point2F32(5, 6), 111 | Point2F32(10, 11.1), 112 | ) 113 | test.assert_true(mpt5 != mpt6, "partial simd_load (n - i < nelts) (b)") 114 | 115 | alias Point2F16 = Point[DType.float16] 116 | let mpt7 = MultiPoint( 117 | Point2F16(1, 2), 118 | Point2F16(5, 6), 119 | Point2F16(10, 11), 120 | ) 121 | let mpt8 = MultiPoint( 122 | Point2F16(1, 2), 123 | Point2F16(5, 6), 124 | Point2F16(10, 11.1), 125 | ) 126 | test.assert_true(mpt7 != mpt8, "__ne__") 127 | 128 | var points_vec2 = DynamicVector[Point64](10) 129 | for n in range(10): 130 | points_vec2.push_back(Point(lon + n, lat - n)) 131 | let mpt9 = MultiPoint(points_vec2) 132 | let mpt10 = MultiPoint(points_vec2) 133 | test.assert_true(mpt9 == mpt10, "__eq__") 134 | test.assert_true(mpt9 != mpt2, "__ne__") 135 | 136 | let mpt11 = MultiPoint(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 137 | let mpt12 = MultiPoint(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 138 | test.assert_true(mpt11 == mpt12, "__eq__") 139 | test.assert_true(mpt9 != mpt12, "__ne__") 140 | 141 | 142 | fn test_is_empty() raises: 143 | let test = MojoTest("is_empty") 144 | let empty_mpt = MultiPoint() 145 | test.assert_true(empty_mpt.is_empty() == True, "is_empty()") 146 | 147 | 148 | fn test_repr() raises: 149 | let test = MojoTest("__repr__") 150 | let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) 151 | let s = mpt.__repr__() 152 | test.assert_true(s == "MultiPoint [Point, float64](2 points)", "__repr__") 153 | 154 | 155 | fn test_stringable() raises: 156 | let test = MojoTest("__str__") 157 | let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) 158 | test.assert_true(mpt.__str__() == mpt.__repr__(), "__str__") 159 | 160 | 161 | fn test_wktable() raises: 162 | let test = MojoTest("wktable") 163 | let path = Path("mogeo/test/fixtures/wkt/multi_point") 164 | let fixtures = VariadicList("point.wkt", "point_z.wkt") 165 | for i in range(len(fixtures)): 166 | let file = path / fixtures[i] 167 | with open(file.path, "r") as f: 168 | let wkt = f.read() 169 | let mp = MultiPoint.from_wkt(wkt) 170 | test.assert_true( 171 | mp.wkt() != "FIXME", "wkt" 172 | ) # FIXME: no number formatting so cannot compare wkt strings. 173 | 174 | 175 | fn test_jsonable() raises: 176 | test_json() 177 | test_from_json() 178 | 179 | 180 | fn test_json() raises: 181 | let test = MojoTest("json") 182 | 183 | let mpt = MultiPoint(Point(lon, lat), Point(lon + 1, lat + 1)) 184 | test.assert_true( 185 | mpt.json() 186 | == '{"type":"MultiPoint","coordinates":[[-108.68000000000001,38.973999999999997],[-107.68000000000001,39.973999999999997]]}', 187 | "json", 188 | ) 189 | 190 | let mpt_z = MultiPoint(Point(lon, lat, height), Point(lon + 1, lat + 1, height - 1)) 191 | test.assert_true( 192 | mpt_z.json() 193 | == '{"type":"MultiPoint","coordinates":[[-108.68000000000001,38.973999999999997,8.0],[-107.68000000000001,39.973999999999997,7.0]]}', 194 | "json", 195 | ) 196 | 197 | let expect_error = "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" 198 | var mpt_m = MultiPoint( 199 | Point(lon, lat, measure), Point(lon + 1, lat + 1, measure - 1) 200 | ) 201 | mpt_m.set_ogc_dims(CoordDims.PointM) 202 | try: 203 | _ = mpt_m.json() 204 | except e: 205 | test.assert_true(str(e) == expect_error, "json raises") 206 | 207 | let mpt_zm = MultiPoint( 208 | Point(lon, lat, height, measure), 209 | Point(lon + 1, lat + 1, height * 2, measure - 1), 210 | ) 211 | try: 212 | _ = mpt_zm.json() 213 | except e: 214 | test.assert_true(str(e) == expect_error, "json raises") 215 | 216 | 217 | fn test_from_json() raises: 218 | pass 219 | # let test = MojoTest("from_json") 220 | 221 | # let path = Path("mogeo/test/fixtures/geojson/multi_point") 222 | # let fixtures = VariadicList("multi_point.geojson") # , "multi_point_z.geojson" 223 | # for i in range(len(fixtures)): 224 | # let file = path / fixtures[i] 225 | # with open(file.path, "r") as f: 226 | # let json_str = f.read() 227 | # _ = MultiPoint.from_json(json_str) 228 | # let json_dict = JSONParser.parse(json_str) 229 | # _ = MultiPoint.from_json(json_dict) 230 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_envelope.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | from utils.vector import DynamicVector 4 | from pathlib import Path 5 | from random import rand 6 | 7 | from mogeo.test.pytest import MojoTest 8 | from mogeo.test.constants import lon, lat, height, measure 9 | 10 | from mogeo.geom.envelope import Envelope 11 | from mogeo.geom.point import Point 12 | from mogeo.geom.enums import CoordDims 13 | from mogeo.geom.layout import Layout 14 | from mogeo.geom.traits import Geometric, Emptyable 15 | 16 | 17 | fn main() raises: 18 | test_envelope() 19 | 20 | 21 | fn test_envelope() raises: 22 | test_constructors() 23 | test_repr() 24 | test_min_max() 25 | test_southwesterly_point() 26 | test_northeasterly_point() 27 | test_with_geos() 28 | test_equality_ops() 29 | 30 | # test_wkt() 31 | # test_json() 32 | # test_from_json() 33 | # test_from_wkt() 34 | 35 | 36 | fn test_constructors() raises: 37 | let test = MojoTest("constructors, aliases") 38 | 39 | # from Point 40 | _ = Envelope(Point(lon, lat)) 41 | _ = Envelope(Point(lon, lat, height)) 42 | _ = Envelope(Point(lon, lat, measure)) 43 | _ = Envelope(Point(lon, lat, height, measure)) 44 | 45 | _ = Envelope(Point[DType.int8](lon, lat)) 46 | _ = Envelope(Point(lon, lat, height, measure)) 47 | 48 | # from LineString 49 | # alias Point2_f16 = Point[DType.float16] 50 | # _ = Envelope( 51 | # LineString( 52 | # Point2_f16(lon, lat), 53 | # Point2_f16(lon + 1, lat + 1), 54 | # Point2_f16(lon + 2, lat + 2), 55 | # Point2_f16(lon + 3, lat + 3), 56 | # Point2_f16(lon + 4, lat + 4), 57 | # Point2_f16(lon + 5, lat + 5), 58 | # ) 59 | # ) 60 | 61 | 62 | fn test_repr() raises: 63 | let test = MojoTest("repr") 64 | 65 | # TODO: more variations of envelope structs 66 | 67 | let e_pt2 = Envelope(Point(lon, lat)) 68 | test.assert_true( 69 | e_pt2.__repr__() 70 | == "Envelope [float64](-108.68000000000001, 38.973999999999997, nan, nan," 71 | " -108.68000000000001, 38.973999999999997, nan, nan)", 72 | "__repr__", 73 | ) 74 | 75 | # e = Envelope( 76 | # LineString(Point2(lon, lat), Point2(lon + 1, lat + 1), Point2(lon + 2, lat + 2)) 77 | # ) 78 | # test.assert_true( 79 | # e.__repr__() 80 | # == "Envelope[float64](-108.68000000000001, 38.973999999999997," 81 | # " -106.68000000000001, 40.973999999999997)", 82 | # "__repr__", 83 | # ) 84 | 85 | 86 | fn test_min_max() raises: 87 | let test = MojoTest("min/max methods") 88 | 89 | let e_of_pt2 = Envelope(Point(lon, lat)) 90 | test.assert_true(e_of_pt2.min_x() == lon, "min_x") 91 | test.assert_true(e_of_pt2.min_y() == lat, "min_y") 92 | 93 | test.assert_true(e_of_pt2.max_x() == lon, "max_x") 94 | test.assert_true(e_of_pt2.max_y() == lat, "max_y") 95 | 96 | # let e_of_ls2 = Envelope( 97 | # LineString( 98 | # Point2(lon, lat), 99 | # Point2(lon + 1, lat + 1), 100 | # Point2(lon + 2, lat + 5), 101 | # Point2(lon + 5, lat + 3), 102 | # Point2(lon + 4, lat + 4), 103 | # Point2(lon + 3, lat + 2), 104 | # ) 105 | # ) 106 | # test.assert_true(e_of_ls2.min_x() == lon, "min_x") 107 | # test.assert_true(e_of_ls2.min_y() == lat, "min_y") 108 | 109 | # test.assert_true(e_of_ls2.max_x() == lon + 5, "max_x") 110 | # test.assert_true(e_of_ls2.max_y() == lat + 5, "max_y") 111 | 112 | # let e_of_ls3 = Envelope( 113 | # LineStringZ( 114 | # PointZ(lon, lat, height), 115 | # PointZ(lon + 1, lat + 1, height - 1), 116 | # PointZ(lon + 2, lat + 2, height - 2), 117 | # PointZ(lon + 7, lat + 5, height - 5), 118 | # PointZ(lon + 4, lat + 4, height - 4), 119 | # PointZ(lon + 5, lat + 3, height - 3), 120 | # ) 121 | # ) 122 | # test.assert_true(e_of_ls3.min_x() == lon, "min_x") 123 | # test.assert_true(e_of_ls3.min_y() == lat, "min_y") 124 | # test.assert_true(e_of_ls3.min_z() == height - 5, "min_z") 125 | 126 | # test.assert_true(e_of_ls3.max_x() == lon + 7, "max_x") 127 | # test.assert_true(e_of_ls3.max_y() == lat + 5, "max_y") 128 | # test.assert_true(e_of_ls3.max_z() == height, "max_z") 129 | 130 | # let e_of_ls4 = Envelope( 131 | # LineString( 132 | # PointZ(lon, lat, height, measure), 133 | # PointZ(lon + 1, lat + 1, height - 1, measure + 0.01), 134 | # PointZ(lon + 2, lat + 2, height - 7, measure + 0.05), 135 | # PointZ(lon + 5, lat + 3, height - 3, measure + 0.03), 136 | # PointZ(lon + 4, lat + 5, height - 4, measure + 0.04), 137 | # PointZ(lon + 3, lat + 4, height - 5, measure + 0.02), 138 | # ) 139 | # ) 140 | 141 | # test.assert_true(e_of_ls4.min_x() == lon, "min_x") 142 | # test.assert_true(e_of_ls4.min_y() == lat, "min_y") 143 | # test.assert_true(e_of_ls4.min_z() == height - 7, "min_z") 144 | # test.assert_true(e_of_ls4.min_m() == measure, "min_m") 145 | 146 | # test.assert_true(e_of_ls4.max_x() == lon + 5, "max_x") 147 | # test.assert_true(e_of_ls4.max_y() == lat + 5, "max_y") 148 | # test.assert_true(e_of_ls4.max_z() == height, "max_z") 149 | # test.assert_true(e_of_ls4.max_m() == measure + 0.05, "max_m") 150 | 151 | 152 | fn test_southwesterly_point() raises: 153 | let test = MojoTest("southwesterly_point") 154 | let e = Envelope(Point(lon, lat)) 155 | let sw_pt = e.southwesterly_point() 156 | test.assert_true(sw_pt.x() == lon, "southwesterly_point") 157 | test.assert_true(sw_pt.y() == lat, "southwesterly_point") 158 | 159 | 160 | fn test_northeasterly_point() raises: 161 | let test = MojoTest("northeasterly_point") 162 | let e = Envelope(Point(lon, lat)) 163 | let sw_pt = e.northeasterly_point() 164 | test.assert_true(sw_pt.x() == lon, "northeasterly_point") 165 | test.assert_true(sw_pt.y() == lat, "northeasterly_point") 166 | 167 | 168 | fn test_with_geos() raises: 169 | """ 170 | Check envelope of complex features using shapely's envelope function. 171 | """ 172 | let test = MojoTest("shapely/geos") 173 | 174 | let json = Python.import_module("orjson") 175 | let builtins = Python.import_module("builtins") 176 | let shapely = Python.import_module("shapely") 177 | 178 | let envelope = shapely.envelope 179 | let shape = shapely.geometry.shape 180 | let mapping = shapely.geometry.mapping 181 | 182 | # LineString 183 | 184 | # let path = Path("mogeo/test/fixtures/geojson/line_string") 185 | # let fixtures = VariadicList("curved.geojson", "straight.geojson", "zigzag.geojson") 186 | # for i in range(len(fixtures)): 187 | # let file = path / fixtures[i] 188 | # with open(file, "r") as f: 189 | # let geojson = f.read() 190 | # let geojson_dict = json.loads(geojson) 191 | # let geometry = shape(geojson_dict) 192 | # let expect_bounds = geometry.bounds 193 | # let lstr = LineString.from_json(geojson_dict) 194 | # let env = Envelope(lstr) 195 | # for i in range(4): 196 | # test.assert_true( 197 | # env.coords[i].cast[DType.float64]() 198 | # == expect_bounds[i].to_float64(), 199 | # "envelope index:" + String(i), 200 | # ) 201 | 202 | 203 | fn test_equality_ops() raises: 204 | """ 205 | Test __eq__ and __ne__ methods. 206 | """ 207 | let test = MojoTest("equality ops") 208 | 209 | let e2 = Envelope(Point(lon, lat)) 210 | let e2_eq = Envelope(Point(lon, lat)) 211 | let e2_ne = Envelope(Point(lon + 0.01, lat - 0.02)) 212 | test.assert_true(e2 == e2_eq, "__eq__") 213 | test.assert_true(e2 != e2_ne, "__ne__") 214 | 215 | let e3 = Envelope(Point(lon, lat, height)) 216 | let e3_eq = Envelope(Point(lon, lat, height)) 217 | let e3_ne = Envelope(Point(lon, lat, height * 2)) 218 | test.assert_true(e3 == e3_eq, "__eq__") 219 | test.assert_true(e3 != e3_ne, "__ne__") 220 | 221 | let e4 = Envelope(Point(lon, lat, height, measure)) 222 | let e4_eq = Envelope(Point(lon, lat, height, measure)) 223 | let e4_ne = Envelope(Point(lon, lat, height, measure * 2)) 224 | test.assert_true(e4 == e4_eq, "__eq__") 225 | test.assert_true(e4 != e4_ne, "__ne__") 226 | -------------------------------------------------------------------------------- /mogeo/geom/envelope.mojo: -------------------------------------------------------------------------------- 1 | from utils.index import Index 2 | from math.limit import inf, neginf, max_finite, min_finite 3 | 4 | from sys.info import simdwidthof, simdbitwidth 5 | from algorithm import vectorize 6 | from algorithm.functional import parallelize 7 | import math 8 | from tensor import Tensor 9 | 10 | from mogeo.geom.empty import empty_value, is_empty 11 | from mogeo.geom.point import Point 12 | from mogeo.geom.enums import CoordDims 13 | from mogeo.geom.layout import Layout 14 | from mogeo.geom.traits import Geometric, Emptyable 15 | from mogeo.serialization.traits import JSONable, WKTable, Geoarrowable 16 | from mogeo.serialization import ( 17 | WKTParser, 18 | JSONParser, 19 | ) 20 | 21 | 22 | @value 23 | @register_passable("trivial") 24 | struct Envelope[dtype: DType]( 25 | CollectionElement, 26 | Emptyable, 27 | Geometric, 28 | # JSONable, 29 | Sized, 30 | Stringable, 31 | # WKTable, 32 | ): 33 | """ 34 | Envelope aka Bounding Box. 35 | 36 | > "The value of the bbox member must be an array of length 2*n where n is the number of dimensions represented in 37 | the contained geometries, with all axes of the most southwesterly point followed by all axes of the more 38 | northeasterly point." GeoJSON spec https://datatracker.ietf.org/doc/html/rfc7946 39 | """ 40 | 41 | alias point_simd_dims = 4 42 | alias envelope_simd_dims = 8 43 | alias PointCoordsT = SIMD[dtype, Self.point_simd_dims] 44 | alias PointT = Point[dtype] 45 | alias x_index = 0 46 | alias y_index = 1 47 | alias z_index = 2 48 | alias m_index = 3 49 | 50 | var coords: SIMD[dtype, Self.envelope_simd_dims] 51 | var ogc_dims: CoordDims 52 | 53 | fn __init__(point: Point[dtype]) -> Self: 54 | """ 55 | Construct Envelope of Point. 56 | """ 57 | var coords = SIMD[dtype, Self.envelope_simd_dims]() 58 | 59 | @unroll 60 | for i in range(Self.point_simd_dims): 61 | coords[i] = point.coords[i] 62 | coords[i + Self.point_simd_dims] = point.coords[i] 63 | return Self {coords: coords, ogc_dims: point.ogc_dims} 64 | 65 | # fn __init__(line_string: LineString[simd_dims, dtype]) -> Self: 66 | # """ 67 | # Construct Envelope of LineString. 68 | # """ 69 | # return Self(line_string.data) 70 | 71 | fn __init__(data: Layout[dtype=dtype]) -> Self: 72 | """ 73 | Construct Envelope from memory Layout. 74 | """ 75 | alias nelts = simdbitwidth() 76 | alias n = Self.envelope_simd_dims 77 | var coords = SIMD[dtype, Self.envelope_simd_dims]() 78 | 79 | # fill initial values of with inf/neginf at each position in the 2*n array 80 | @unroll 81 | for d in range(n): # dims 1:4 82 | coords[d] = max_finite[ 83 | dtype 84 | ]() # min (southwest) values, start from max finite. 85 | 86 | @unroll 87 | for d in range(Self.point_simd_dims, n): # dims 5:8 88 | coords[d] = min_finite[ 89 | dtype 90 | ]() # max (northeast) values, start from min finite 91 | 92 | let num_features = data.coordinates.shape()[1] 93 | 94 | # vectorized load and min/max calculation for each of the dims 95 | @unroll 96 | for dim in range(Self.point_simd_dims): 97 | 98 | @parameter 99 | fn min_max_simd[simd_width: Int](feature_idx: Int): 100 | let index = Index(dim, feature_idx) 101 | let values = data.coordinates.simd_load[simd_width](index) 102 | let min = values.reduce_min() 103 | if min < coords[dim]: 104 | coords[dim] = min 105 | let max = values.reduce_max() 106 | if max > coords[Self.point_simd_dims + dim]: 107 | coords[Self.point_simd_dims + dim] = max 108 | 109 | vectorize[nelts, min_max_simd](num_features) 110 | 111 | return Self {coords: coords, ogc_dims: data.ogc_dims} 112 | 113 | @staticmethod 114 | fn empty(ogc_dims: CoordDims = CoordDims.Point) -> Self: 115 | let coords = SIMD[dtype, Self.envelope_simd_dims](empty_value[dtype]()) 116 | return Self {coords: coords, ogc_dims: ogc_dims} 117 | 118 | fn __eq__(self, other: Self) -> Bool: 119 | # NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. 120 | @unroll 121 | for i in range(Self.envelope_simd_dims): 122 | if is_empty(self.coords[i]) and is_empty(other.coords[i]): 123 | pass # equality at index i 124 | else: 125 | if is_empty(self.coords[i]) or is_empty(other.coords[i]): 126 | return False # early out: one or the other is empty (but not both) -> not equal 127 | if self.coords[i] != other.coords[i]: 128 | return False # not equal 129 | return True # equal 130 | 131 | fn __ne__(self, other: Self) -> Bool: 132 | return not self == other 133 | 134 | fn __repr__(self) -> String: 135 | var res = "Envelope [" + dtype.__str__() + "](" 136 | for i in range(Self.envelope_simd_dims): 137 | res += str(self.coords[i]) 138 | if i < Self.envelope_simd_dims - 1: 139 | res += ", " 140 | res += ")" 141 | return res 142 | 143 | fn __len__(self) -> Int: 144 | return self.dims() 145 | 146 | fn __str__(self) -> String: 147 | return self.__repr__() 148 | 149 | # 150 | # Getters 151 | # 152 | fn southwesterly_point(self) -> Self.PointT: 153 | alias offset = 0 154 | return Self.PointT(self.coords.slice[Self.point_simd_dims](offset)) 155 | 156 | fn northeasterly_point(self) -> Self.PointT: 157 | alias offset = Self.point_simd_dims 158 | return Self.PointT(self.coords.slice[Self.point_simd_dims](offset)) 159 | 160 | @always_inline 161 | fn min_x(self) -> SIMD[dtype, 1]: 162 | let i = self.x_index 163 | return self.coords[i] 164 | 165 | @always_inline 166 | fn max_x(self) -> SIMD[dtype, 1]: 167 | let i = Self.point_simd_dims + self.x_index 168 | return self.coords[i] 169 | 170 | @always_inline 171 | fn min_y(self) -> SIMD[dtype, 1]: 172 | alias i = self.y_index 173 | return self.coords[i] 174 | 175 | @always_inline 176 | fn max_y(self) -> SIMD[dtype, 1]: 177 | alias i = Self.point_simd_dims + Self.y_index 178 | return self.coords[i] 179 | 180 | @always_inline 181 | fn min_z(self) -> SIMD[dtype, 1]: 182 | alias i = Self.z_index 183 | return self.coords[i] 184 | 185 | @always_inline 186 | fn max_z(self) -> SIMD[dtype, 1]: 187 | alias i = Self.point_simd_dims + Self.z_index 188 | return self.coords[i] 189 | 190 | @always_inline 191 | fn min_m(self) -> SIMD[dtype, 1]: 192 | let i = self.m_index 193 | return self.coords[i] 194 | 195 | @always_inline 196 | fn max_m(self) -> SIMD[dtype, 1]: 197 | let i = Self.point_simd_dims + Self.m_index 198 | return self.coords[i] 199 | 200 | fn dims(self) -> Int: 201 | return len(self.ogc_dims) 202 | 203 | fn set_ogc_dims(inout self, ogc_dims: CoordDims): 204 | """ 205 | Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. 206 | (ex: when Point Z vs Point M is ambiguous. 207 | """ 208 | debug_assert( 209 | len(self.ogc_dims) == 3 and len(ogc_dims) == 3, 210 | "Unsafe change of dimension number", 211 | ) 212 | self.ogc_dims = ogc_dims 213 | 214 | fn has_height(self) -> Bool: 215 | return (self.ogc_dims == CoordDims.PointZ) or ( 216 | self.ogc_dims == CoordDims.PointZM 217 | ) 218 | 219 | fn has_measure(self) -> Bool: 220 | return (self.ogc_dims == CoordDims.PointM) or ( 221 | self.ogc_dims == CoordDims.PointZM 222 | ) 223 | 224 | fn is_empty(self) -> Bool: 225 | return is_empty[dtype](self.coords) 226 | 227 | fn envelope[dtype: DType = dtype](self) -> Self: 228 | """ 229 | Geometric trait. 230 | """ 231 | return self 232 | 233 | fn wkt(self) -> String: 234 | """ 235 | TODO: wkt. 236 | POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin)). 237 | """ 238 | return "POLYGON ((xmin ymin, xmax ymin, xmax ymax, xmin ymax, xmin ymin))" 239 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_line_string.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | from utils.vector import DynamicVector 4 | from utils.index import Index 5 | from pathlib import Path 6 | 7 | from mogeo.test.pytest import MojoTest 8 | from mogeo.test.constants import lon, lat, height, measure 9 | from mogeo.geom.point import Point, Point64 10 | from mogeo.geom.line_string import LineString 11 | 12 | 13 | fn main() raises: 14 | test_constructors() 15 | test_validate() 16 | test_memory_layout() 17 | test_get_item() 18 | test_equality_ops() 19 | test_repr() 20 | test_stringable() 21 | test_emptyable() 22 | test_wktable() 23 | test_jsonable() 24 | test_geoarrowable() 25 | 26 | 27 | fn test_constructors() raises: 28 | var test = MojoTest("variadic list constructor") 29 | 30 | let lstr = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 31 | test.assert_true(lstr[0] == Point(lon, lat), "variadic list constructor") 32 | test.assert_true(lstr[1] == Point(lon, lat), "variadic list constructor") 33 | test.assert_true(lstr[2] == Point(lon, lat + 1), "variadic list constructor") 34 | test.assert_true(lstr.__len__() == 3, "variadic list constructor") 35 | 36 | test = MojoTest("vector constructor") 37 | 38 | var points_vec = DynamicVector[Point64](10) 39 | for n in range(10): 40 | points_vec.push_back(Point(lon + n, lat - n)) 41 | let lstr2 = LineString[DType.float64](points_vec) 42 | for n in range(10): 43 | let expect_pt = Point(lon + n, lat - n) 44 | test.assert_true(lstr2[n] == expect_pt, "vector constructor") 45 | test.assert_true(lstr2.__len__() == 10, "vector constructor") 46 | 47 | 48 | fn test_validate() raises: 49 | let test = MojoTest("is_valid") 50 | 51 | var err = String() 52 | var valid: Bool = False 53 | 54 | valid = LineString(Point(lon, lat), Point(lon, lat)).is_valid(err) 55 | test.assert_true(not valid, "is_valid") 56 | test.assert_true( 57 | err == "LineStrings with exactly two identical points are invalid.", 58 | "unexpected error value", 59 | ) 60 | 61 | valid = LineString(Point(lon, lat)).is_valid(err) 62 | test.assert_true( 63 | err == "LineStrings must have either 0 or 2 or more points.", 64 | "unexpected error value", 65 | ) 66 | 67 | valid = LineString( 68 | Point(lon, lat), Point(lon + 1, lat + 1), Point(lon, lat) 69 | ).is_valid(err) 70 | test.assert_true( 71 | err == "LineStrings must not be closed: try LinearRing.", 72 | "unexpected error value", 73 | ) 74 | 75 | 76 | fn test_memory_layout() raises: 77 | # Test if LineString fills the Layout struct correctly. 78 | let test = MojoTest("memory_layout") 79 | 80 | # equality check each point by indexing into the LineString. 81 | var points_vec20 = DynamicVector[Point64](10) 82 | for n in range(10): 83 | points_vec20.push_back(Point(lon + n, lat - n)) 84 | let lstr = LineString(points_vec20) 85 | for n in range(10): 86 | let expect_pt = Point(lon + n, lat - n) 87 | test.assert_true(lstr[n] == expect_pt, "memory_layout") 88 | 89 | # here the geometry_offsets, part_offsets, and ring_offsets are unused because 90 | # of using "struct coordinate representation" (tensor) 91 | let layout = lstr.data 92 | test.assert_true( 93 | layout.geometry_offsets.num_elements() == 0, "geo_arrow geometry_offsets" 94 | ) 95 | test.assert_true(layout.part_offsets.num_elements() == 0, "geo_arrow part_offsets") 96 | test.assert_true(layout.ring_offsets.num_elements() == 0, "geo_arrow ring_offsets") 97 | 98 | 99 | fn test_get_item() raises: 100 | let test = MojoTest("get_item") 101 | var points_vec = DynamicVector[Point64](10) 102 | for n in range(10): 103 | points_vec.push_back(Point(lon + n, lat - n)) 104 | let lstr = LineString(points_vec) 105 | for n in range(10): 106 | let expect_pt = Point(lon + n, lat - n) 107 | let got_pt = lstr[n] 108 | test.assert_true(got_pt == expect_pt, "get_item") 109 | 110 | 111 | fn test_equality_ops() raises: 112 | let test = MojoTest("equality operators") 113 | 114 | # partial simd_load (n - i < nelts) 115 | let lstr8 = LineString( 116 | Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8), Point(9, 10) 117 | ) 118 | let lstr9 = LineString( 119 | Point(1.1, 2.1), 120 | Point(3.1, 4.1), 121 | Point(5.1, 6.1), 122 | Point(7.1, 8.1), 123 | Point(9.1, 10.1), 124 | ) 125 | test.assert_true(lstr8 != lstr9, "partial simd_load (n - i < nelts)") 126 | 127 | # partial simd_load (n - i < nelts) 128 | alias PointF32 = Point[DType.float32] 129 | let lstr10 = LineString( 130 | PointF32(1, 2), 131 | PointF32(5, 6), 132 | PointF32(10, 11), 133 | ) 134 | let lstr11 = LineString( 135 | PointF32(1, 2), 136 | PointF32(5, 6), 137 | PointF32(10, 11.1), 138 | ) 139 | test.assert_true(lstr10 != lstr11, "partial simd_load (n - i < nelts) (b)") 140 | 141 | # not equal 142 | alias PointF16 = Point[DType.float16] 143 | let lstr12 = LineString( 144 | PointF16(1, 2), 145 | PointF16(5, 6), 146 | PointF16(10, 11), 147 | ) 148 | let lstr13 = LineString( 149 | PointF16(1, 2), 150 | PointF16(5, 6), 151 | PointF16(10, 11.1), 152 | ) 153 | test.assert_true(lstr12 != lstr13, "__ne__") 154 | 155 | var points_vec = DynamicVector[Point64](10) 156 | for n in range(10): 157 | points_vec.push_back(Point(lon + n, lat - n)) 158 | 159 | let lstr2 = LineString(points_vec) 160 | let lstr3 = LineString(points_vec) 161 | test.assert_true(lstr2 == lstr3, "__eq__") 162 | 163 | let lstr4 = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 164 | let lstr5 = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 165 | test.assert_true(lstr4 == lstr5, "__eq__") 166 | 167 | let lstr6 = LineString(Point(42, lat), Point(lon, lat)) 168 | test.assert_true(lstr5 != lstr6, "__eq__") 169 | 170 | 171 | fn test_emptyable() raises: 172 | let test = MojoTest("is_empty") 173 | let empty_lstr = LineString() 174 | _ = empty_lstr.is_empty() 175 | 176 | 177 | fn test_repr() raises: 178 | let test = MojoTest("__repr__") 179 | let lstr = LineString(Point(42, lat), Point(lon, lat)) 180 | test.assert_true( 181 | lstr.__repr__() == "LineString [Point, float64](2 points)", "__repr__" 182 | ) 183 | 184 | 185 | fn test_sized() raises: 186 | let test = MojoTest("sized") 187 | let lstr = LineString(Point(42, lat), Point(lon, lat)) 188 | test.assert_true(len(lstr) == 2, "__len__") 189 | 190 | 191 | fn test_stringable() raises: 192 | let test = MojoTest("stringable") 193 | let lstr = LineString(Point(42, lat), Point(lon, lat)) 194 | test.assert_true(lstr.__str__() == lstr.__repr__(), "__str__") 195 | 196 | 197 | fn test_wktable() raises: 198 | test_wkt() 199 | # test_from_wkt() 200 | 201 | 202 | fn test_wkt() raises: 203 | let test = MojoTest("wkt") 204 | let lstr = LineString(Point(lon, lat), Point(lon, lat), Point(lon, lat + 1)) 205 | test.assert_true( 206 | lstr.wkt() 207 | == "LINESTRING(-108.68000000000001 38.973999999999997, -108.68000000000001" 208 | " 38.973999999999997, -108.68000000000001 39.973999999999997)", 209 | "wkt", 210 | ) 211 | 212 | 213 | fn test_jsonable() raises: 214 | test_json() 215 | test_from_json() 216 | 217 | 218 | fn test_json() raises: 219 | let test = MojoTest("json") 220 | var points_vec = DynamicVector[Point64](10) 221 | for n in range(10): 222 | points_vec.push_back(Point(lon + n, lat - n)) 223 | let json = LineString(points_vec).json() 224 | test.assert_true( 225 | json 226 | == '{"type":"LineString","coordinates":[[-108.68000000000001,38.973999999999997],[-107.68000000000001,37.973999999999997],[-106.68000000000001,36.973999999999997],[-105.68000000000001,35.973999999999997],[-104.68000000000001,34.973999999999997],[-103.68000000000001,33.973999999999997],[-102.68000000000001,32.973999999999997],[-101.68000000000001,31.973999999999997],[-100.68000000000001,30.973999999999997],[-99.680000000000007,29.973999999999997]]}', 227 | "json", 228 | ) 229 | 230 | 231 | fn test_from_json() raises: 232 | let test = MojoTest("from_json()") 233 | 234 | let json = Python.import_module("orjson") 235 | let builtins = Python.import_module("builtins") 236 | let path = Path("mogeo/test/fixtures/geojson/line_string") 237 | let fixtures = VariadicList("curved.geojson", "straight.geojson", "zigzag.geojson") 238 | 239 | for i in range(len(fixtures)): 240 | let file = path / fixtures[i] 241 | with open(file.path, "r") as f: 242 | let geojson = f.read() 243 | let geojson_dict = json.loads(geojson) 244 | _ = LineString.from_json(geojson_dict) 245 | 246 | 247 | fn test_geoarrowable() raises: 248 | # TODO: geoarrowable trait 249 | pass 250 | -------------------------------------------------------------------------------- /mogeo/geom/line_string.mojo: -------------------------------------------------------------------------------- 1 | from tensor import Tensor, TensorSpec, TensorShape 2 | from utils.index import Index 3 | from utils.vector import DynamicVector 4 | from memory import memcmp 5 | from python import Python 6 | 7 | from mogeo.serialization import WKTParser, JSONParser 8 | from mogeo.geom.point import Point 9 | from mogeo.geom.layout import Layout 10 | from mogeo.geom.enums import CoordDims 11 | from mogeo.geom.empty import is_empty, empty_value 12 | from mogeo.geom.traits import Geometric, Emptyable 13 | from mogeo.serialization.traits import WKTable, JSONable, Geoarrowable 14 | from mogeo.serialization import ( 15 | WKTParser, 16 | JSONParser, 17 | ) 18 | 19 | 20 | @value 21 | struct LineString[dtype: DType = DType.float64]( 22 | CollectionElement, 23 | Emptyable, 24 | # Geoarrowable, 25 | Geometric, 26 | JSONable, 27 | Sized, 28 | Stringable, 29 | # WKTable, 30 | ): 31 | """ 32 | Models an OGC-style LineString. 33 | 34 | A LineString consists of a sequence of two or more vertices along with all points along the linearly-interpolated 35 | curves (line segments) between each pair of consecutive vertices. Consecutive vertices may be equal. 36 | 37 | The line segments in the line may intersect each other (in other words, the linestring may "curl back" in itself and 38 | self-intersect). 39 | 40 | - Linestrings with exactly two identical points are invalid. 41 | - Linestrings must have either 0 or 2 or more points. 42 | - If these conditions are not met, the constructors raise an Error. 43 | """ 44 | 45 | var data: Layout[dtype] 46 | 47 | fn __init__(inout self): 48 | """ 49 | Construct empty LineString. 50 | """ 51 | self.data = Layout[dtype]() 52 | 53 | fn __init(inout self, data: Layout[dtype]): 54 | self.data = data 55 | 56 | fn __init__(inout self, *points: Point[dtype]): 57 | """ 58 | Construct `LineString` from variadic list of `Point`. 59 | """ 60 | debug_assert(len(points) > 0, "unreachable") 61 | let n = len(points) 62 | 63 | if n == 0: 64 | # empty linestring 65 | self.data = Layout[dtype]() 66 | return 67 | 68 | let sample_pt = points[0] 69 | let dims = len(sample_pt) 70 | self.data = Layout[dtype](coords_size=n) 71 | 72 | for y in range(dims): 73 | for x in range(len(points)): 74 | self.data.coordinates[Index(y, x)] = points[x].coords[y] 75 | 76 | fn __init__(inout self, points: DynamicVector[Point[dtype]]): 77 | """ 78 | Construct `LineString` from a vector of `Points`. 79 | """ 80 | # here the geometry_offsets, part_offsets, and ring_offsets are unused because 81 | # of using "struct coordinate representation" (tensor) 82 | 83 | let n = len(points) 84 | 85 | if n == 0: 86 | # empty linestring 87 | self.data = Layout[dtype]() 88 | return 89 | 90 | let sample_pt = points[0] 91 | let dims = len(sample_pt) 92 | self.data = Layout[dtype](coords_size=n) 93 | 94 | for y in range(dims): 95 | for x in range(len(points)): 96 | self.data.coordinates[Index(y, x)] = points[x].coords[y] 97 | 98 | @staticmethod 99 | fn empty(dims: CoordDims = CoordDims.Point) -> Self: 100 | return Self() 101 | 102 | fn __len__(self) -> Int: 103 | """ 104 | Return the number of Point elements. 105 | """ 106 | return self.data.coordinates.shape()[1] 107 | 108 | fn dims(self) -> Int: 109 | return len(self.data.ogc_dims) 110 | 111 | fn __eq__(self, other: Self) -> Bool: 112 | return self.data == other.data 113 | 114 | fn __ne__(self, other: Self) -> Bool: 115 | return not self.__eq__(other) 116 | 117 | fn __repr__(self) -> String: 118 | return ( 119 | "LineString [" 120 | + str(self.data.ogc_dims) 121 | + ", " 122 | + dtype.__str__() 123 | + "](" 124 | + String(len(self)) 125 | + " points)" 126 | ) 127 | 128 | fn __getitem__(self: Self, feature_index: Int) -> Point[dtype]: 129 | """ 130 | Get Point from LineString at index. 131 | """ 132 | var result = Point[dtype]() 133 | for i in range(self.dims()): 134 | result.coords[i] = self.data.coordinates[Index(i, feature_index)] 135 | return result 136 | 137 | fn has_height(self) -> Bool: 138 | return self.data.has_height() 139 | 140 | fn has_measure(self) -> Bool: 141 | return self.data.has_measure() 142 | 143 | fn set_ogc_dims(inout self, ogc_dims: CoordDims): 144 | """ 145 | Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. 146 | (ex: when Point Z vs Point M is ambiguous. 147 | """ 148 | debug_assert( 149 | len(self.data.ogc_dims) == len(ogc_dims), 150 | "Unsafe change of dimension number", 151 | ) 152 | self.data.set_ogc_dims(ogc_dims) 153 | 154 | fn is_valid(self, inout err: String) -> Bool: 155 | """ 156 | Validate geometry. When False, sets the `err` string with a condition. 157 | 158 | - Linestrings with exactly two identical points are invalid. 159 | - Linestrings must have either 0 or 2 or more points. 160 | - LineStrings must not be closed: try LinearRing. 161 | """ 162 | if self.is_empty(): 163 | return True 164 | 165 | let n = len(self) 166 | if n == 2 and self[0] == self[1]: 167 | err = "LineStrings with exactly two identical points are invalid." 168 | return False 169 | if n == 1: 170 | err = "LineStrings must have either 0 or 2 or more points." 171 | return False 172 | if self.is_closed(): 173 | err = "LineStrings must not be closed: try LinearRing." 174 | 175 | return True 176 | 177 | @staticmethod 178 | fn from_json(json_dict: PythonObject) raises -> Self: 179 | """ 180 | Construct `MultiPoint` from GeoJSON Python dictionary. 181 | """ 182 | var json_coords = json_dict.get("coordinates", Python.none()) 183 | if not json_coords: 184 | raise Error("LineString.from_json(): coordinates property missing in dict.") 185 | var points = DynamicVector[Point[dtype]]() 186 | for coords in json_coords: 187 | let lon = coords[0].to_float64().cast[dtype]() 188 | let lat = coords[1].to_float64().cast[dtype]() 189 | let pt = Point[dtype](lon, lat) 190 | points.push_back(pt) 191 | return LineString[dtype](points) 192 | 193 | @staticmethod 194 | fn from_json(json_str: String) raises -> Self: 195 | """ 196 | Construct `LineString` from GeoJSON serialized string. 197 | """ 198 | let json_dict = JSONParser.parse(json_str) 199 | return Self.from_json(json_dict) 200 | 201 | fn __str__(self) -> String: 202 | return self.__repr__() 203 | 204 | fn json(self) raises -> String: 205 | """ 206 | Serialize `LineString` to GeoJSON. Coordinates of LineString are an array of positions. 207 | 208 | ### Spec 209 | 210 | - https://geojson.org 211 | - https://datatracker.ietf.org/doc/html/rfc7946 212 | 213 | ```json 214 | { 215 | "type": "LineString", 216 | "coordinates": [ 217 | [100.0, 0.0], 218 | [101.0, 1.0] 219 | ] 220 | } 221 | ``` 222 | """ 223 | if self.data.ogc_dims.value > CoordDims.PointZ.value: 224 | raise Error( 225 | "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" 226 | ) 227 | 228 | let dims = self.dims() 229 | let n = len(self) 230 | var res = String('{"type":"LineString","coordinates":[') 231 | for i in range(n): 232 | let pt = self[i] 233 | res += "[" 234 | for dim in range(3): 235 | if dim > dims - 1: 236 | break 237 | res += pt[dim] 238 | if dim < dims - 1: 239 | res += "," 240 | res += "]" 241 | if i < n - 1: 242 | res += "," 243 | res += "]}" 244 | return res 245 | 246 | fn wkt(self) -> String: 247 | if self.is_empty(): 248 | return "LINESTRING EMPTY" 249 | let dims = self.dims() 250 | var res = String("LINESTRING(") 251 | let n = len(self) 252 | for i in range(n): 253 | let pt = self[i] 254 | for j in range(dims): 255 | res += pt.coords[j] 256 | if j < dims - 1: 257 | res += " " 258 | if i < n - 1: 259 | res += ", " 260 | res += ")" 261 | return res 262 | 263 | fn is_closed(self) -> Bool: 264 | """ 265 | If LineString is closed (0 and n-1 points are equal), it's not valid: a LinearRing should be used instead. 266 | """ 267 | let n = len(self) 268 | if n == 1: 269 | return False 270 | let start_pt = self[0] 271 | let end_pt = self[n - 1] 272 | return start_pt == end_pt 273 | 274 | fn is_empty(self) -> Bool: 275 | return len(self) == 0 276 | -------------------------------------------------------------------------------- /mogeo/geom/multi_point.mojo: -------------------------------------------------------------------------------- 1 | from tensor import Tensor, TensorSpec, TensorShape 2 | from utils.index import Index 3 | from utils.vector import DynamicVector 4 | from memory import memcmp 5 | from python import Python 6 | 7 | from mogeo.serialization import WKTParser, JSONParser 8 | from mogeo.geom.layout import Layout 9 | from mogeo.geom.empty import is_empty, empty_value 10 | from mogeo.geom.traits import Geometric, Emptyable 11 | from mogeo.geom.point import Point 12 | from mogeo.geom.enums import CoordDims 13 | from mogeo.serialization.traits import WKTable, JSONable, Geoarrowable 14 | from mogeo.serialization import ( 15 | WKTParser, 16 | JSONParser, 17 | ) 18 | 19 | 20 | @value 21 | struct MultiPoint[dtype: DType = DType.float64]( 22 | CollectionElement, 23 | Emptyable, 24 | Geoarrowable, 25 | Geometric, 26 | JSONable, 27 | Sized, 28 | Stringable, 29 | WKTable, 30 | ): 31 | """ 32 | Models an OGC-style MultiPoint. Any collection of Points is a valid MultiPoint, 33 | except [heterogeneous dimension multipoints](https://geoarrow.org/format) which are unsupported. 34 | """ 35 | 36 | var data: Layout[dtype] 37 | 38 | fn __init__(inout self): 39 | """ 40 | Construct empty MultiPoint. 41 | """ 42 | self.data = Layout[dtype]() 43 | 44 | fn __init(inout self, data: Layout[dtype]): 45 | self.data = data 46 | 47 | fn __init__(inout self, *points: Point[dtype]): 48 | """ 49 | Construct `MultiPoint` from a variadic list of `Points`. 50 | """ 51 | debug_assert(len(points) > 0, "unreachable") 52 | let n = len(points) 53 | # sample 1st point as prototype to get dims 54 | let sample_pt = points[0] 55 | let dims = len(sample_pt) 56 | self.data = Layout[dtype](ogc_dims=sample_pt.ogc_dims, coords_size=n) 57 | for y in range(dims): 58 | for x in range(n): 59 | self.data.coordinates[Index(y, x)] = points[x].coords[y] 60 | 61 | fn __init__(inout self, points: DynamicVector[Point[dtype]]): 62 | """ 63 | Construct `MultiPoint` from a vector of `Point`. 64 | """ 65 | let n = len(points) 66 | if len(points) == 0: 67 | # early out with empty MultiPoint 68 | self.data = Layout[dtype]() 69 | return 70 | # sample 1st point as prototype to get dims 71 | let sample_pt = points[0] 72 | let dims = len(sample_pt) 73 | self.data = Layout[dtype](ogc_dims=sample_pt.ogc_dims, coords_size=n) 74 | for dim in range(dims): 75 | for i in range(n): 76 | let value = points[i].coords[dim] 77 | self.data.coordinates[Index(dim, i)] = value 78 | 79 | @staticmethod 80 | fn from_json(json_dict: PythonObject) raises -> Self: 81 | """ 82 | Construct `MultiPoint` from GeoJSON (Python dictionary). 83 | """ 84 | let json_coords = json_dict["coordinates"] 85 | let n = int(json_coords.__len__()) 86 | # TODO: type checking of json_dict (coordinates property exists) 87 | let dims = json_coords[0].__len__().to_float64().to_int() 88 | let ogc_dims = CoordDims.PointZ if dims == 3 else CoordDims.Point 89 | var data = Layout[dtype](ogc_dims, coords_size=n) 90 | for dim in range(dims): 91 | for i in range(n): 92 | let point = json_coords[i] 93 | # TODO: bounds check of geojson point 94 | let value = point[dim].to_float64().cast[dtype]() 95 | data.coordinates[Index(dim, i)] = value 96 | return Self(data) 97 | 98 | @staticmethod 99 | fn from_json(json_str: String) raises -> Self: 100 | """ 101 | Construct `MultiPoint` from GeoJSON serialized string. 102 | """ 103 | let json_dict = JSONParser.parse(json_str) 104 | return Self.from_json(json_dict) 105 | 106 | @staticmethod 107 | fn from_wkt(wkt: String) raises -> Self: 108 | let geometry_sequence = WKTParser.parse(wkt) 109 | # TODO: validate PythonObject is a class MultiPoint https://shapely.readthedocs.io/en/stable/reference/shapely.MultiPoint.html 110 | let n = geometry_sequence.geoms.__len__().to_float64().to_int() 111 | if n == 0: 112 | return Self() 113 | let sample_pt = geometry_sequence.geoms[0] 114 | let coords_tuple = sample_pt.coords[0] 115 | let dims = coords_tuple.__len__().to_float64().to_int() 116 | let ogc_dims = CoordDims.PointZ if dims == 3 else CoordDims.Point 117 | var data = Layout[dtype](ogc_dims, coords_size=n) 118 | for y in range(dims): 119 | for x in range(n): 120 | let geom = geometry_sequence.geoms[x] 121 | let coords_tuple = geom.coords[0] 122 | let value = coords_tuple[y].to_float64().cast[dtype]() 123 | data.coordinates[Index(y, x)] = value 124 | return Self(data) 125 | 126 | @staticmethod 127 | fn from_geoarrow(table: PythonObject) raises -> Self: 128 | let ga = Python.import_module("geoarrow.pyarrow") 129 | let geoarrow = ga.as_geoarrow(table["geometry"]) 130 | let chunk = geoarrow[0] 131 | let n = chunk.value.__len__() 132 | # TODO: inspect first point to see number of dims (same as in from_wkt above) 133 | if n > 2: 134 | raise Error("Invalid Point dims parameter vs. geoarrow: " + str(n)) 135 | # TODO: add to Layout 136 | # return result 137 | return Self() 138 | 139 | @staticmethod 140 | fn empty(ogc_dims: CoordDims = CoordDims.Point) -> Self: 141 | return Self() 142 | 143 | fn __len__(self) -> Int: 144 | """ 145 | Returns the number of Point elements. 146 | """ 147 | return self.data.coordinates.shape()[1] 148 | 149 | fn __eq__(self, other: Self) -> Bool: 150 | return self.data == other.data 151 | 152 | fn __ne__(self, other: Self) -> Bool: 153 | return not self.__eq__(other) 154 | 155 | fn __repr__(self) -> String: 156 | return ( 157 | "MultiPoint [" 158 | + str(self.data.ogc_dims) 159 | + ", " 160 | + str(dtype) 161 | + "](" 162 | + String(len(self)) 163 | + " points)" 164 | ) 165 | 166 | fn dims(self) -> Int: 167 | return len(self.data.ogc_dims) 168 | 169 | fn has_height(self) -> Bool: 170 | return self.data.has_height() 171 | 172 | fn has_measure(self) -> Bool: 173 | return self.data.has_measure() 174 | 175 | fn set_ogc_dims(inout self, ogc_dims: CoordDims): 176 | """ 177 | Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. 178 | (ex: when Point Z vs Point M is ambiguous. 179 | """ 180 | debug_assert( 181 | len(self.data.ogc_dims) == len(ogc_dims), 182 | "Unsafe change of dimension number", 183 | ) 184 | self.data.set_ogc_dims(ogc_dims) 185 | 186 | fn __getitem__(self: Self, feature_index: Int) -> Point[dtype]: 187 | """ 188 | Get Point from MultiPoint at index. 189 | """ 190 | var point = Point[dtype](self.data.ogc_dims) 191 | for dim_index in range(self.dims()): 192 | point.coords[dim_index] = self.data.coordinates[ 193 | Index(dim_index, feature_index) 194 | ] 195 | return point 196 | 197 | fn __str__(self) -> String: 198 | return self.__repr__() 199 | 200 | fn json(self) raises -> String: 201 | """ 202 | Serialize `MultiPoint` to GeoJSON. Coordinates of MultiPoint are an array of positions. 203 | 204 | ### Spec 205 | 206 | - https://geojson.org 207 | - https://datatracker.ietf.org/doc/html/rfc7946 208 | 209 | ```json 210 | { 211 | "type": "MultiPoint", 212 | "coordinates": [ 213 | [100.0, 0.0], 214 | [101.0, 1.0] 215 | ] 216 | } 217 | ``` 218 | """ 219 | if self.data.ogc_dims.value > CoordDims.PointZ.value: 220 | raise Error( 221 | "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" 222 | ) 223 | 224 | let n = len(self) 225 | let dims = self.data.dims() 226 | var res = String('{"type":"MultiPoint","coordinates":[') 227 | for i in range(n): 228 | let pt = self[i] 229 | res += "[" 230 | for dim in range(dims): 231 | res += pt[dim] 232 | if dim < dims - 1: 233 | res += "," 234 | res += "]" 235 | if i < n - 1: 236 | res += "," 237 | res += "]}" 238 | return res 239 | 240 | fn wkt(self) -> String: 241 | if self.is_empty(): 242 | return "MULTIPOINT EMPTY" 243 | let dims = self.data.dims() 244 | var res = String("MULTIPOINT (") 245 | let n = len(self) 246 | for i in range(n): 247 | let pt = self[i] 248 | for dim in range(dims): 249 | res += pt[dims] 250 | if dim < dims - 1: 251 | res += " " 252 | if i < n - 1: 253 | res += ", " 254 | res += ")" 255 | return res 256 | 257 | fn is_empty(self) -> Bool: 258 | return len(self) == 0 259 | 260 | fn geoarrow(self) -> PythonObject: 261 | # TODO: geoarrow 262 | return PythonObject() 263 | -------------------------------------------------------------------------------- /mogeo/geom/point.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from math import nan, isnan 3 | from math.limit import max_finite 4 | 5 | from mogeo.geom.empty import empty_value, is_empty 6 | from mogeo.geom.envelope import Envelope 7 | from mogeo.serialization.traits import WKTable, JSONable, Geoarrowable 8 | from mogeo.serialization import ( 9 | WKTParser, 10 | JSONParser, 11 | ) 12 | from .traits import Geometric, Emptyable 13 | from .enums import CoordDims 14 | 15 | alias Point64 = Point[DType.float64] 16 | alias Point32 = Point[DType.float32] 17 | alias Point16 = Point[DType.float16] 18 | 19 | 20 | @value 21 | @register_passable("trivial") 22 | struct Point[dtype: DType = DType.float64]( 23 | CollectionElement, 24 | Geoarrowable, 25 | Geometric, 26 | Emptyable, 27 | JSONable, 28 | Sized, 29 | Stringable, 30 | WKTable, 31 | ): 32 | """ 33 | Point is a register-passable (copy-efficient) struct holding 2 or more dimension values. 34 | 35 | ### Parameters 36 | 37 | - dtype: supports any float or integer type (default = float64) 38 | 39 | ### Memory Layouts 40 | 41 | Some examples of memory layout using Mojo SIMD[dtype, 4] value: 42 | 43 | ```txt 44 | 45 | ``` 46 | """ 47 | 48 | alias simd_dims = 4 49 | alias x_index = 0 50 | alias y_index = 1 51 | alias z_index = 2 52 | alias m_index = 3 53 | 54 | var coords: SIMD[dtype, Self.simd_dims] 55 | var ogc_dims: CoordDims 56 | 57 | # 58 | # Constructors (in addition to @value's member-wise init) 59 | # 60 | fn __init__(dims: CoordDims = CoordDims.Point) -> Self: 61 | """ 62 | Create Point with empty values. 63 | """ 64 | let empty = empty_value[dtype]() 65 | let coords = SIMD[dtype, Self.simd_dims](empty) 66 | return Self {coords: coords, ogc_dims: dims} 67 | 68 | fn __init__(*coords_list: SIMD[dtype, 1]) -> Self: 69 | """ 70 | Create Point from variadic list of SIMD values. Any missing elements are padded with empty values. 71 | 72 | ### See also 73 | 74 | Setter method for ogc_dims enum. May be useful when Point Z vs Point M is ambiguous in this constructor. 75 | """ 76 | let empty = empty_value[dtype]() 77 | var coords = SIMD[dtype, Self.simd_dims](empty) 78 | var ogc_dims = CoordDims.Point 79 | let n = len(coords_list) 80 | for i in range(Self.simd_dims): 81 | if i < n: 82 | coords[i] = coords_list[i] 83 | 84 | if n == 3: 85 | ogc_dims = CoordDims.PointZ 86 | # workaround in case this is a Point M (measure). Duplicate the measure value in index 2 and 3. 87 | coords[Self.m_index] = coords[Self.z_index] 88 | elif n >= 4: 89 | ogc_dims = CoordDims.PointZM 90 | 91 | return Self {coords: coords, ogc_dims: ogc_dims} 92 | 93 | fn __init__( 94 | coords: SIMD[dtype, Self.simd_dims], dims: CoordDims = CoordDims.Point 95 | ) -> Self: 96 | """ 97 | Create Point from existing SIMD vector of coordinates. 98 | """ 99 | return Self {coords: coords, ogc_dims: dims} 100 | 101 | # 102 | # Static constructor methods. 103 | # 104 | @staticmethod 105 | fn from_json(json_dict: PythonObject) raises -> Self: 106 | # TODO: type checking of json_dict 107 | # TODO: bounds checking of coords_len 108 | let json_coords = json_dict["coordinates"] 109 | let coords_len = int(json_coords.__len__()) 110 | var result = Self() 111 | for i in range(coords_len): 112 | result.coords[i] = json_coords[i].to_float64().cast[dtype]() 113 | return result 114 | 115 | @staticmethod 116 | fn from_json(json_str: String) raises -> Self: 117 | let json_dict = JSONParser.parse(json_str) 118 | return Self.from_json(json_dict) 119 | 120 | @staticmethod 121 | fn from_wkt(wkt: String) raises -> Self: 122 | var result = Self() 123 | let geos_pt = WKTParser.parse(wkt) 124 | let coords_tuple = geos_pt.coords[0] 125 | let coords_len = coords_tuple.__len__().to_float64().to_int() 126 | for i in range(coords_len): 127 | result.coords[i] = coords_tuple[i].to_float64().cast[dtype]() 128 | return result 129 | 130 | @staticmethod 131 | fn from_geoarrow(table: PythonObject) raises -> Self: 132 | let ga = Python.import_module("geoarrow.pyarrow") 133 | let geoarrow = ga.as_geoarrow(table["geometry"]) 134 | let chunk = geoarrow[0] 135 | let n = chunk.value.__len__() 136 | if n > Self.simd_dims: 137 | raise Error("Invalid Point dims parameter vs. geoarrow: " + str(n)) 138 | var result = Self() 139 | for dim in range(n): 140 | let val = chunk.value[dim].as_py().to_float64().cast[dtype]() 141 | result.coords[dim] = val 142 | return result 143 | 144 | @staticmethod 145 | fn empty(dims: CoordDims = CoordDims.Point) -> Self: 146 | """ 147 | Emptyable trait. 148 | """ 149 | return Self.__init__(dims) 150 | 151 | # 152 | # Getters/Setters 153 | # 154 | fn set_ogc_dims(inout self, ogc_dims: CoordDims): 155 | """ 156 | Setter for ogc_dims enum. May be only be useful if the Point constructor with variadic list of coordinate values. 157 | (ex: when Point Z vs Point M is ambiguous. 158 | """ 159 | debug_assert( 160 | len(self.ogc_dims) == len(ogc_dims), "Unsafe change of dimension number" 161 | ) 162 | self.ogc_dims = ogc_dims 163 | 164 | fn dims(self) -> Int: 165 | return len(self.ogc_dims) 166 | 167 | fn has_height(self) -> Bool: 168 | return (self.ogc_dims == CoordDims.PointZ) or ( 169 | self.ogc_dims == CoordDims.PointZM 170 | ) 171 | 172 | fn has_measure(self) -> Bool: 173 | return (self.ogc_dims == CoordDims.PointM) or ( 174 | self.ogc_dims == CoordDims.PointZM 175 | ) 176 | 177 | fn is_empty(self) -> Bool: 178 | return is_empty[dtype](self.coords) 179 | 180 | @always_inline 181 | fn x(self) -> SIMD[dtype, 1]: 182 | """ 183 | Get the x value (0 index). 184 | """ 185 | return self.coords[self.x_index] 186 | 187 | @always_inline 188 | fn y(self) -> SIMD[dtype, 1]: 189 | """ 190 | Get the y value (1 index). 191 | """ 192 | return self.coords[self.y_index] 193 | 194 | @always_inline 195 | fn z(self) -> SIMD[dtype, 1]: 196 | """ 197 | Get the z or altitude value (2 index). 198 | """ 199 | return self.coords[self.z_index] 200 | 201 | @always_inline 202 | fn alt(self) -> SIMD[dtype, 1]: 203 | """ 204 | Get the z or altitude value (2 index). 205 | """ 206 | return self.z() 207 | 208 | @always_inline 209 | fn m(self) -> SIMD[dtype, 1]: 210 | """ 211 | Get the measure value (3 index). 212 | """ 213 | return self.coords[self.m_index] 214 | 215 | fn envelope(self) -> Envelope[dtype]: 216 | return Envelope[dtype](self) 217 | 218 | fn __len__(self) -> Int: 219 | """ 220 | Returns the number of non-empty dimensions. 221 | """ 222 | return self.dims() 223 | 224 | fn __getitem__(self, d: Int) -> SIMD[dtype, 1]: 225 | """ 226 | Get the value of coordinate at this dimension. 227 | """ 228 | return self.coords[d] if d < Self.simd_dims else empty_value[dtype]() 229 | 230 | fn __eq__(self, other: Self) -> Bool: 231 | # NaN is used as empty value, so here cannot simply compare with __eq__ on the SIMD values. 232 | @unroll 233 | for i in range(Self.simd_dims): 234 | if is_empty(self.coords[i]) and is_empty(other.coords[i]): 235 | pass # equality at index i 236 | else: 237 | if is_empty(self.coords[i]) or is_empty(other.coords[i]): 238 | return False # early out: one or the other is empty (but not both) -> not equal 239 | if self.coords[i] != other.coords[i]: 240 | return False # not equal 241 | return True # equal 242 | 243 | fn __ne__(self, other: Self) -> Bool: 244 | return not self.__eq__(other) 245 | 246 | fn __repr__(self) -> String: 247 | let point_variant = str(self.ogc_dims) 248 | var res = point_variant + " [" + dtype.__str__() + "](" 249 | for i in range(Self.simd_dims): 250 | res += str(self.coords[i]) 251 | if i < Self.simd_dims - 1: 252 | res += ", " 253 | res += ")" 254 | return res 255 | 256 | fn __str__(self) -> String: 257 | return self.__repr__() 258 | 259 | fn json(self) raises -> String: 260 | if self.ogc_dims.value > CoordDims.PointZ.value: 261 | raise Error( 262 | "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" 263 | ) 264 | 265 | # include only x, y, and optionally z (height) 266 | var res = String('{"type":"Point","coordinates":[') 267 | let dims = 3 if self.has_height() else 2 268 | for i in range(dims): 269 | if i > 3: 270 | break 271 | res += self.coords[i] 272 | if i < dims - 1: 273 | res += "," 274 | res += "]}" 275 | return res 276 | 277 | fn wkt(self) -> String: 278 | if self.is_empty(): 279 | return "POINT EMPTY" 280 | var result = str(self.ogc_dims) + " (" 281 | result += str(self.x()) + " " + str(self.y()) 282 | if self.ogc_dims == CoordDims.PointZ or self.ogc_dims == CoordDims.PointZM: 283 | result += " " + str(self.z()) 284 | if self.ogc_dims == CoordDims.PointM or self.ogc_dims == CoordDims.PointZM: 285 | result += " " + str(self.m()) 286 | result += ")" 287 | return result 288 | 289 | fn geoarrow(self) -> PythonObject: 290 | # TODO: geoarrow() 291 | return None 292 | -------------------------------------------------------------------------------- /mogeo/test/geom/test_point.mojo: -------------------------------------------------------------------------------- 1 | from python import Python 2 | from python.object import PythonObject 3 | from pathlib import Path 4 | 5 | from mogeo.geom.empty import empty_value, is_empty 6 | from mogeo.geom.point import Point, CoordDims 7 | from mogeo.geom.traits import Dimensionable, Geometric, Emptyable 8 | from mogeo.test.helpers import load_geoarrow_test_fixture 9 | from mogeo.test.pytest import MojoTest 10 | from mogeo.test.constants import lon, lat, height, measure 11 | 12 | 13 | fn main() raises: 14 | test_constructors() 15 | test_repr() 16 | test_equality_ops() 17 | test_getters() 18 | test_setters() 19 | test_sized() 20 | test_stringable() 21 | test_dimensionable() 22 | test_geometric() 23 | test_emptyable() 24 | test_stringable() 25 | test_wktable() 26 | test_jsonable() 27 | test_geoarrowable() 28 | 29 | 30 | fn test_constructors(): 31 | let test = MojoTest("constructors") 32 | 33 | _ = Point() 34 | _ = Point(lon, lat) 35 | _ = Point(lon, lat, height) 36 | _ = Point(lon, lat, measure) 37 | _ = Point(lon, lat, height, measure) 38 | _ = Point[DType.int32]() 39 | _ = Point[DType.float32]() 40 | _ = Point[DType.int32](lon, lat) 41 | _ = Point[DType.float32](lon, lat) 42 | _ = Point[dtype = DType.float16](SIMD[DType.float16, 4](lon, lat, height, measure)) 43 | _ = Point[dtype = DType.float32](SIMD[DType.float32, 4](lon, lat, height, measure)) 44 | 45 | 46 | fn test_repr() raises: 47 | let test = MojoTest("repr") 48 | 49 | let pt = Point(lon, lat) 50 | test.assert_true( 51 | pt.__repr__() 52 | == "Point [float64](-108.68000000000001, 38.973999999999997, nan, nan)", 53 | "repr", 54 | ) 55 | 56 | let pt_z = Point(lon, lat, height) 57 | test.assert_true( 58 | pt_z.__repr__() 59 | == "Point Z [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", 60 | "repr", 61 | ) 62 | 63 | # the variadic list constructor cannot distinguish Point Z from Point M, so use the set_ogc_dims method. 64 | var pt_m = pt_z 65 | pt_m.set_ogc_dims(CoordDims.PointM) 66 | test.assert_true( 67 | pt_m.__repr__() 68 | == "Point M [float64](-108.68000000000001, 38.973999999999997, 8.0, 8.0)", 69 | "repr", 70 | ) 71 | 72 | let pt_zm = Point(lon, lat, height, measure) 73 | test.assert_true( 74 | pt_zm.__repr__() 75 | == "Point ZM [float64](-108.68000000000001, 38.973999999999997, 8.0, 42.0)", 76 | "repr", 77 | ) 78 | 79 | 80 | fn test_stringable() raises: 81 | let test = MojoTest("stringable") 82 | let pt_z = Point(lon, lat, height) 83 | test.assert_true(str(pt_z) == pt_z.__repr__(), "__str__") 84 | 85 | 86 | fn test_sized() raises: 87 | let test = MojoTest("sized") 88 | let pt_z = Point(lon, lat, height) 89 | test.assert_true(len(pt_z) == 3, "__len__") 90 | 91 | 92 | fn test_dimensionable() raises: 93 | let pt = Point(lon, lat) 94 | let pt_z = Point(lon, lat, height) 95 | var pt_m = Point(lon, lat, measure) 96 | let pt_zm = Point(lon, lat, height, measure) 97 | 98 | var test = MojoTest("dims") 99 | test.assert_true(pt.dims() == 2, "dims") 100 | test.assert_true(pt_z.dims() == 3, "dims") 101 | test.assert_true(pt_m.dims() == 3, "dims") 102 | test.assert_true(pt_zm.dims() == 4, "dims") 103 | 104 | test = MojoTest("has_height") 105 | test.assert_true(pt_z.has_height(), "has_height") 106 | 107 | test = MojoTest("has_measure") 108 | test.assert_true(not pt_m.has_measure(), "has_measure") 109 | pt_m.set_ogc_dims(CoordDims.PointM) 110 | test.assert_true(pt_m.has_measure(), "has_measure") 111 | 112 | 113 | fn test_geometric() raises: 114 | let test = MojoTest("geometric") 115 | 116 | 117 | fn test_emptyable() raises: 118 | let test = MojoTest("emptyable") 119 | let pt_e = Point.empty() 120 | test.assert_true(is_empty(pt_e.x()), "empty") 121 | test.assert_true(is_empty(pt_e.y()), "empty") 122 | test.assert_true(is_empty(pt_e.z()), "empty") 123 | test.assert_true(is_empty(pt_e.m()), "empty") 124 | test.assert_true(is_empty(pt_e.coords), "empty") 125 | 126 | 127 | fn test_empty_default_values() raises: 128 | let test = MojoTest("empty default/padding values") 129 | 130 | let pt_4 = Point(lon, lat) 131 | let expect_value = empty_value[pt_4.dtype]() 132 | test.assert_true(pt_4.coords[2] == expect_value, "NaN expected") 133 | test.assert_true(pt_4.coords[3] == expect_value, "NaN expected") 134 | 135 | let pt_4_int = Point[DType.uint16](lon, lat) 136 | let expect_value_int = empty_value[pt_4_int.dtype]() 137 | test.assert_true(pt_4_int.coords[2] == expect_value_int, "max_finite expected") 138 | test.assert_true(pt_4_int.coords[3] == expect_value_int, "max_finite expected") 139 | 140 | 141 | fn test_equality_ops() raises: 142 | let test = MojoTest("equality operators") 143 | 144 | let p2a = Point(lon, lat) 145 | let p2b = Point(lon, lat) 146 | test.assert_true(p2a == p2b, "__eq__") 147 | 148 | let p2i = Point[DType.int16](lon, lat) 149 | let p2ib = Point[DType.int16](lon, lat) 150 | test.assert_true(p2i == p2ib, "__eq__") 151 | 152 | let p2ic = Point[DType.int16](lon + 1, lat) 153 | test.assert_true(p2i != p2ic, "__ne_") 154 | 155 | let p4 = Point(lon, lat, height, measure) 156 | let p4a = Point(lon, lat, height, measure) 157 | let p4b = Point(lon + 0.001, lat, height, measure) 158 | test.assert_true(p4 == p4a, "__eq__") 159 | test.assert_true(p4 != p4b, "__eq__") 160 | 161 | 162 | fn test_is_empty() raises: 163 | let test = MojoTest("is_empty") 164 | 165 | let pt2 = Point() 166 | test.assert_true(pt2.is_empty(), "is_empty") 167 | 168 | let pti = Point[DType.int8]() 169 | test.assert_true(pti.is_empty(), "is_empty") 170 | 171 | let pt_z = Point[DType.int8](CoordDims.PointZ) 172 | test.assert_true(pt_z.is_empty(), "is_empty") 173 | 174 | let pt_m = Point[DType.int8](CoordDims.PointM) 175 | test.assert_true(pt_m.is_empty(), "is_empty") 176 | 177 | let pt_zm = Point[DType.int8](CoordDims.PointZM) 178 | test.assert_true(pt_zm.is_empty(), "is_empty") 179 | 180 | 181 | fn test_getters() raises: 182 | let test = MojoTest("getters") 183 | 184 | let pt2 = Point(lon, lat) 185 | test.assert_true(pt2.x() == lon, "p2.x() == lon") 186 | test.assert_true(pt2.y() == lat, "p2.y() == lat") 187 | 188 | let pt_z = Point(lon, lat, height) 189 | test.assert_true(pt_z.x() == lon, "pt_z.x() == lon") 190 | test.assert_true(pt_z.y() == lat, "pt_z.y() == lat") 191 | test.assert_true(pt_z.z() == height, "pt_z.z() == height") 192 | 193 | let pt_m = Point(lon, lat, measure) 194 | test.assert_true(pt_m.x() == lon, "pt_m.x() == lon") 195 | test.assert_true(pt_m.y() == lat, "pt_m.y() == lat") 196 | test.assert_true(pt_m.m() == measure, "pt_m.m() == measure") 197 | 198 | let point_zm = Point(lon, lat, height, measure) 199 | test.assert_true(point_zm.x() == lon, "point_zm.x() == lon") 200 | test.assert_true(point_zm.y() == lat, "point_zm.y() == lat") 201 | test.assert_true(point_zm.z() == height, "point_zm.z() == height") 202 | test.assert_true(point_zm.m() == measure, "point_zm.m() == measure") 203 | 204 | 205 | fn test_setters() raises: 206 | let test = MojoTest("setters") 207 | 208 | var pt = Point(lon, lat, measure) 209 | pt.set_ogc_dims(CoordDims.PointM) 210 | test.assert_true(pt.ogc_dims == CoordDims.PointM, "set_ogc_dims") 211 | 212 | 213 | fn test_jsonable() raises: 214 | test_json() 215 | test_from_json() 216 | 217 | 218 | fn test_json() raises: 219 | let test = MojoTest("json") 220 | 221 | let pt2 = Point(lon, lat) 222 | test.assert_true( 223 | pt2.json() 224 | == '{"type":"Point","coordinates":[-108.68000000000001,38.973999999999997]}', 225 | "json()", 226 | ) 227 | let pt3 = Point(lon, lat, height) 228 | test.assert_true( 229 | pt3.json() 230 | == '{"type":"Point","coordinates":[-108.68000000000001,38.973999999999997,8.0]}', 231 | "json()", 232 | ) 233 | 234 | let expect_error = "GeoJSON only allows dimensions X, Y, and optionally Z (RFC 7946)" 235 | var pt_m = Point(lon, lat, measure) 236 | pt_m.set_ogc_dims(CoordDims.PointM) 237 | try: 238 | _ = pt_m.json() 239 | except e: 240 | test.assert_true(str(e) == expect_error, "json raises") 241 | 242 | let pt4 = Point(lon, lat, height, measure) 243 | try: 244 | _ = pt4.json() 245 | except e: 246 | test.assert_true(str(e) == expect_error, "json raises") 247 | 248 | 249 | fn test_from_json() raises: 250 | let test = MojoTest("from_json") 251 | 252 | let orjson = Python.import_module("orjson") 253 | let json_str = String('{"type":"Point","coordinates":[102.001, 3.502]}') 254 | let json_dict = orjson.loads(json_str) 255 | 256 | let pt2 = Point.from_json(json_dict) 257 | test.assert_true(pt2.x() == 102.001, "pt2.x()") 258 | test.assert_true(pt2.y() == 3.502, "pt2.y()") 259 | 260 | let ptz = Point.from_json(json_dict) 261 | test.assert_true(ptz.x() == 102.001, "ptz.x()") 262 | test.assert_true(ptz.y() == 3.502, "ptz.y()") 263 | 264 | let pt_f32 = Point[dtype = DType.float32].from_json(json_str) 265 | test.assert_true(pt_f32.x() == 102.001, "pt_f32.x()") 266 | test.assert_true(pt_f32.y() == 3.502, "pt_f32.y()") 267 | 268 | let pt_int = Point[dtype = DType.uint8].from_json(json_dict) 269 | test.assert_true(pt_int.x() == 102, "pt_int.x()") 270 | test.assert_true(pt_int.y() == 3, "pt_int.y()") 271 | 272 | 273 | fn test_wktable() raises: 274 | test_wkt() 275 | test_from_wkt() 276 | 277 | 278 | fn test_wkt() raises: 279 | let test = MojoTest("wkt") 280 | 281 | let pt = Point(lon, lat) 282 | test.assert_true( 283 | pt.wkt() == "Point (-108.68000000000001 38.973999999999997)", "wkt" 284 | ) 285 | 286 | let pt_z = Point(lon, lat, height) 287 | test.assert_true( 288 | pt_z.wkt() == "Point Z (-108.68000000000001 38.973999999999997 8.0)", "wkt" 289 | ) 290 | 291 | var pt_m = Point(lon, lat, measure) 292 | pt_m.set_ogc_dims(CoordDims.PointM) 293 | test.assert_true( 294 | pt_m.wkt() == "Point M (-108.68000000000001 38.973999999999997 42.0)", "wkt" 295 | ) 296 | 297 | let pt_zm = Point(lon, lat, height, measure) 298 | test.assert_true( 299 | pt_zm.wkt() == "Point ZM (-108.68000000000001 38.973999999999997 8.0 42.0)", 300 | "wkt", 301 | ) 302 | 303 | let p2i = Point[DType.int32](lon, lat) 304 | test.assert_true(p2i.wkt() == "Point (-108 38)", "wkt") 305 | 306 | 307 | fn test_from_wkt() raises: 308 | let test = MojoTest("from_wkt") 309 | 310 | let path = Path("mogeo/test/fixtures/wkt/point/point.wkt") 311 | let wkt: String 312 | with open(path, "rb") as f: 313 | wkt = f.read() 314 | 315 | let expect_x = -108.68000000000001 316 | let expect_y = 38.973999999999997 317 | try: 318 | let point_2d = Point.from_wkt(wkt) 319 | test.assert_true(point_2d.x() == expect_x, "point_2d.x()") 320 | test.assert_true(point_2d.y() == expect_y, "point_2d.y()") 321 | 322 | let point_3d = Point.from_wkt(wkt) 323 | test.assert_true( 324 | point_3d.__repr__() 325 | == "Point [float64](-108.68000000000001, 38.973999999999997, nan, nan)", 326 | "from_wkt", 327 | ) 328 | 329 | let point_2d_u8 = Point[DType.uint8].from_wkt(wkt) 330 | test.assert_true( 331 | point_2d_u8.__repr__() == "Point [uint8](148, 38, 255, 255)", "from_wkt())" 332 | ) 333 | 334 | let point_2d_f32 = Point[DType.float32].from_wkt(wkt) 335 | test.assert_true( 336 | point_2d_f32.__repr__() 337 | == "Point [float32](-108.68000030517578, 38.9739990234375, nan, nan)", 338 | "from_wkt", 339 | ) 340 | except: 341 | raise Error( 342 | "from_wkt(): Maybe failed to import_module of shapely? check venv's install" 343 | " packages." 344 | ) 345 | 346 | 347 | fn test_geoarrowable() raises: 348 | # TODO test_geoarrow() 349 | test_from_geoarrow() 350 | 351 | 352 | # fn test_geoarrow() raises: 353 | # let test = MojoTest("geoarrow") 354 | 355 | 356 | fn test_from_geoarrow() raises: 357 | let test = MojoTest("from_geoarrow") 358 | 359 | let ga = Python.import_module("geoarrow.pyarrow") 360 | let path = Path("mogeo/test/fixtures/geoarrow/geoarrow-data/example") 361 | let empty = empty_value[DType.float64]() 362 | var file = path / "example-point.arrow" 363 | var table = load_geoarrow_test_fixture(file) 364 | var geoarrow = ga.as_geoarrow(table["geometry"]) 365 | var chunk = geoarrow[0] 366 | let point_2d = Point.from_geoarrow(table) 367 | let expect_point_2d = Point( 368 | SIMD[point_2d.dtype, point_2d.simd_dims](30.0, 10.0, empty, empty) 369 | ) 370 | test.assert_true(point_2d == expect_point_2d, "expect_coords_2d") 371 | 372 | file = path / "example-point_z.arrow" 373 | table = load_geoarrow_test_fixture(file) 374 | geoarrow = ga.as_geoarrow(table["geometry"]) 375 | chunk = geoarrow[0] 376 | # print(chunk.wkt) 377 | let point_3d = Point.from_geoarrow(table) 378 | let expect_point_3d = Point( 379 | SIMD[point_3d.dtype, point_3d.simd_dims]( 380 | 30.0, 10.0, 40.0, empty_value[point_3d.dtype]() 381 | ) 382 | ) 383 | for i in range(3): 384 | # cannot check the nan for equality 385 | test.assert_true(point_3d == expect_point_3d, "expect_point_3d") 386 | 387 | file = path / "example-point_zm.arrow" 388 | table = load_geoarrow_test_fixture(file) 389 | geoarrow = ga.as_geoarrow(table["geometry"]) 390 | chunk = geoarrow[0] 391 | # print(chunk.wkt) 392 | let point_4d = Point.from_geoarrow(table) 393 | let expect_point_4d = Point( 394 | SIMD[point_4d.dtype, point_4d.simd_dims](30.0, 10.0, 40.0, 300.0) 395 | ) 396 | test.assert_true(point_4d == expect_point_4d, "expect_point_4d") 397 | 398 | file = path / "example-point_m.arrow" 399 | table = load_geoarrow_test_fixture(file) 400 | geoarrow = ga.as_geoarrow(table["geometry"]) 401 | chunk = geoarrow[0] 402 | # print(chunk.wkt) 403 | let point_m = Point.from_geoarrow(table) 404 | let expect_coords_m = SIMD[point_m.dtype, point_m.simd_dims]( 405 | 30.0, 10.0, 300.0, empty_value[point_m.dtype]() 406 | ) 407 | for i in range(3): # cannot equality check the NaN 408 | test.assert_true(point_m.coords[i] == expect_coords_m[i], "expect_coords_m") 409 | test.assert_true(is_empty(point_m.coords[3]), "expect_coords_m") 410 | --------------------------------------------------------------------------------