geomask 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.

Potentially problematic release.


This version of geomask might be problematic. Click here for more details.

geomask-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Henrique Campiotti
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.
geomask-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.3
2
+ Name: geomask
3
+ Version: 0.1.0
4
+ Summary: A very simple lib for creating geometric masks from spatial data using regular grids
5
+ License: MIT
6
+ Keywords: gis,spatial,grid,geometry,mask
7
+ Author: campiohe
8
+ Author-email: campiottihenrique@gmail.com
9
+ Requires-Python: >=3.11
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Scientific/Engineering :: GIS
17
+ Requires-Dist: shapely (>=2.1.1,<3.0.0)
18
+ Project-URL: Homepage, https://github.com/campiohe/geomask
19
+ Project-URL: Issues, https://github.com/campiohe/geomask/issues
20
+ Project-URL: Repository, https://github.com/campiohe/geomask
21
+ Description-Content-Type: text/markdown
22
+
23
+ # GeoMask
24
+
25
+ A very simple lib for creating geometric masks from spatial data using regular grids.
26
+
27
+ ## Features
28
+
29
+ - Create regular grids of points inside spacial geometries (polygons or multipolygons)
30
+ - Control grid spacing with a resolution parameter
31
+ - Optional point limits to avoid huge grids
32
+ - Export to pandas DataFrames
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install geomask
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ```python
43
+ from geomask import GeoMask
44
+ from shapely import Polygon
45
+
46
+ # Create a polygon
47
+ polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
48
+
49
+ # Generate a grid mask
50
+ mask = GeoMask(geom=polygon, resolution=1.0)
51
+
52
+ print(f"Generated {len(mask)} points")
53
+ print(f"Area: {mask.area}")
54
+ print(f"Bounds: {mask.bounds}")
55
+ ```
56
+
57
+ ## Core Functionality
58
+
59
+ ### Creating Masks
60
+
61
+ ```python
62
+ from geomask import GeoMask
63
+ from shapely import Polygon, MultiPolygon
64
+
65
+ # Basic usage
66
+ polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
67
+ mask = GeoMask(geom=polygon, resolution=2.0)
68
+
69
+ # With point limit
70
+ mask = GeoMask(geom=polygon, resolution=0.5, limit=100)
71
+
72
+ # With grid offset
73
+ mask = GeoMask(geom=polygon, resolution=1.0, offset=(0.5, 0.5))
74
+
75
+ # Works with MultiPolygons too
76
+ poly1 = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
77
+ poly2 = Polygon([(10, 10), (15, 10), (15, 15), (10, 15)])
78
+ multipoly = MultiPolygon([poly1, poly2])
79
+ mask = GeoMask(geom=multipoly, resolution=1.0)
80
+ ```
81
+
82
+ ### Extracting Coordinates
83
+
84
+ ```python
85
+ # As numpy array
86
+ coords = mask.to_coordinates()
87
+ print(f"Shape: {coords.shape}") # (n_points, 2)
88
+
89
+ # As pandas DataFrame (requires pandas)
90
+ df = mask.to_dataframe()
91
+ print(df.head())
92
+
93
+ # Custom column names
94
+ df = mask.to_dataframe(x_col='longitude', y_col='latitude')
95
+ df = mask.to_dataframe(x_col='easting', y_col='northing')
96
+ ```
97
+
98
+ ### Filtering and Analysis
99
+
100
+ ```python
101
+ # Filter by another geometry
102
+ filter_polygon = Polygon([(2, 2), (8, 2), (8, 8), (2, 8)])
103
+ filtered_mask = mask.filter_by_geometry(filter_polygon)
104
+
105
+ # Properties and methods
106
+ print(f"Point count: {len(mask)}")
107
+ print(f"Has points: {bool(mask)}")
108
+ print(f"Area: {mask.area}")
109
+ print(f"Bounds: {mask.bounds}")
110
+ ```
111
+
112
+ ### Integration with Pandas
113
+
114
+ ```python
115
+ import pandas as pd
116
+ import numpy as np
117
+
118
+ # Convert to DataFrame for analysis
119
+ df = mask.to_dataframe(x_col='x', y_col='y')
120
+
121
+ # Add computed columns
122
+ df['distance_from_origin'] = np.sqrt(df.x**2 + df.y**2)
123
+ df['quadrant'] = df.apply(
124
+ lambda row: ('N' if row.y >= 0 else 'S') + ('E' if row.x >= 0 else 'W'),
125
+ axis=1
126
+ )
127
+
128
+ # Analysis
129
+ print(df.describe())
130
+ print(df.quadrant.value_counts())
131
+
132
+ # Filter points
133
+ close_points = df[df.distance_from_origin < 5.0]
134
+ ```
135
+
136
+ ## Advanced Usage
137
+
138
+ ### Working with Complex Geometries
139
+
140
+ ```python
141
+ # Polygon with hole
142
+ outer = [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]
143
+ inner = [(3, 3), (7, 3), (7, 7), (3, 7), (3, 3)]
144
+ polygon_with_hole = Polygon(outer, [inner])
145
+
146
+ mask = GeoMask(geom=polygon_with_hole, resolution=1.0)
147
+ ```
148
+
149
+ ### Performance Optimization
150
+
151
+ ```python
152
+ # Use limits for large areas
153
+ large_polygon = Polygon([(0, 0), (1000, 0), (1000, 1000), (0, 1000)])
154
+
155
+ # This will automatically adjust resolution to meet the limit
156
+ mask = GeoMask(geom=large_polygon, resolution=1.0, limit=10000)
157
+ print(f"Actual resolution used: {mask.resolution}")
158
+ ```
159
+
160
+ ## Requirements
161
+
162
+ - Python 3.11+
163
+ - shapely >= 2.1.1
164
+ - numpy
165
+ - pandas (optional, for DataFrame functionality)
166
+
167
+ ## Development
168
+
169
+ ### Testing
170
+
171
+ Run the comprehensive test suite:
172
+
173
+ ```bash
174
+ # Basic tests
175
+ pytest test_geomask.py
176
+
177
+ # With coverage
178
+ pytest test_geomask.py --cov=geomask --cov-report=term-missing
179
+
180
+ # Verbose output
181
+ pytest test_geomask.py -v
182
+ ```
183
+
@@ -0,0 +1,160 @@
1
+ # GeoMask
2
+
3
+ A very simple lib for creating geometric masks from spatial data using regular grids.
4
+
5
+ ## Features
6
+
7
+ - Create regular grids of points inside spacial geometries (polygons or multipolygons)
8
+ - Control grid spacing with a resolution parameter
9
+ - Optional point limits to avoid huge grids
10
+ - Export to pandas DataFrames
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ pip install geomask
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```python
21
+ from geomask import GeoMask
22
+ from shapely import Polygon
23
+
24
+ # Create a polygon
25
+ polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
26
+
27
+ # Generate a grid mask
28
+ mask = GeoMask(geom=polygon, resolution=1.0)
29
+
30
+ print(f"Generated {len(mask)} points")
31
+ print(f"Area: {mask.area}")
32
+ print(f"Bounds: {mask.bounds}")
33
+ ```
34
+
35
+ ## Core Functionality
36
+
37
+ ### Creating Masks
38
+
39
+ ```python
40
+ from geomask import GeoMask
41
+ from shapely import Polygon, MultiPolygon
42
+
43
+ # Basic usage
44
+ polygon = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])
45
+ mask = GeoMask(geom=polygon, resolution=2.0)
46
+
47
+ # With point limit
48
+ mask = GeoMask(geom=polygon, resolution=0.5, limit=100)
49
+
50
+ # With grid offset
51
+ mask = GeoMask(geom=polygon, resolution=1.0, offset=(0.5, 0.5))
52
+
53
+ # Works with MultiPolygons too
54
+ poly1 = Polygon([(0, 0), (5, 0), (5, 5), (0, 5)])
55
+ poly2 = Polygon([(10, 10), (15, 10), (15, 15), (10, 15)])
56
+ multipoly = MultiPolygon([poly1, poly2])
57
+ mask = GeoMask(geom=multipoly, resolution=1.0)
58
+ ```
59
+
60
+ ### Extracting Coordinates
61
+
62
+ ```python
63
+ # As numpy array
64
+ coords = mask.to_coordinates()
65
+ print(f"Shape: {coords.shape}") # (n_points, 2)
66
+
67
+ # As pandas DataFrame (requires pandas)
68
+ df = mask.to_dataframe()
69
+ print(df.head())
70
+
71
+ # Custom column names
72
+ df = mask.to_dataframe(x_col='longitude', y_col='latitude')
73
+ df = mask.to_dataframe(x_col='easting', y_col='northing')
74
+ ```
75
+
76
+ ### Filtering and Analysis
77
+
78
+ ```python
79
+ # Filter by another geometry
80
+ filter_polygon = Polygon([(2, 2), (8, 2), (8, 8), (2, 8)])
81
+ filtered_mask = mask.filter_by_geometry(filter_polygon)
82
+
83
+ # Properties and methods
84
+ print(f"Point count: {len(mask)}")
85
+ print(f"Has points: {bool(mask)}")
86
+ print(f"Area: {mask.area}")
87
+ print(f"Bounds: {mask.bounds}")
88
+ ```
89
+
90
+ ### Integration with Pandas
91
+
92
+ ```python
93
+ import pandas as pd
94
+ import numpy as np
95
+
96
+ # Convert to DataFrame for analysis
97
+ df = mask.to_dataframe(x_col='x', y_col='y')
98
+
99
+ # Add computed columns
100
+ df['distance_from_origin'] = np.sqrt(df.x**2 + df.y**2)
101
+ df['quadrant'] = df.apply(
102
+ lambda row: ('N' if row.y >= 0 else 'S') + ('E' if row.x >= 0 else 'W'),
103
+ axis=1
104
+ )
105
+
106
+ # Analysis
107
+ print(df.describe())
108
+ print(df.quadrant.value_counts())
109
+
110
+ # Filter points
111
+ close_points = df[df.distance_from_origin < 5.0]
112
+ ```
113
+
114
+ ## Advanced Usage
115
+
116
+ ### Working with Complex Geometries
117
+
118
+ ```python
119
+ # Polygon with hole
120
+ outer = [(0, 0), (10, 0), (10, 10), (0, 10), (0, 0)]
121
+ inner = [(3, 3), (7, 3), (7, 7), (3, 7), (3, 3)]
122
+ polygon_with_hole = Polygon(outer, [inner])
123
+
124
+ mask = GeoMask(geom=polygon_with_hole, resolution=1.0)
125
+ ```
126
+
127
+ ### Performance Optimization
128
+
129
+ ```python
130
+ # Use limits for large areas
131
+ large_polygon = Polygon([(0, 0), (1000, 0), (1000, 1000), (0, 1000)])
132
+
133
+ # This will automatically adjust resolution to meet the limit
134
+ mask = GeoMask(geom=large_polygon, resolution=1.0, limit=10000)
135
+ print(f"Actual resolution used: {mask.resolution}")
136
+ ```
137
+
138
+ ## Requirements
139
+
140
+ - Python 3.11+
141
+ - shapely >= 2.1.1
142
+ - numpy
143
+ - pandas (optional, for DataFrame functionality)
144
+
145
+ ## Development
146
+
147
+ ### Testing
148
+
149
+ Run the comprehensive test suite:
150
+
151
+ ```bash
152
+ # Basic tests
153
+ pytest test_geomask.py
154
+
155
+ # With coverage
156
+ pytest test_geomask.py --cov=geomask --cov-report=term-missing
157
+
158
+ # Verbose output
159
+ pytest test_geomask.py -v
160
+ ```
@@ -0,0 +1,173 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Self, Union, TYPE_CHECKING
3
+ import numpy as np
4
+ from shapely import Polygon, MultiPolygon, intersection, Geometry, MultiPoint
5
+
6
+ if TYPE_CHECKING:
7
+ import pandas as pd
8
+
9
+ SpatialGeometry = Union[Polygon, MultiPolygon]
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class GeoMask:
14
+ """A geometric mask for spatial data processing.
15
+
16
+ This class creates a regular grid of points within a given geometry
17
+ and provides methods for spatial masking operations.
18
+
19
+ Attributes:
20
+ geom: The input spatial geometry (Polygon or MultiPolygon)
21
+ resolution: Grid resolution for point generation
22
+ offset: Optional offset for grid positioning
23
+ limit: Optional limit on number of points
24
+ mask: The resulting geometric mask after intersection (computed automatically)
25
+ """
26
+
27
+ geom: SpatialGeometry
28
+ resolution: float
29
+ offset: tuple[float, float] | None = None
30
+ limit: int | None = None
31
+ mask: Geometry = field(init=False)
32
+
33
+ def __post_init__(self) -> None:
34
+ if self.resolution <= 0:
35
+ raise ValueError("Resolution must be positive")
36
+ if self.limit is not None and self.limit <= 0:
37
+ raise ValueError("Limit must be positive")
38
+ if not isinstance(self.geom, (Polygon, MultiPolygon)):
39
+ raise TypeError("Geometry must be a Polygon or MultiPolygon")
40
+
41
+ self.mask = self._generate_mask()
42
+
43
+ def _generate_mask(self) -> Geometry:
44
+ """Generate the grid mask based on geometry and parameters.
45
+
46
+ Returns:
47
+ The geometric mask after intersection with the grid
48
+ """
49
+ actual_resolution = self.resolution
50
+ if self.limit:
51
+ estimated_points = self.geom.area / (self.resolution**2)
52
+ if estimated_points > self.limit:
53
+ actual_resolution = np.sqrt(self.geom.area / self.limit)
54
+ object.__setattr__(self, "resolution", actual_resolution)
55
+
56
+ xmin, ymin, xmax, ymax = self.geom.bounds
57
+
58
+ grid_bounds = (
59
+ np.floor(xmin / actual_resolution) * actual_resolution,
60
+ np.floor(ymin / actual_resolution) * actual_resolution,
61
+ np.ceil(xmax / actual_resolution) * actual_resolution,
62
+ np.ceil(ymax / actual_resolution) * actual_resolution,
63
+ )
64
+
65
+ xcoords, ycoords = np.meshgrid(
66
+ np.arange(grid_bounds[0], grid_bounds[2], actual_resolution),
67
+ np.arange(grid_bounds[1], grid_bounds[3], actual_resolution),
68
+ )
69
+
70
+ if self.offset:
71
+ xoffset, yoffset = self.offset
72
+ xcoords += xoffset
73
+ ycoords += yoffset
74
+
75
+ mcoords = np.column_stack((xcoords.flatten(), ycoords.flatten()))
76
+ points = MultiPoint(mcoords)
77
+
78
+ return intersection(self.geom, points)
79
+
80
+ @property
81
+ def bounds(self) -> tuple[float, float, float, float]:
82
+ return self.geom.bounds
83
+
84
+ @property
85
+ def area(self) -> float:
86
+ return self.geom.area
87
+
88
+ @property
89
+ def point_count(self) -> int:
90
+ """Get the number of points in the mask."""
91
+ if self.mask.is_empty:
92
+ return 0
93
+ elif hasattr(self.mask, "geoms"):
94
+ return len([p for p in self.mask.geoms if not p.is_empty])
95
+ return 1 if not self.mask.is_empty else 0
96
+
97
+ def to_coordinates(self) -> np.ndarray:
98
+ """Extract coordinates from the mask as a numpy array.
99
+
100
+ Returns:
101
+ Array of shape (n, 2) containing x, y coordinates
102
+ """
103
+ if self.mask.is_empty:
104
+ return np.array([]).reshape(0, 2)
105
+ elif hasattr(self.mask, "geoms"):
106
+ coords = []
107
+ for point in self.mask.geoms:
108
+ if not point.is_empty:
109
+ coords.append([point.x, point.y])
110
+ return np.array(coords) if coords else np.array([]).reshape(0, 2)
111
+ elif hasattr(self.mask, "x") and hasattr(self.mask, "y"):
112
+ return np.array([[self.mask.x, self.mask.y]])
113
+ return np.array([]).reshape(0, 2)
114
+
115
+ def to_dataframe(self, x_col: str = "x", y_col: str = "y") -> "pd.DataFrame":
116
+ """Extract coordinates from the mask as a pandas DataFrame.
117
+
118
+ Args:
119
+ x_col: Name for the x-coordinate column (default: 'x')
120
+ y_col: Name for the y-coordinate column (default: 'y')
121
+
122
+ Returns:
123
+ DataFrame with specified column names containing coordinates
124
+
125
+ Raises:
126
+ ImportError: If pandas is not available
127
+ """
128
+ try:
129
+ import pandas as pd
130
+ except ImportError:
131
+ raise ImportError(
132
+ "pandas is required for to_dataframe(). "
133
+ "Install it with: pip install pandas"
134
+ )
135
+
136
+ coords = self.to_coordinates()
137
+ if coords.size == 0:
138
+ return pd.DataFrame(columns=[x_col, y_col])
139
+
140
+ return pd.DataFrame(coords, columns=[x_col, y_col])
141
+
142
+ def filter_by_geometry(self, filter_geom: SpatialGeometry, **kwargs) -> Self:
143
+ """Create a new GeoMask filtered by another geometry.
144
+
145
+ Args:
146
+ filter_geom: Geometry to filter by
147
+ **kwargs: Additional arguments passed to shapely.intersection
148
+
149
+ Returns:
150
+ A new filtered GeoMask instance
151
+ """
152
+ filtered_mask = intersection(self.mask, filter_geom, **kwargs)
153
+
154
+ new_instance = self.__class__(
155
+ geom=self.geom,
156
+ resolution=self.resolution,
157
+ offset=self.offset,
158
+ limit=self.limit,
159
+ )
160
+ object.__setattr__(new_instance, "mask", filtered_mask)
161
+ return new_instance
162
+
163
+ def __len__(self) -> int:
164
+ return self.point_count
165
+
166
+ def __bool__(self) -> bool:
167
+ return not self.mask.is_empty
168
+
169
+ def __repr__(self) -> str:
170
+ return (
171
+ f"GeoMask(area={self.area:.2f}, resolution={self.resolution:.3f}, "
172
+ f"points={self.point_count}, offset={self.offset}, limit={self.limit})"
173
+ )
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "geomask"
3
+ version = "0.1.0"
4
+ description = "A very simple lib for creating geometric masks from spatial data using regular grids"
5
+ authors = [
6
+ {name = "campiohe",email = "campiottihenrique@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ license = {text = "MIT"}
11
+ keywords = ["gis", "spatial", "grid", "geometry", "mask"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Science/Research",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Scientific/Engineering :: GIS",
20
+ ]
21
+ dependencies = [
22
+ "shapely (>=2.1.1,<3.0.0)"
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/campiohe/geomask"
27
+ Repository = "https://github.com/campiohe/geomask"
28
+ Issues = "https://github.com/campiohe/geomask/issues"
29
+
30
+ [tool.poetry]
31
+
32
+ [tool.poetry.group.dev.dependencies]
33
+ pytest = "^8.4.2"
34
+ pytest-cov = "^7.0.0"
35
+ pandas = "^2.3.2"
36
+ ruff = "^0.13.0"
37
+
38
+ [build-system]
39
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
40
+ build-backend = "poetry.core.masonry.api"
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ python_files = ["test_*.py"]
45
+ python_classes = ["Test*"]
46
+ python_functions = ["test_*"]
47
+ addopts = [
48
+ "-v",
49
+ "--strict-markers",
50
+ "--strict-config",
51
+ "--disable-warnings",
52
+ ]
53
+
54
+ [tool.coverage.run]
55
+ source = ["geomask"]
56
+ omit = ["tests/*"]
57
+
58
+ [tool.coverage.report]
59
+ exclude_lines = [
60
+ "pragma: no cover",
61
+ "def __repr__",
62
+ "if TYPE_CHECKING:",
63
+ "raise AssertionError",
64
+ "raise NotImplementedError",
65
+ ]