geo-micro 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geo_micro-0.1.0/LICENSE +21 -0
- geo_micro-0.1.0/PKG-INFO +46 -0
- geo_micro-0.1.0/README.md +31 -0
- geo_micro-0.1.0/geomicro/__init__.py +24 -0
- geo_micro-0.1.0/geomicro/core.py +168 -0
- geo_micro-0.1.0/pyproject.toml +28 -0
geo_micro-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Coby Williams
|
|
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.
|
geo_micro-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geo-micro
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A zero-dependency micro-utility for fast GeoJSON sanitisation and spatial checks.
|
|
5
|
+
Author-email: Coby Williams <cobyw16@hotmail.com>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Project-URL: Homepage, https://github.com/LionEmpire/geo-micro
|
|
14
|
+
|
|
15
|
+
# geo-micro
|
|
16
|
+
|
|
17
|
+
A zero-dependency, ultra-lightweight (<10KB) Python utility for GeoJSON sanitisation, coordinate order correction, bounding box calculations, and fast ray-casted point-in-polygon checks.
|
|
18
|
+
Designed for AWS Lambda functions, microservices, and edge computing environments where heavy C-based libraries like Shapely or GDAL are overkill.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- Zero External Dependencies
|
|
23
|
+
- Dirty Data Repair (string coordinates to floats and auto-closes unclosed polygon rings)
|
|
24
|
+
- Coordinate Swapper: (`[Lat, Lng]` to `[Lng, Lat]` and vice versa)
|
|
25
|
+
- Point-in-Polygon
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from geomicro import GeoMicro, convert_coords
|
|
31
|
+
|
|
32
|
+
# 1. Fix Lat/Lng order for a list of coordinates
|
|
33
|
+
bad_coords = [[-27.468, 153.028], [-27.470, 153.030]]
|
|
34
|
+
fixed = convert_coords(bad_coords, mode="to_geojson")
|
|
35
|
+
|
|
36
|
+
# 2. Check containment and bounding boxes
|
|
37
|
+
polygon_feature = {
|
|
38
|
+
"type": "Polygon",
|
|
39
|
+
"coordinates": [[[153.0, -27.0], [154.0, -27.0], [154.0, -28.0], [153.0, -27.0]]]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
geo = GeoMicro(polygon_feature)
|
|
43
|
+
print(geo.bbox()) # (153.0, -28.0, 154.0, -27.0)
|
|
44
|
+
print(geo.contains([153.5, -27.5])) # True
|
|
45
|
+
```
|
|
46
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# geo-micro
|
|
2
|
+
|
|
3
|
+
A zero-dependency, ultra-lightweight (<10KB) Python utility for GeoJSON sanitisation, coordinate order correction, bounding box calculations, and fast ray-casted point-in-polygon checks.
|
|
4
|
+
Designed for AWS Lambda functions, microservices, and edge computing environments where heavy C-based libraries like Shapely or GDAL are overkill.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- Zero External Dependencies
|
|
9
|
+
- Dirty Data Repair (string coordinates to floats and auto-closes unclosed polygon rings)
|
|
10
|
+
- Coordinate Swapper: (`[Lat, Lng]` to `[Lng, Lat]` and vice versa)
|
|
11
|
+
- Point-in-Polygon
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from geomicro import GeoMicro, convert_coords
|
|
17
|
+
|
|
18
|
+
# 1. Fix Lat/Lng order for a list of coordinates
|
|
19
|
+
bad_coords = [[-27.468, 153.028], [-27.470, 153.030]]
|
|
20
|
+
fixed = convert_coords(bad_coords, mode="to_geojson")
|
|
21
|
+
|
|
22
|
+
# 2. Check containment and bounding boxes
|
|
23
|
+
polygon_feature = {
|
|
24
|
+
"type": "Polygon",
|
|
25
|
+
"coordinates": [[[153.0, -27.0], [154.0, -27.0], [154.0, -28.0], [153.0, -27.0]]]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
geo = GeoMicro(polygon_feature)
|
|
29
|
+
print(geo.bbox()) # (153.0, -28.0, 154.0, -27.0)
|
|
30
|
+
print(geo.contains([153.5, -27.5])) # True
|
|
31
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
sanitize_coord,
|
|
3
|
+
swap_lat_lng,
|
|
4
|
+
lat_lng_to_geojson,
|
|
5
|
+
geojson_to_lat_lng,
|
|
6
|
+
convert_coords,
|
|
7
|
+
auto_repair_ring,
|
|
8
|
+
get_bbox,
|
|
9
|
+
point_in_polygon,
|
|
10
|
+
GeoMicro,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
__all__ = [
|
|
15
|
+
"sanitize_coord",
|
|
16
|
+
"swap_lat_lng",
|
|
17
|
+
"lat_lng_to_geojson",
|
|
18
|
+
"geojson_to_lat_lng",
|
|
19
|
+
"convert_coords",
|
|
20
|
+
"auto_repair_ring",
|
|
21
|
+
"get_bbox",
|
|
22
|
+
"point_in_polygon",
|
|
23
|
+
"GeoMicro",
|
|
24
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
geo-micro: Zero-dependency GeoJSON micro-utility.
|
|
3
|
+
Lightweight, pure-Python geometry sanitisation and spatial checks.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Tuple, Union, Dict, Any
|
|
7
|
+
|
|
8
|
+
PointType = Union[List[float], Tuple[float, float]]
|
|
9
|
+
BoundingBox = Tuple[float, float, float, float] # (min_lng, min_lat, max_lng, max_lat)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def sanitize_coord(coord: Any) -> Tuple[float, float]:
|
|
13
|
+
"""Ensures coordinates are valid floats."""
|
|
14
|
+
try:
|
|
15
|
+
lng = float(coord[0])
|
|
16
|
+
lat = float(coord[1])
|
|
17
|
+
return lng, lat
|
|
18
|
+
except (IndexError, TypeError, ValueError) as e:
|
|
19
|
+
raise ValueError(f"Invalid coordinate format: {coord}") from e
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def lat_lng_to_geojson(point: PointType) -> Tuple[float, float]:
|
|
23
|
+
"""
|
|
24
|
+
Converts a single (Latitude, Longitude) pair into standard GeoJSON (Longitude, Latitude).
|
|
25
|
+
Example: (-27.468, 153.028) -> (153.028, -27.468)
|
|
26
|
+
"""
|
|
27
|
+
lat, lng = sanitize_coord(point)
|
|
28
|
+
return lng, lat
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def geojson_to_lat_lng(point: PointType) -> Tuple[float, float]:
|
|
32
|
+
"""
|
|
33
|
+
Converts a single GeoJSON (Longitude, Latitude) pair into standard (Latitude, Longitude).
|
|
34
|
+
Example: (153.028, -27.468) -> (-27.468, 153.028)
|
|
35
|
+
"""
|
|
36
|
+
lng, lat = sanitize_coord(point)
|
|
37
|
+
return lat, lng
|
|
38
|
+
|
|
39
|
+
def swap_lat_lng(coord: PointType) -> Tuple[float, float]:
|
|
40
|
+
"""Swaps a (Lat, Lng) pair to (Lng, Lat) or vice versa."""
|
|
41
|
+
a, b = sanitize_coord(coord)
|
|
42
|
+
return b, a
|
|
43
|
+
|
|
44
|
+
def convert_coords(coords: List[Any], mode: str = "to_geojson") -> List[Any]:
|
|
45
|
+
"""
|
|
46
|
+
Recursively converts coordinates across nested structures (arrays, rings, polygons).
|
|
47
|
+
|
|
48
|
+
:param coords: List of coordinate pairs or nested lists.
|
|
49
|
+
:param mode: 'to_geojson' (swaps Lat/Lng -> Lng/Lat) or 'to_latlng' (swaps Lng/Lat -> Lat/Lng).
|
|
50
|
+
"""
|
|
51
|
+
if not coords:
|
|
52
|
+
return []
|
|
53
|
+
if isinstance(coords[0], (int, float, str)):
|
|
54
|
+
if mode == "to_geojson":
|
|
55
|
+
lng, lat = lat_lng_to_geojson(coords)
|
|
56
|
+
return [lng, lat]
|
|
57
|
+
elif mode == "to_latlng":
|
|
58
|
+
lat, lng = geojson_to_lat_lng(coords)
|
|
59
|
+
return [lat, lng]
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"Invalid mode: '{mode}'. Use 'to_geojson' or 'to_latlng'.")
|
|
62
|
+
|
|
63
|
+
return [convert_coords(c, mode=mode) for c in coords]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def auto_repair_ring(ring: List[PointType]) -> List[Tuple[float, float]]:
|
|
67
|
+
"""
|
|
68
|
+
Cleans ring coordinates: casts string values to floats and ensures
|
|
69
|
+
the polygon ring is closed (first point equals last point).
|
|
70
|
+
"""
|
|
71
|
+
if len(ring) < 3:
|
|
72
|
+
raise ValueError("A polygon ring must contain at least 3 points.")
|
|
73
|
+
|
|
74
|
+
clean_ring = [sanitize_coord(p) for p in ring]
|
|
75
|
+
|
|
76
|
+
# Close ring if open
|
|
77
|
+
if clean_ring[0] != clean_ring[-1]:
|
|
78
|
+
clean_ring.append(clean_ring[0])
|
|
79
|
+
|
|
80
|
+
return clean_ring
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_bbox(geometry_or_geojson: Dict[str, Any]) -> BoundingBox:
|
|
84
|
+
"""
|
|
85
|
+
Computes a (min_lng, min_lat, max_lng, max_lat) bounding box for any
|
|
86
|
+
supported GeoJSON object (Point, LineString, Polygon, Feature, FeatureCollection).
|
|
87
|
+
"""
|
|
88
|
+
coords: List[Tuple[float, float]] = []
|
|
89
|
+
|
|
90
|
+
def _extract_coords(obj: Any):
|
|
91
|
+
if isinstance(obj, dict):
|
|
92
|
+
if obj.get("type") == "FeatureCollection":
|
|
93
|
+
for feat in obj.get("features", []):
|
|
94
|
+
_extract_coords(feat)
|
|
95
|
+
elif obj.get("type") == "Feature":
|
|
96
|
+
_extract_coords(obj.get("geometry", {}))
|
|
97
|
+
elif "coordinates" in obj:
|
|
98
|
+
_extract_coords(obj["coordinates"])
|
|
99
|
+
elif isinstance(obj, list):
|
|
100
|
+
if len(obj) >= 2 and isinstance(obj[0], (int, float, str)) and isinstance(obj[1], (int, float, str)):
|
|
101
|
+
coords.append(sanitize_coord(obj))
|
|
102
|
+
else:
|
|
103
|
+
for sub in obj:
|
|
104
|
+
_extract_coords(sub)
|
|
105
|
+
|
|
106
|
+
_extract_coords(geometry_or_geojson)
|
|
107
|
+
|
|
108
|
+
if not coords:
|
|
109
|
+
raise ValueError("No valid coordinates found to calculate bounding box.")
|
|
110
|
+
|
|
111
|
+
lngs = [c[0] for c in coords]
|
|
112
|
+
lats = [c[1] for c in coords]
|
|
113
|
+
|
|
114
|
+
return min(lngs), min(lats), max(lngs), max(lats)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def point_in_polygon(point: PointType, polygon_ring: List[PointType]) -> bool:
|
|
118
|
+
"""
|
|
119
|
+
Determines if a point is inside a polygon ring using the Ray-Casting algorithm.
|
|
120
|
+
Expects coordinates in standard GeoJSON [Lng, Lat] format.
|
|
121
|
+
"""
|
|
122
|
+
px, py = sanitize_coord(point)
|
|
123
|
+
ring = auto_repair_ring(polygon_ring)
|
|
124
|
+
|
|
125
|
+
inside = False
|
|
126
|
+
n = len(ring)
|
|
127
|
+
|
|
128
|
+
j = n - 1
|
|
129
|
+
for i in range(n):
|
|
130
|
+
xi, yi = ring[i]
|
|
131
|
+
xj, yj = ring[j]
|
|
132
|
+
|
|
133
|
+
# Ray-casting intersection check
|
|
134
|
+
intersect = ((yi > py) != (yj > py)) and (px < (xj - xi) * (py - yi) / (yj - yi + 1e-12) + xi)
|
|
135
|
+
if intersect:
|
|
136
|
+
inside = not inside
|
|
137
|
+
j = i
|
|
138
|
+
|
|
139
|
+
return inside
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class GeoMicro:
|
|
143
|
+
"""Convenience wrapper class for fluent chaining."""
|
|
144
|
+
|
|
145
|
+
def __init__(self, geojson: Dict[str, Any]):
|
|
146
|
+
self.geojson = geojson
|
|
147
|
+
|
|
148
|
+
def bbox(self) -> BoundingBox:
|
|
149
|
+
return get_bbox(self.geojson)
|
|
150
|
+
|
|
151
|
+
def contains(self, point: PointType) -> bool:
|
|
152
|
+
geom_type = self.geojson.get("type")
|
|
153
|
+
|
|
154
|
+
if geom_type == "Polygon":
|
|
155
|
+
coords = self.geojson.get("coordinates", [])
|
|
156
|
+
elif geom_type == "Feature":
|
|
157
|
+
coords = self.geojson.get("geometry", {}).get("coordinates", [])
|
|
158
|
+
else:
|
|
159
|
+
raise NotImplementedError(f"Point containment for type '{geom_type}' is not directly supported.")
|
|
160
|
+
|
|
161
|
+
if not coords:
|
|
162
|
+
raise ValueError("No coordinates found in GeoJSON object.")
|
|
163
|
+
|
|
164
|
+
outer_ring = coords[0]
|
|
165
|
+
if isinstance(outer_ring, list) and len(outer_ring) > 0 and isinstance(outer_ring[0], list) and isinstance(outer_ring[0][0], (list, tuple)):
|
|
166
|
+
outer_ring = outer_ring[0]
|
|
167
|
+
|
|
168
|
+
return point_in_polygon(point, outer_ring)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=3.2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "geo-micro"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A zero-dependency micro-utility for fast GeoJSON sanitisation and spatial checks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "Coby Williams", email = "cobyw16@hotmail.com" }]
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
17
|
+
]
|
|
18
|
+
requires-python = ">=3.8"
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/LionEmpire/geo-micro"
|
|
22
|
+
|
|
23
|
+
[tool.pytest.ini_options]
|
|
24
|
+
pythonpath = "."
|
|
25
|
+
testpaths = ["tests"]
|
|
26
|
+
|
|
27
|
+
[tool.flit.module]
|
|
28
|
+
name = "geomicro"
|