sh-batch-grid-builder 0.2.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.
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: sh-batch-grid-builder
3
+ Version: 0.2.0
4
+ Summary: A tool for generating projection grid aligned bounding boxes and pixelated geometries from AOI (Area of Interest) files for Sentinel Hub Batch V2 API on CDSE
5
+ Author-email: Maxim Lamare <maxim.lamare@sinergise.com>
6
+ License: MIT
7
+ Keywords: gis,geospatial,grid,pixelation,bounding-box
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Scientific/Engineering :: GIS
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: geopandas>=0.12.0
21
+ Requires-Dist: pyproj>=3.4.0
22
+ Requires-Dist: shapely>=2.0.0
23
+ Requires-Dist: rasterio>=1.3.0
24
+ Requires-Dist: numpy>=1.21.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
28
+
29
+ # SH Batch Grid Builder
30
+
31
+ This tool is designed to build custom tiling grids for the [Sentinel Hub Batch V2 API](https://documentation.dataspace.copernicus.eu/APIs/SentinelHub/BatchV2.html) on [CDSE](https://dataspace.copernicus.eu/). The custom grid is built around an input AOI for a given projection and ensures the Batch request produces outputs matching the pixel grid of the given projection.
32
+
33
+ ## Features
34
+
35
+ - **Aligned Bounding Boxes**: Generate projection grid-aligned bounding boxes that snap to a specified grid resolution
36
+ - **Pixelated Geometries**: Convert geometries to pixelated representations in order to only query data for the given AOI
37
+ - **Automatic Splitting**: Automatically splits large geometries that exceed pixel limits
38
+ - **Multiple CRS Support**: Works with any EPSG code, automatically handling CRS-specific grid origins
39
+
40
+ ## Installation
41
+
42
+ ### From PyPI
43
+
44
+ ```bash
45
+ pip install sh-batch-grid-builder
46
+ ```
47
+
48
+ ### From Source
49
+
50
+ ```bash
51
+ git clone https://github.com/maximlamare/SH-Batch-Grid-Builder.git
52
+ cd SH-Batch-Grid-Builder
53
+ pip install .
54
+ ```
55
+
56
+ ### Development Installation
57
+
58
+ ```bash
59
+ git clone https://github.com/maximlamare/SH-Batch-Grid-Builder.git
60
+ cd SH-Batch-Grid-Builder
61
+ pip install -e ".[dev]"
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Command Line Interface
67
+
68
+ The tool provides a command-line interface via the `sh-grid-builder` command:
69
+
70
+ ```bash
71
+ sh-grid-builder <input_aoi> --resolution "(x,y)" --epsg <epsg_code> --output-type <type> -o <output_file>
72
+ ```
73
+
74
+ #### Arguments
75
+
76
+ - `input_aoi`: Path to input AOI file (GeoJSON, GPKG, or other formats supported by GeoPandas)
77
+ - `--resolution`: Grid resolution as `(x,y)` tuple in CRS coordinate units:
78
+ - Format: `"(x,y)"` or `"x,y"` (brackets optional, e.g., `"(300,359)"` or `"300,359"`)
79
+ - **Important**: Always quote the resolution value to prevent shell interpretation (e.g., `--resolution "(300,359)"` or `--resolution "300,359"`)
80
+ - Note the resolution **must** be in the units of the selected projection (e.g. EPSG:3035: resolution in meters; EPSG:4326: resolution in degrees).
81
+ - X and Y resolutions can be different to support non-square pixels
82
+ - The tool automatically detects and displays the CRS units when running
83
+ - `--epsg`: EPSG code for the output CRS (e.g., `3035` for ETRS89 / LAEA Europe, `4326` for WGS84)
84
+ - `--output-type`: Type of output to generate:
85
+ - `bounding-box`: Generate an aligned bounding box that covers the AOI
86
+ - `pixelated`: Generate pixelated geometry of the AOI
87
+ - `-o, --output`: Path to output file (GPKG format required)
88
+
89
+ #### Examples
90
+
91
+ Generate aligned bounding boxes with same resolution for x and y:
92
+
93
+ ```bash
94
+ sh-grid-builder data/aoi.geojson --resolution "(10,10)" --epsg 3035 --output-type bounding-box -o output_bbox.gpkg
95
+ ```
96
+
97
+ Generate aligned bounding boxes with different x and y resolutions:
98
+
99
+ ```bash
100
+ sh-grid-builder data/aoi.geojson --resolution "(300,359)" --epsg 32632 --output-type bounding-box -o output_bbox.gpkg
101
+ ```
102
+
103
+ Generate pixelated geometry:
104
+
105
+ ```bash
106
+ sh-grid-builder data/aoi.geojson --resolution "10,10" --epsg 3035 --output-type pixelated -o output_pixelated.gpkg
107
+ ```
108
+
109
+ Example with geographic CRS (degrees):
110
+
111
+ ```bash
112
+ sh-grid-builder data/aoi.geojson --resolution "(0.001,0.001)" --epsg 4326 --output-type bounding-box -o output_bbox.gpkg
113
+ ```
114
+
115
+ ### Python API
116
+
117
+ You can also use the package programmatically:
118
+
119
+ ```python
120
+ from sh_batch_grid_builder import GeoData
121
+
122
+ # Initialize with AOI file, EPSG code, and resolutions (x, y)
123
+ geo_data = GeoData("path/to/aoi.geojson", epsg_code=3035, resolution_x=10.0, resolution_y=10.0)
124
+
125
+ # Or with different x and y resolutions
126
+ geo_data = GeoData("path/to/aoi.geojson", epsg_code=4326, resolution_x=0.002976190476204, resolution_y=0.002976190476204)
127
+
128
+ # Generate aligned bounding boxes
129
+ aligned_bboxes = geo_data.create_aligned_bounding_box(max_pixels=3500)
130
+
131
+ # Generate pixelated geometry (includes all pixels that touch/intersect the AOI)
132
+ pixelated_geom = geo_data.create_pixelated_geometry(max_pixels=3500)
133
+
134
+ # Save results
135
+ aligned_bboxes.to_file("output_bbox.gpkg", driver="GPKG")
136
+ pixelated_geom.to_file("output_pixelated.gpkg", driver="GPKG")
137
+ ```
138
+
139
+ ## How It Works
140
+
141
+ ### Aligned Bounding Boxes
142
+
143
+ The tool creates bounding boxes that are aligned to a grid based on:
144
+ 1. The specified X and Y resolutions (can be different for non-square pixels)
145
+ 2. The CRS origin (false easting/northing) for projected coordinate systems
146
+ 3. Automatic splitting when dimensions exceed 3500 pixels (fixed limit)
147
+
148
+ ### Pixelated Geometries
149
+
150
+ The pixelated geometry generation uses a raster-based approach:
151
+ 1. Converts the input geometry to a raster mask
152
+ 2. Polygonizes the raster back to vector format
153
+ 3. Automatically splits large geometries to avoid memory issues
154
+
155
+ This approach is much faster than vector-based methods for large grids. Pixelated
156
+ output includes any pixel that touches/intersects the AOI.
157
+
158
+ ## Requirements
159
+
160
+ - Python >= 3.8
161
+ - geopandas >= 0.12.0
162
+ - pyproj >= 3.4.0
163
+ - shapely >= 2.0.0
164
+ - rasterio >= 1.3.0
165
+ - numpy >= 1.21.0
166
+
167
+ ## Development
168
+
169
+ ### Running Tests
170
+
171
+ ```bash
172
+ pytest
173
+ ```
174
+
175
+ ### Building the Package
176
+
177
+ ```bash
178
+ python -m build
179
+ ```
180
+
181
+ ## License
182
+
183
+ MIT License
184
+
185
+ ## Contributing
186
+
187
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,159 @@
1
+ # SH Batch Grid Builder
2
+
3
+ This tool is designed to build custom tiling grids for the [Sentinel Hub Batch V2 API](https://documentation.dataspace.copernicus.eu/APIs/SentinelHub/BatchV2.html) on [CDSE](https://dataspace.copernicus.eu/). The custom grid is built around an input AOI for a given projection and ensures the Batch request produces outputs matching the pixel grid of the given projection.
4
+
5
+ ## Features
6
+
7
+ - **Aligned Bounding Boxes**: Generate projection grid-aligned bounding boxes that snap to a specified grid resolution
8
+ - **Pixelated Geometries**: Convert geometries to pixelated representations in order to only query data for the given AOI
9
+ - **Automatic Splitting**: Automatically splits large geometries that exceed pixel limits
10
+ - **Multiple CRS Support**: Works with any EPSG code, automatically handling CRS-specific grid origins
11
+
12
+ ## Installation
13
+
14
+ ### From PyPI
15
+
16
+ ```bash
17
+ pip install sh-batch-grid-builder
18
+ ```
19
+
20
+ ### From Source
21
+
22
+ ```bash
23
+ git clone https://github.com/maximlamare/SH-Batch-Grid-Builder.git
24
+ cd SH-Batch-Grid-Builder
25
+ pip install .
26
+ ```
27
+
28
+ ### Development Installation
29
+
30
+ ```bash
31
+ git clone https://github.com/maximlamare/SH-Batch-Grid-Builder.git
32
+ cd SH-Batch-Grid-Builder
33
+ pip install -e ".[dev]"
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Command Line Interface
39
+
40
+ The tool provides a command-line interface via the `sh-grid-builder` command:
41
+
42
+ ```bash
43
+ sh-grid-builder <input_aoi> --resolution "(x,y)" --epsg <epsg_code> --output-type <type> -o <output_file>
44
+ ```
45
+
46
+ #### Arguments
47
+
48
+ - `input_aoi`: Path to input AOI file (GeoJSON, GPKG, or other formats supported by GeoPandas)
49
+ - `--resolution`: Grid resolution as `(x,y)` tuple in CRS coordinate units:
50
+ - Format: `"(x,y)"` or `"x,y"` (brackets optional, e.g., `"(300,359)"` or `"300,359"`)
51
+ - **Important**: Always quote the resolution value to prevent shell interpretation (e.g., `--resolution "(300,359)"` or `--resolution "300,359"`)
52
+ - Note the resolution **must** be in the units of the selected projection (e.g. EPSG:3035: resolution in meters; EPSG:4326: resolution in degrees).
53
+ - X and Y resolutions can be different to support non-square pixels
54
+ - The tool automatically detects and displays the CRS units when running
55
+ - `--epsg`: EPSG code for the output CRS (e.g., `3035` for ETRS89 / LAEA Europe, `4326` for WGS84)
56
+ - `--output-type`: Type of output to generate:
57
+ - `bounding-box`: Generate an aligned bounding box that covers the AOI
58
+ - `pixelated`: Generate pixelated geometry of the AOI
59
+ - `-o, --output`: Path to output file (GPKG format required)
60
+
61
+ #### Examples
62
+
63
+ Generate aligned bounding boxes with same resolution for x and y:
64
+
65
+ ```bash
66
+ sh-grid-builder data/aoi.geojson --resolution "(10,10)" --epsg 3035 --output-type bounding-box -o output_bbox.gpkg
67
+ ```
68
+
69
+ Generate aligned bounding boxes with different x and y resolutions:
70
+
71
+ ```bash
72
+ sh-grid-builder data/aoi.geojson --resolution "(300,359)" --epsg 32632 --output-type bounding-box -o output_bbox.gpkg
73
+ ```
74
+
75
+ Generate pixelated geometry:
76
+
77
+ ```bash
78
+ sh-grid-builder data/aoi.geojson --resolution "10,10" --epsg 3035 --output-type pixelated -o output_pixelated.gpkg
79
+ ```
80
+
81
+ Example with geographic CRS (degrees):
82
+
83
+ ```bash
84
+ sh-grid-builder data/aoi.geojson --resolution "(0.001,0.001)" --epsg 4326 --output-type bounding-box -o output_bbox.gpkg
85
+ ```
86
+
87
+ ### Python API
88
+
89
+ You can also use the package programmatically:
90
+
91
+ ```python
92
+ from sh_batch_grid_builder import GeoData
93
+
94
+ # Initialize with AOI file, EPSG code, and resolutions (x, y)
95
+ geo_data = GeoData("path/to/aoi.geojson", epsg_code=3035, resolution_x=10.0, resolution_y=10.0)
96
+
97
+ # Or with different x and y resolutions
98
+ geo_data = GeoData("path/to/aoi.geojson", epsg_code=4326, resolution_x=0.002976190476204, resolution_y=0.002976190476204)
99
+
100
+ # Generate aligned bounding boxes
101
+ aligned_bboxes = geo_data.create_aligned_bounding_box(max_pixels=3500)
102
+
103
+ # Generate pixelated geometry (includes all pixels that touch/intersect the AOI)
104
+ pixelated_geom = geo_data.create_pixelated_geometry(max_pixels=3500)
105
+
106
+ # Save results
107
+ aligned_bboxes.to_file("output_bbox.gpkg", driver="GPKG")
108
+ pixelated_geom.to_file("output_pixelated.gpkg", driver="GPKG")
109
+ ```
110
+
111
+ ## How It Works
112
+
113
+ ### Aligned Bounding Boxes
114
+
115
+ The tool creates bounding boxes that are aligned to a grid based on:
116
+ 1. The specified X and Y resolutions (can be different for non-square pixels)
117
+ 2. The CRS origin (false easting/northing) for projected coordinate systems
118
+ 3. Automatic splitting when dimensions exceed 3500 pixels (fixed limit)
119
+
120
+ ### Pixelated Geometries
121
+
122
+ The pixelated geometry generation uses a raster-based approach:
123
+ 1. Converts the input geometry to a raster mask
124
+ 2. Polygonizes the raster back to vector format
125
+ 3. Automatically splits large geometries to avoid memory issues
126
+
127
+ This approach is much faster than vector-based methods for large grids. Pixelated
128
+ output includes any pixel that touches/intersects the AOI.
129
+
130
+ ## Requirements
131
+
132
+ - Python >= 3.8
133
+ - geopandas >= 0.12.0
134
+ - pyproj >= 3.4.0
135
+ - shapely >= 2.0.0
136
+ - rasterio >= 1.3.0
137
+ - numpy >= 1.21.0
138
+
139
+ ## Development
140
+
141
+ ### Running Tests
142
+
143
+ ```bash
144
+ pytest
145
+ ```
146
+
147
+ ### Building the Package
148
+
149
+ ```bash
150
+ python -m build
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT License
156
+
157
+ ## Contributing
158
+
159
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sh-batch-grid-builder"
7
+ version = "0.2.0"
8
+ description = "A tool for generating projection grid aligned bounding boxes and pixelated geometries from AOI (Area of Interest) files for Sentinel Hub Batch V2 API on CDSE"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Maxim Lamare", email = "maxim.lamare@sinergise.com"}
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Science/Research",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.8",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Topic :: Scientific/Engineering :: GIS",
26
+ ]
27
+ keywords = ["gis", "geospatial", "grid", "pixelation", "bounding-box"]
28
+
29
+ dependencies = [
30
+ "geopandas>=0.12.0",
31
+ "pyproj>=3.4.0",
32
+ "shapely>=2.0.0",
33
+ "rasterio>=1.3.0",
34
+ "numpy>=1.21.0",
35
+ ]
36
+
37
+ [project.optional-dependencies]
38
+ dev = [
39
+ "pytest>=7.0.0",
40
+ "pytest-cov>=4.0.0",
41
+ ]
42
+
43
+ [project.scripts]
44
+ sh-grid-builder = "sh_batch_grid_builder.cli:main"
45
+
46
+ [tool.setuptools]
47
+ packages = ["sh_batch_grid_builder"]
48
+
49
+ [tool.setuptools.package-data]
50
+ "*" = ["*.txt", "*.md"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ python_files = ["test_*.py"]
55
+ python_classes = ["Test*"]
56
+ python_functions = ["test_*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """
2
+ SH Batch Grid Builder
3
+
4
+ A tool for generating aligned bounding boxes and pixelated geometries from AOI files.
5
+ """
6
+
7
+ from sh_batch_grid_builder.geo import GeoData
8
+ from sh_batch_grid_builder.crs import get_crs_data, get_crs_units
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = ["GeoData", "get_crs_data", "get_crs_units"]
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Command-line interface for SH Batch Grid Builder.
4
+
5
+ This tool generates bounding boxes or pixelated geometries from AOI files.
6
+ """
7
+ import argparse
8
+ import sys
9
+ from pathlib import Path
10
+ from sh_batch_grid_builder.geo import GeoData
11
+ from sh_batch_grid_builder.crs import get_crs_units
12
+
13
+ # Fixed maximum pixels setting - geometries will be automatically split if they exceed this limit
14
+ MAX_PIXELS = 3500
15
+
16
+
17
+ def _parse_resolution(value: str) -> tuple[float, float]:
18
+ cleaned = value.strip()
19
+ if cleaned.startswith("(") and cleaned.endswith(")"):
20
+ cleaned = cleaned[1:-1].strip()
21
+ parts = [part.strip() for part in cleaned.split(",") if part.strip()]
22
+ if len(parts) != 2:
23
+ raise ValueError(
24
+ "Resolution must be provided as '(x,y)' or 'x,y' with two values."
25
+ )
26
+ try:
27
+ resolution_x = float(parts[0])
28
+ resolution_y = float(parts[1])
29
+ except ValueError as exc:
30
+ raise ValueError("Resolution values must be numeric.") from exc
31
+ if resolution_x <= 0 or resolution_y <= 0:
32
+ raise ValueError(
33
+ f"Resolution must be positive, got ({resolution_x}, {resolution_y})"
34
+ )
35
+ return resolution_x, resolution_y
36
+
37
+
38
+ def main():
39
+ """Main entry point for the CLI."""
40
+ parser = argparse.ArgumentParser(
41
+ description="Generate bounding boxes or pixelated geometries from AOI files",
42
+ formatter_class=argparse.RawDescriptionHelpFormatter,
43
+ epilog="""
44
+ Examples:
45
+ # Generate bounding box
46
+ sh-grid-builder input.geojson --resolution "(10,10)" --epsg 3035 --output-type bounding-box -o output.gpkg
47
+
48
+ # Generate pixelated geometry
49
+ sh-grid-builder input.geojson --resolution "10,10" --epsg 3035 --output-type pixelated -o output.gpkg
50
+ """,
51
+ )
52
+
53
+ parser.add_argument(
54
+ "input_aoi",
55
+ type=str,
56
+ help="Path to input AOI file (GeoJSON, GPKG, or other geospatial formats supported by GeoPandas)",
57
+ )
58
+
59
+ parser.add_argument(
60
+ "--resolution",
61
+ type=str,
62
+ required=True,
63
+ help=(
64
+ "Grid resolution as '(x,y)' or 'x,y' in CRS coordinate units "
65
+ "(e.g., '(10,10)' for 10 meters, or '0.001,0.001' for degrees)"
66
+ ),
67
+ )
68
+
69
+ parser.add_argument(
70
+ "--epsg",
71
+ type=int,
72
+ required=True,
73
+ help="EPSG code for the output CRS (e.g., 3035 for ETRS89 / LAEA Europe)",
74
+ )
75
+
76
+ parser.add_argument(
77
+ "--output-type",
78
+ type=str,
79
+ choices=["bounding-box", "pixelated"],
80
+ required=True,
81
+ help="Type of output to generate: 'bounding-box' for aligned bounding boxes, 'pixelated' for pixelated geometry",
82
+ )
83
+
84
+ parser.add_argument(
85
+ "-o",
86
+ "--output",
87
+ type=str,
88
+ required=True,
89
+ help="Path to output file (GPKG format required)",
90
+ )
91
+
92
+ args = parser.parse_args()
93
+
94
+ # Validate input file exists
95
+ input_path = Path(args.input_aoi)
96
+ if not input_path.exists():
97
+ print(f"Error: Input file '{args.input_aoi}' does not exist.", file=sys.stderr)
98
+ sys.exit(1)
99
+
100
+ # Parse and validate resolution
101
+ try:
102
+ resolution_x, resolution_y = _parse_resolution(args.resolution)
103
+ except ValueError as exc:
104
+ print(f"Error: {exc}", file=sys.stderr)
105
+ sys.exit(1)
106
+
107
+ # Check CRS units and warn user
108
+ try:
109
+ crs_units = get_crs_units(args.epsg)
110
+ print(f"CRS EPSG:{args.epsg} uses units: {crs_units}")
111
+ print(f"Resolution: ({resolution_x}, {resolution_y}) {crs_units}")
112
+
113
+ # Warn if using geographic CRS (degrees) with potentially inappropriate resolution
114
+ if crs_units == "degrees":
115
+ if resolution_x > 1.0 or resolution_y > 1.0:
116
+ print(
117
+ f"Warning: Resolution of ({resolution_x}, {resolution_y}) degrees is very large. "
118
+ f"For EPSG:{args.epsg} (geographic CRS), resolution should be in degrees. "
119
+ f"Typical values are small (e.g., 0.001 degrees ≈ 111 meters).",
120
+ file=sys.stderr,
121
+ )
122
+ elif resolution_x < 0.00001 or resolution_y < 0.00001:
123
+ print(
124
+ f"Warning: Resolution of ({resolution_x}, {resolution_y}) degrees is very small. "
125
+ f"This may result in extremely large pixel counts.",
126
+ file=sys.stderr,
127
+ )
128
+ except Exception as e:
129
+ print(f"Warning: Could not determine CRS units: {e}", file=sys.stderr)
130
+ print("Proceeding with resolution as provided...", file=sys.stderr)
131
+
132
+ try:
133
+ # Initialize GeoData
134
+ print(f"Loading AOI from: {args.input_aoi}")
135
+ geo_data = GeoData(input_path, args.epsg, resolution_x, resolution_y)
136
+
137
+ # Generate output based on type
138
+ if args.output_type == "bounding-box":
139
+ print(
140
+ f"Generating aligned bounding box(es) with resolution ({resolution_x}, {resolution_y})..."
141
+ )
142
+ result_gdf = geo_data.create_aligned_bounding_box(max_pixels=MAX_PIXELS)
143
+ print(f"Created {len(result_gdf)} aligned bounding box(es)")
144
+ else: # pixelated
145
+ print(
146
+ f"Generating pixelated geometry with resolution ({resolution_x}, {resolution_y})..."
147
+ )
148
+ result_gdf = geo_data.create_pixelated_geometry(max_pixels=MAX_PIXELS)
149
+ print(f"Created {len(result_gdf)} pixelated geometry/geometries")
150
+
151
+ # Save output
152
+ output_path = Path(args.output)
153
+ result_gdf.to_file(output_path, driver="GPKG")
154
+ print(f"Successfully saved {len(result_gdf)} feature(s) to {output_path}")
155
+
156
+ except Exception as e:
157
+ print(f"Error: {str(e)}", file=sys.stderr)
158
+ sys.exit(1)
159
+
160
+
161
+ if __name__ == "__main__":
162
+ main()
@@ -0,0 +1,62 @@
1
+ from typing import Tuple
2
+ from pyproj import CRS
3
+
4
+
5
+ def get_crs_data(target_epsg: int) -> Tuple[float, float]:
6
+ crs = CRS.from_epsg(target_epsg)
7
+
8
+ origin_x = 0.0
9
+ origin_y = 0.0
10
+
11
+ if crs.is_projected:
12
+ for param in crs.coordinate_operation.params:
13
+ if "easting" in param.name.lower() or "false easting" in param.name.lower():
14
+ origin_x = param.value
15
+ if (
16
+ "northing" in param.name.lower()
17
+ or "false northing" in param.name.lower()
18
+ ):
19
+ origin_y = param.value
20
+
21
+ return origin_x, origin_y
22
+
23
+
24
+ def get_crs_units(target_epsg: int) -> str:
25
+ """
26
+ Get the units of the CRS.
27
+
28
+ Args:
29
+ target_epsg: EPSG code
30
+
31
+ Returns:
32
+ String describing the units (e.g., "degrees", "metre", "meter", "foot", etc.)
33
+ """
34
+ crs = CRS.from_epsg(target_epsg)
35
+
36
+ if crs.is_geographic:
37
+ return "degrees"
38
+ elif crs.is_projected:
39
+ # Get units from axis info
40
+ if crs.axis_info:
41
+ unit_name = crs.axis_info[0].unit_name.lower()
42
+ # Normalize common variations
43
+ if unit_name in ["metre", "meter", "m"]:
44
+ return "meters"
45
+ elif unit_name in ["degree", "degrees", "deg"]:
46
+ return "degrees"
47
+ elif unit_name in ["foot", "feet", "ft"]:
48
+ return "feet"
49
+ else:
50
+ return unit_name
51
+ else:
52
+ # Default for projected CRS is usually meters
53
+ return "meters"
54
+ else:
55
+ # Fallback - try to get from CRS string
56
+ crs_str = str(crs).lower()
57
+ if "degree" in crs_str:
58
+ return "degrees"
59
+ elif "metre" in crs_str or "meter" in crs_str:
60
+ return "meters"
61
+ else:
62
+ return "unknown"
@@ -0,0 +1,278 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+ import math
4
+ import geopandas as gpd
5
+ from shapely.geometry import box, shape
6
+ from shapely.ops import unary_union
7
+ from sh_batch_grid_builder.crs import get_crs_data
8
+ from pyproj import CRS
9
+ from rasterio import features
10
+ from rasterio.transform import from_origin
11
+
12
+
13
+ class GeoData:
14
+ """
15
+ A class for working with geodata and creating aligned bounding boxes to the projection grid.
16
+
17
+ Args:
18
+ filepath: Path to the input geodata file
19
+ epsg_code: EPSG code of the input geodata
20
+ resolution_x: Resolution of the input geodata in x direction
21
+ resolution_y: Resolution of the input geodata in y direction
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ filepath: Union[str, Path],
27
+ epsg_code: int,
28
+ resolution_x: float,
29
+ resolution_y: float,
30
+ ):
31
+ self.gdf = self.read_geodata(filepath)
32
+ self.crs = epsg_code
33
+ self.bounds = self.gdf.total_bounds
34
+
35
+ self.resolution_x = resolution_x
36
+ self.resolution_y = resolution_y
37
+ self._validate_resolutions()
38
+
39
+ self._validate_epsg(epsg_code)
40
+ self.epsg_code = epsg_code
41
+
42
+ def _validate_resolutions(self):
43
+ if self.resolution_x <= 0:
44
+ raise ValueError(f"Resolution X must be positive, got {self.resolution_x}")
45
+ if self.resolution_y <= 0:
46
+ raise ValueError(f"Resolution Y must be positive, got {self.resolution_y}")
47
+
48
+ def _validate_epsg(self, epsg_code: int):
49
+ if self.gdf.crs.to_epsg() is None:
50
+ raise ValueError(
51
+ f"Could not determine EPSG code from input file CRS. "
52
+ f"Expected EPSG:{epsg_code}. Please ensure the file has a valid EPSG CRS."
53
+ )
54
+
55
+ if self.gdf.crs.to_epsg() != epsg_code:
56
+ raise ValueError(
57
+ f"Input file CRS (EPSG:{self.gdf.crs.to_epsg()}) does not match target EPSG ({epsg_code}). "
58
+ f"Please reproject the input file to EPSG:{epsg_code} before processing, "
59
+ f"or use EPSG:{self.gdf.crs.to_epsg()} as the target EPSG."
60
+ )
61
+
62
+ def _align_axis(
63
+ self, minv: float, maxv: float, origin: float, res: float
64
+ ) -> tuple[float, float]:
65
+ # snap to grid defined by origin + k*res
66
+ aligned_min = origin + math.floor((minv - origin) / res) * res
67
+ aligned_max = origin + math.ceil((maxv - origin) / res) * res
68
+
69
+ # ensure width/height is multiple of res (guards floating error)
70
+ size = aligned_max - aligned_min
71
+ steps = math.ceil(size / res)
72
+ aligned_max = aligned_min + steps * res
73
+
74
+ return aligned_min, aligned_max
75
+
76
+ def _split_pixel_counts(self, total: int, parts: int) -> list[int]:
77
+ base = total // parts
78
+ remainder = total % parts
79
+ return [base + 1 if i < remainder else base for i in range(parts)]
80
+
81
+ def _grid_origin(self) -> tuple[float, float]:
82
+ origin_x, origin_y = get_crs_data(self.crs)
83
+ if not CRS.from_epsg(self.crs).is_projected:
84
+ origin_x -= self.resolution_x / 2
85
+ origin_y -= self.resolution_y / 2
86
+ return origin_x, origin_y
87
+
88
+ def read_geodata(self, filepath: Union[str, Path]):
89
+ gdf = gpd.read_file(filepath)
90
+ return gdf
91
+
92
+ def create_aligned_bounding_box(self, max_pixels: int = 3500) -> gpd.GeoDataFrame:
93
+ """
94
+ Create an aligned bounding box to the projection grid that covers the input geometry.
95
+
96
+ Args:
97
+ max_pixels: Maximum allowed pixels in either dimension (default: 3500)
98
+
99
+ Returns:
100
+ GeoDataFrame with one or more bounding boxes (split if exceeds max_pixels)
101
+ """
102
+ # Get the grid origin from the CRS
103
+ origin_x, origin_y = self._grid_origin()
104
+
105
+ # Get the grid bounds of the input geometry
106
+ minx, miny, maxx, maxy = self.bounds
107
+
108
+ aligned_minx, aligned_maxx = self._align_axis(
109
+ minx, maxx, origin_x, self.resolution_x
110
+ )
111
+ aligned_miny, aligned_maxy = self._align_axis(
112
+ miny, maxy, origin_y, self.resolution_y
113
+ )
114
+
115
+ # Calculate width and height in pixels of the aligned bounding box
116
+ width_px = int(round((aligned_maxx - aligned_minx) / self.resolution_x))
117
+ height_px = int(round((aligned_maxy - aligned_miny) / self.resolution_y))
118
+
119
+ tiles_x = max(1, math.ceil(width_px / max_pixels))
120
+ tiles_y = max(1, math.ceil(height_px / max_pixels))
121
+
122
+ widths = self._split_pixel_counts(width_px, tiles_x)
123
+ heights = self._split_pixel_counts(height_px, tiles_y)
124
+
125
+ geometries = []
126
+ y_min = aligned_miny
127
+ for row_idx, tile_h in enumerate(heights, start=1):
128
+ y_max = y_min + tile_h * self.resolution_y
129
+ x_min = aligned_minx
130
+ for col_idx, tile_w in enumerate(widths, start=1):
131
+ x_max = x_min + tile_w * self.resolution_x
132
+ geometries.append(
133
+ {
134
+ "geometry": box(x_min, y_min, x_max, y_max),
135
+ "width": tile_w,
136
+ "height": tile_h,
137
+ "row_idx": row_idx,
138
+ "col_idx": col_idx,
139
+ }
140
+ )
141
+ x_min = x_max
142
+ y_min = y_max
143
+
144
+ if not geometries:
145
+ return gpd.GeoDataFrame(
146
+ [], columns=["id", "identifier", "width", "height", "geometry"], crs=CRS.from_epsg(self.crs)
147
+ )
148
+
149
+ aoi_union = unary_union(self.gdf.geometry)
150
+ filtered_tiles = [
151
+ tile for tile in geometries if tile["geometry"].intersects(aoi_union)
152
+ ]
153
+
154
+ if not filtered_tiles:
155
+ return gpd.GeoDataFrame(
156
+ [], columns=["id", "identifier", "width", "height", "geometry"], crs=CRS.from_epsg(self.crs)
157
+ )
158
+
159
+ # Renumber tiles without gaps in row/col identifiers
160
+ filtered_tiles.sort(key=lambda tile: (tile["row_idx"], tile["col_idx"]))
161
+ renumbered_tiles = []
162
+ current_row = None
163
+ row_counter = 0
164
+ col_counter = 0
165
+ for tile in filtered_tiles:
166
+ if tile["row_idx"] != current_row:
167
+ row_counter += 1
168
+ current_row = tile["row_idx"]
169
+ col_counter = 0
170
+ col_counter += 1
171
+ renumbered_tiles.append(
172
+ {
173
+ "geometry": tile["geometry"],
174
+ "width": tile["width"],
175
+ "height": tile["height"],
176
+ "identifier": f"tile_{col_counter}_{row_counter}",
177
+ }
178
+ )
179
+
180
+ # Create GeoDataFrame and renumber sequentially
181
+ bbox_gdf = gpd.GeoDataFrame(renumbered_tiles, crs=CRS.from_epsg(self.crs))
182
+ bbox_gdf["id"] = range(1, len(bbox_gdf) + 1)
183
+
184
+ # Reorder columns to match expected format
185
+ bbox_gdf = bbox_gdf[["id", "identifier", "width", "height", "geometry"]]
186
+
187
+ return bbox_gdf
188
+
189
+ def create_pixelated_geometry(self, max_pixels: int = 3500) -> gpd.GeoDataFrame:
190
+ """
191
+ Rasterize AOI to aligned grid, then dissolve connected pixels into polygons.
192
+
193
+ Args:
194
+ max_pixels: Maximum allowed pixels in either dimension (default: 3500)
195
+
196
+ Returns:
197
+ GeoDataFrame of dissolved pixel groups.
198
+ """
199
+ # Get the grid origin from the CRS
200
+ origin_x, origin_y = self._grid_origin()
201
+
202
+ # Get the grid bounds of the input geometry
203
+ minx, miny, maxx, maxy = self.bounds
204
+
205
+ aligned_minx, aligned_maxx = self._align_axis(
206
+ minx, maxx, origin_x, self.resolution_x
207
+ )
208
+ aligned_miny, aligned_maxy = self._align_axis(
209
+ miny, maxy, origin_y, self.resolution_y
210
+ )
211
+
212
+ # Calculate width and height in pixels of the aligned bounding box
213
+ width_px = int(round((aligned_maxx - aligned_minx) / self.resolution_x))
214
+ height_px = int(round((aligned_maxy - aligned_miny) / self.resolution_y))
215
+
216
+ if width_px <= 0 or height_px <= 0:
217
+ return gpd.GeoDataFrame([], crs=self.gdf.crs)
218
+
219
+ transform = from_origin(
220
+ aligned_minx, aligned_maxy, self.resolution_x, self.resolution_y
221
+ )
222
+
223
+ shapes = ((geom, 1) for geom in self.gdf.geometry)
224
+ mask = features.rasterize(
225
+ shapes=shapes,
226
+ out_shape=(height_px, width_px),
227
+ transform=transform,
228
+ fill=0,
229
+ all_touched=True,
230
+ dtype="uint8",
231
+ )
232
+
233
+ tiles_x = max(1, math.ceil(width_px / max_pixels))
234
+ tiles_y = max(1, math.ceil(height_px / max_pixels))
235
+
236
+ widths = self._split_pixel_counts(width_px, tiles_x)
237
+ heights = self._split_pixel_counts(height_px, tiles_y)
238
+
239
+ split_polygons = []
240
+ y_offset = 0
241
+ for row_idx, tile_h in enumerate(heights, start=1):
242
+ x_offset = 0
243
+ for col_idx, tile_w in enumerate(widths, start=1):
244
+ window = mask[
245
+ y_offset : y_offset + tile_h,
246
+ x_offset : x_offset + tile_w,
247
+ ]
248
+ if window.any():
249
+ tile_identifier = f"tile_{col_idx}_{row_idx}"
250
+ window_transform = from_origin(
251
+ aligned_minx + x_offset * self.resolution_x,
252
+ aligned_maxy - y_offset * self.resolution_y,
253
+ self.resolution_x,
254
+ self.resolution_y,
255
+ )
256
+ feature_idx = 0
257
+ for geom, value in features.shapes(
258
+ window, mask=window > 0, transform=window_transform
259
+ ):
260
+ feature_idx += 1
261
+ split_polygons.append(
262
+ {
263
+ "geometry": shape(geom),
264
+ "width": tile_w,
265
+ "height": tile_h,
266
+ "identifier": f"{tile_identifier}_{feature_idx}",
267
+ }
268
+ )
269
+ x_offset += tile_w
270
+ y_offset += tile_h
271
+
272
+ if not split_polygons:
273
+ return gpd.GeoDataFrame([], crs=self.gdf.crs)
274
+
275
+ pixel_gdf = gpd.GeoDataFrame(split_polygons, crs=self.gdf.crs)
276
+ pixel_gdf["id"] = range(1, len(pixel_gdf) + 1)
277
+ pixel_gdf = pixel_gdf[["id", "identifier", "width", "height", "geometry"]]
278
+ return pixel_gdf
@@ -0,0 +1,187 @@
1
+ Metadata-Version: 2.4
2
+ Name: sh-batch-grid-builder
3
+ Version: 0.2.0
4
+ Summary: A tool for generating projection grid aligned bounding boxes and pixelated geometries from AOI (Area of Interest) files for Sentinel Hub Batch V2 API on CDSE
5
+ Author-email: Maxim Lamare <maxim.lamare@sinergise.com>
6
+ License: MIT
7
+ Keywords: gis,geospatial,grid,pixelation,bounding-box
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Topic :: Scientific/Engineering :: GIS
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: geopandas>=0.12.0
21
+ Requires-Dist: pyproj>=3.4.0
22
+ Requires-Dist: shapely>=2.0.0
23
+ Requires-Dist: rasterio>=1.3.0
24
+ Requires-Dist: numpy>=1.21.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
27
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
28
+
29
+ # SH Batch Grid Builder
30
+
31
+ This tool is designed to build custom tiling grids for the [Sentinel Hub Batch V2 API](https://documentation.dataspace.copernicus.eu/APIs/SentinelHub/BatchV2.html) on [CDSE](https://dataspace.copernicus.eu/). The custom grid is built around an input AOI for a given projection and ensures the Batch request produces outputs matching the pixel grid of the given projection.
32
+
33
+ ## Features
34
+
35
+ - **Aligned Bounding Boxes**: Generate projection grid-aligned bounding boxes that snap to a specified grid resolution
36
+ - **Pixelated Geometries**: Convert geometries to pixelated representations in order to only query data for the given AOI
37
+ - **Automatic Splitting**: Automatically splits large geometries that exceed pixel limits
38
+ - **Multiple CRS Support**: Works with any EPSG code, automatically handling CRS-specific grid origins
39
+
40
+ ## Installation
41
+
42
+ ### From PyPI
43
+
44
+ ```bash
45
+ pip install sh-batch-grid-builder
46
+ ```
47
+
48
+ ### From Source
49
+
50
+ ```bash
51
+ git clone https://github.com/maximlamare/SH-Batch-Grid-Builder.git
52
+ cd SH-Batch-Grid-Builder
53
+ pip install .
54
+ ```
55
+
56
+ ### Development Installation
57
+
58
+ ```bash
59
+ git clone https://github.com/maximlamare/SH-Batch-Grid-Builder.git
60
+ cd SH-Batch-Grid-Builder
61
+ pip install -e ".[dev]"
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Command Line Interface
67
+
68
+ The tool provides a command-line interface via the `sh-grid-builder` command:
69
+
70
+ ```bash
71
+ sh-grid-builder <input_aoi> --resolution "(x,y)" --epsg <epsg_code> --output-type <type> -o <output_file>
72
+ ```
73
+
74
+ #### Arguments
75
+
76
+ - `input_aoi`: Path to input AOI file (GeoJSON, GPKG, or other formats supported by GeoPandas)
77
+ - `--resolution`: Grid resolution as `(x,y)` tuple in CRS coordinate units:
78
+ - Format: `"(x,y)"` or `"x,y"` (brackets optional, e.g., `"(300,359)"` or `"300,359"`)
79
+ - **Important**: Always quote the resolution value to prevent shell interpretation (e.g., `--resolution "(300,359)"` or `--resolution "300,359"`)
80
+ - Note the resolution **must** be in the units of the selected projection (e.g. EPSG:3035: resolution in meters; EPSG:4326: resolution in degrees).
81
+ - X and Y resolutions can be different to support non-square pixels
82
+ - The tool automatically detects and displays the CRS units when running
83
+ - `--epsg`: EPSG code for the output CRS (e.g., `3035` for ETRS89 / LAEA Europe, `4326` for WGS84)
84
+ - `--output-type`: Type of output to generate:
85
+ - `bounding-box`: Generate an aligned bounding box that covers the AOI
86
+ - `pixelated`: Generate pixelated geometry of the AOI
87
+ - `-o, --output`: Path to output file (GPKG format required)
88
+
89
+ #### Examples
90
+
91
+ Generate aligned bounding boxes with same resolution for x and y:
92
+
93
+ ```bash
94
+ sh-grid-builder data/aoi.geojson --resolution "(10,10)" --epsg 3035 --output-type bounding-box -o output_bbox.gpkg
95
+ ```
96
+
97
+ Generate aligned bounding boxes with different x and y resolutions:
98
+
99
+ ```bash
100
+ sh-grid-builder data/aoi.geojson --resolution "(300,359)" --epsg 32632 --output-type bounding-box -o output_bbox.gpkg
101
+ ```
102
+
103
+ Generate pixelated geometry:
104
+
105
+ ```bash
106
+ sh-grid-builder data/aoi.geojson --resolution "10,10" --epsg 3035 --output-type pixelated -o output_pixelated.gpkg
107
+ ```
108
+
109
+ Example with geographic CRS (degrees):
110
+
111
+ ```bash
112
+ sh-grid-builder data/aoi.geojson --resolution "(0.001,0.001)" --epsg 4326 --output-type bounding-box -o output_bbox.gpkg
113
+ ```
114
+
115
+ ### Python API
116
+
117
+ You can also use the package programmatically:
118
+
119
+ ```python
120
+ from sh_batch_grid_builder import GeoData
121
+
122
+ # Initialize with AOI file, EPSG code, and resolutions (x, y)
123
+ geo_data = GeoData("path/to/aoi.geojson", epsg_code=3035, resolution_x=10.0, resolution_y=10.0)
124
+
125
+ # Or with different x and y resolutions
126
+ geo_data = GeoData("path/to/aoi.geojson", epsg_code=4326, resolution_x=0.002976190476204, resolution_y=0.002976190476204)
127
+
128
+ # Generate aligned bounding boxes
129
+ aligned_bboxes = geo_data.create_aligned_bounding_box(max_pixels=3500)
130
+
131
+ # Generate pixelated geometry (includes all pixels that touch/intersect the AOI)
132
+ pixelated_geom = geo_data.create_pixelated_geometry(max_pixels=3500)
133
+
134
+ # Save results
135
+ aligned_bboxes.to_file("output_bbox.gpkg", driver="GPKG")
136
+ pixelated_geom.to_file("output_pixelated.gpkg", driver="GPKG")
137
+ ```
138
+
139
+ ## How It Works
140
+
141
+ ### Aligned Bounding Boxes
142
+
143
+ The tool creates bounding boxes that are aligned to a grid based on:
144
+ 1. The specified X and Y resolutions (can be different for non-square pixels)
145
+ 2. The CRS origin (false easting/northing) for projected coordinate systems
146
+ 3. Automatic splitting when dimensions exceed 3500 pixels (fixed limit)
147
+
148
+ ### Pixelated Geometries
149
+
150
+ The pixelated geometry generation uses a raster-based approach:
151
+ 1. Converts the input geometry to a raster mask
152
+ 2. Polygonizes the raster back to vector format
153
+ 3. Automatically splits large geometries to avoid memory issues
154
+
155
+ This approach is much faster than vector-based methods for large grids. Pixelated
156
+ output includes any pixel that touches/intersects the AOI.
157
+
158
+ ## Requirements
159
+
160
+ - Python >= 3.8
161
+ - geopandas >= 0.12.0
162
+ - pyproj >= 3.4.0
163
+ - shapely >= 2.0.0
164
+ - rasterio >= 1.3.0
165
+ - numpy >= 1.21.0
166
+
167
+ ## Development
168
+
169
+ ### Running Tests
170
+
171
+ ```bash
172
+ pytest
173
+ ```
174
+
175
+ ### Building the Package
176
+
177
+ ```bash
178
+ python -m build
179
+ ```
180
+
181
+ ## License
182
+
183
+ MIT License
184
+
185
+ ## Contributing
186
+
187
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ sh_batch_grid_builder/__init__.py
4
+ sh_batch_grid_builder/cli.py
5
+ sh_batch_grid_builder/crs.py
6
+ sh_batch_grid_builder/geo.py
7
+ sh_batch_grid_builder.egg-info/PKG-INFO
8
+ sh_batch_grid_builder.egg-info/SOURCES.txt
9
+ sh_batch_grid_builder.egg-info/dependency_links.txt
10
+ sh_batch_grid_builder.egg-info/entry_points.txt
11
+ sh_batch_grid_builder.egg-info/requires.txt
12
+ sh_batch_grid_builder.egg-info/top_level.txt
13
+ tests/test_crs.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sh-grid-builder = sh_batch_grid_builder.cli:main
@@ -0,0 +1,9 @@
1
+ geopandas>=0.12.0
2
+ pyproj>=3.4.0
3
+ shapely>=2.0.0
4
+ rasterio>=1.3.0
5
+ numpy>=1.21.0
6
+
7
+ [dev]
8
+ pytest>=7.0.0
9
+ pytest-cov>=4.0.0
@@ -0,0 +1 @@
1
+ sh_batch_grid_builder
@@ -0,0 +1,31 @@
1
+ import pytest
2
+ from sh_batch_grid_builder.crs import get_crs_data
3
+
4
+
5
+ class TestGetCrsData:
6
+ """Test the get_crs_data function with known EPSG codes and their expected origins."""
7
+
8
+ def test_epsg_4326(self):
9
+ origin_x, origin_y = get_crs_data(4326)
10
+ assert origin_x == 0.0
11
+ assert origin_y == 0.0
12
+
13
+ def test_epsg_3857(self):
14
+ origin_x, origin_y = get_crs_data(3857)
15
+ assert origin_x == 0.0
16
+ assert origin_y == 0.0
17
+
18
+ def test_epsg_3035(self):
19
+ origin_x, origin_y = get_crs_data(3035)
20
+ assert origin_x == 4321000
21
+ assert origin_y == 3210000
22
+
23
+ def test_epsg_32633(self):
24
+ origin_x, origin_y = get_crs_data(32633)
25
+ assert origin_x == 500000
26
+ assert origin_y == 0
27
+
28
+ def test_epsg_2154(self):
29
+ origin_x, origin_y = get_crs_data(2154)
30
+ assert origin_x == 700000
31
+ assert origin_y == 6600000