async-geotiff 0.1.0b1__py3-none-any.whl
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.
- async_geotiff/__init__.py +10 -0
- async_geotiff/_crs.py +621 -0
- async_geotiff/_geotiff.py +405 -0
- async_geotiff/_overview.py +86 -0
- async_geotiff/_version.py +6 -0
- async_geotiff/enums.py +59 -0
- async_geotiff/py.typed +0 -0
- async_geotiff/tms.py +141 -0
- async_geotiff-0.1.0b1.dist-info/METADATA +35 -0
- async_geotiff-0.1.0b1.dist-info/RECORD +11 -0
- async_geotiff-0.1.0b1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python.
|
|
2
|
+
|
|
3
|
+
[cogeo]: https://cogeo.org/
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ._geotiff import GeoTIFF
|
|
7
|
+
from ._overview import Overview
|
|
8
|
+
from ._version import __version__
|
|
9
|
+
|
|
10
|
+
__all__ = ["GeoTIFF", "Overview", "__version__"]
|
async_geotiff/_crs.py
ADDED
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
"""Parse a pyproj CRS from a GeoKeyDirectory.
|
|
2
|
+
|
|
3
|
+
In the simple case, a GeoTIFF is defined by an EPSG code, which we can pass to
|
|
4
|
+
`pyproj.CRS.from_epsg`.
|
|
5
|
+
|
|
6
|
+
In the complex case, a GeoTIFF is defined by a set of geo keys that specify all
|
|
7
|
+
the parameters of a user-defined CRS. In this case, we need to build a PROJJSON
|
|
8
|
+
dictionary representing the CRS, which we can then pass to `pyproj.CRS.from_json_dict`.
|
|
9
|
+
|
|
10
|
+
Most of the code to construct a custom PROJJSON is generated by Claude. For any files
|
|
11
|
+
where CRS generation fails, we should add a new test case in `test_crs.py`.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from pyproj import CRS
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from async_tiff import GeoKeyDirectory
|
|
22
|
+
|
|
23
|
+
MODEL_TYPE_PROJECTED = 1
|
|
24
|
+
"""GeoTIFF model type constant for projected coordinate systems."""
|
|
25
|
+
|
|
26
|
+
MODEL_TYPE_GEOGRAPHIC = 2
|
|
27
|
+
"""GeoTIFF model type constant for geographic coordinate systems."""
|
|
28
|
+
|
|
29
|
+
USER_DEFINED_CRS = 32767
|
|
30
|
+
"""Sentinel value for user-defined CRS."""
|
|
31
|
+
|
|
32
|
+
# The two "public" functions of this module
|
|
33
|
+
__all__ = ["crs_from_geo_keys", "projjson_from_geo_keys"]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def crs_from_geo_keys(gkd: GeoKeyDirectory) -> CRS:
|
|
37
|
+
"""Parse a pyproj CRS from a GeoKeyDirectory.
|
|
38
|
+
|
|
39
|
+
Supports both EPSG-coded and user-defined CRSes.
|
|
40
|
+
"""
|
|
41
|
+
model_type = gkd.model_type
|
|
42
|
+
|
|
43
|
+
if model_type == MODEL_TYPE_PROJECTED:
|
|
44
|
+
crs = _projected_projection(gkd)
|
|
45
|
+
if isinstance(crs, int):
|
|
46
|
+
return CRS.from_epsg(crs)
|
|
47
|
+
|
|
48
|
+
return CRS.from_json_dict(crs)
|
|
49
|
+
|
|
50
|
+
if model_type == MODEL_TYPE_GEOGRAPHIC:
|
|
51
|
+
crs = _geographic_projection(gkd)
|
|
52
|
+
if isinstance(crs, int):
|
|
53
|
+
return CRS.from_epsg(crs)
|
|
54
|
+
|
|
55
|
+
return CRS.from_json_dict(crs)
|
|
56
|
+
|
|
57
|
+
raise ValueError(f"Unsupported GeoTIFF model type: {model_type}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def projjson_from_geo_keys(gkd: GeoKeyDirectory) -> dict:
|
|
61
|
+
"""Build a PROJJSON dict from a GeoKeyDirectory.
|
|
62
|
+
|
|
63
|
+
For EPSG-coded CRSes, resolves via pyproj and returns the canonical
|
|
64
|
+
PROJJSON.
|
|
65
|
+
|
|
66
|
+
For user-defined CRSes, constructs the PROJJSON from individual
|
|
67
|
+
geo key parameters.
|
|
68
|
+
"""
|
|
69
|
+
model_type = gkd.model_type
|
|
70
|
+
|
|
71
|
+
if model_type == MODEL_TYPE_PROJECTED:
|
|
72
|
+
crs = _projected_projection(gkd)
|
|
73
|
+
if isinstance(crs, int):
|
|
74
|
+
return CRS.from_epsg(crs).to_json_dict()
|
|
75
|
+
|
|
76
|
+
return crs
|
|
77
|
+
|
|
78
|
+
if model_type == MODEL_TYPE_GEOGRAPHIC:
|
|
79
|
+
crs = _geographic_projection(gkd)
|
|
80
|
+
if isinstance(crs, int):
|
|
81
|
+
return CRS.from_epsg(crs).to_json_dict()
|
|
82
|
+
|
|
83
|
+
return crs
|
|
84
|
+
|
|
85
|
+
raise ValueError(f"Unsupported GeoTIFF model type: {model_type}")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _geographic_projection(gkd: GeoKeyDirectory) -> int | dict:
|
|
89
|
+
"""Infer the geographic CRS from geo keys.
|
|
90
|
+
|
|
91
|
+
If an EPSG code is present, returns that integer. Otherwise, constructs
|
|
92
|
+
a user-defined geographic CRS PROJJSON dict.
|
|
93
|
+
"""
|
|
94
|
+
epsg = gkd.geographic_type
|
|
95
|
+
if epsg is not None and epsg != USER_DEFINED_CRS:
|
|
96
|
+
return epsg
|
|
97
|
+
|
|
98
|
+
return _build_user_defined_geographic_projjson(gkd)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _projected_projection(gkd: GeoKeyDirectory) -> int | dict:
|
|
102
|
+
"""Infer the projected CRS from geo keys.
|
|
103
|
+
|
|
104
|
+
If an EPSG code is present, returns that integer. Otherwise, constructs
|
|
105
|
+
a user-defined projected CRS PROJJSON dict.
|
|
106
|
+
"""
|
|
107
|
+
epsg = gkd.projected_type
|
|
108
|
+
if epsg is not None and epsg != USER_DEFINED_CRS:
|
|
109
|
+
return epsg
|
|
110
|
+
|
|
111
|
+
return _build_user_defined_projected_projjson(gkd)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _build_user_defined_geographic_projjson(gkd: GeoKeyDirectory) -> dict:
|
|
115
|
+
"""Build a user-defined geographic CRS PROJJSON dict.
|
|
116
|
+
|
|
117
|
+
Constructs PROJJSON from the ellipsoid, datum, prime meridian, and angular
|
|
118
|
+
unit parameters stored in the GeoKeyDirectory.
|
|
119
|
+
"""
|
|
120
|
+
# Build ellipsoid parameters
|
|
121
|
+
ellipsoid = _build_ellipsoid_params(gkd)
|
|
122
|
+
|
|
123
|
+
# Build prime meridian
|
|
124
|
+
pm_name = "Greenwich"
|
|
125
|
+
pm_longitude = 0.0
|
|
126
|
+
if (
|
|
127
|
+
gkd.geog_prime_meridian is not None
|
|
128
|
+
and gkd.geog_prime_meridian != USER_DEFINED_CRS
|
|
129
|
+
):
|
|
130
|
+
# Known prime meridian by EPSG code
|
|
131
|
+
pm_name = f"EPSG:{gkd.geog_prime_meridian}"
|
|
132
|
+
elif gkd.geog_prime_meridian_long is not None:
|
|
133
|
+
pm_longitude = gkd.geog_prime_meridian_long
|
|
134
|
+
pm_name = "User-defined"
|
|
135
|
+
|
|
136
|
+
# Build datum
|
|
137
|
+
datum_json: dict
|
|
138
|
+
if (
|
|
139
|
+
gkd.geog_geodetic_datum is not None
|
|
140
|
+
and gkd.geog_geodetic_datum != USER_DEFINED_CRS
|
|
141
|
+
):
|
|
142
|
+
datum_json = {
|
|
143
|
+
"type": "GeodeticReferenceFrame",
|
|
144
|
+
"name": f"Unknown datum based upon EPSG {gkd.geog_geodetic_datum} ellipsoid", # noqa: E501
|
|
145
|
+
}
|
|
146
|
+
else:
|
|
147
|
+
datum_json = {
|
|
148
|
+
"type": "GeodeticReferenceFrame",
|
|
149
|
+
"name": gkd.geog_citation or "User-defined",
|
|
150
|
+
"ellipsoid": ellipsoid,
|
|
151
|
+
"prime_meridian": {
|
|
152
|
+
"name": pm_name,
|
|
153
|
+
"longitude": pm_longitude,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"type": "GeographicCRS",
|
|
159
|
+
"$schema": "https://proj.org/schemas/v0.7/projjson.schema.json",
|
|
160
|
+
"name": gkd.geog_citation or "User-defined",
|
|
161
|
+
"datum": datum_json,
|
|
162
|
+
"coordinate_system": _geographic_cs(gkd),
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _build_user_defined_projected_projjson(gkd: GeoKeyDirectory) -> dict:
|
|
167
|
+
"""Build a user-defined projected CRS PROJJSON dict.
|
|
168
|
+
|
|
169
|
+
Constructs PROJJSON from the geographic base CRS and projection parameters
|
|
170
|
+
stored in the GeoKeyDirectory.
|
|
171
|
+
"""
|
|
172
|
+
# Build the base geographic CRS as PROJJSON
|
|
173
|
+
base_crs = _geographic_projection(gkd)
|
|
174
|
+
if isinstance(base_crs, int):
|
|
175
|
+
base_crs_json = CRS.from_epsg(base_crs).to_json_dict()
|
|
176
|
+
else:
|
|
177
|
+
base_crs_json = base_crs
|
|
178
|
+
|
|
179
|
+
# Build the coordinate operation (projection)
|
|
180
|
+
conversion = _build_conversion(gkd)
|
|
181
|
+
|
|
182
|
+
# Build the coordinate system for the projected CRS
|
|
183
|
+
cs = _projected_cs(gkd)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"type": "ProjectedCRS",
|
|
187
|
+
"$schema": "https://proj.org/schemas/v0.7/projjson.schema.json",
|
|
188
|
+
"name": gkd.proj_citation or "User-defined",
|
|
189
|
+
"base_crs": base_crs_json,
|
|
190
|
+
"conversion": conversion,
|
|
191
|
+
"coordinate_system": cs,
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _build_ellipsoid_params(gkd: GeoKeyDirectory) -> dict:
|
|
196
|
+
"""Build ellipsoid JSON parameters from geo keys."""
|
|
197
|
+
if gkd.geog_ellipsoid is not None and gkd.geog_ellipsoid != USER_DEFINED_CRS:
|
|
198
|
+
# Known ellipsoid by EPSG code — use parameters from geo keys if present
|
|
199
|
+
ellipsoid: dict = {"name": f"EPSG ellipsoid {gkd.geog_ellipsoid}"}
|
|
200
|
+
|
|
201
|
+
if gkd.geog_semi_major_axis is not None:
|
|
202
|
+
ellipsoid["semi_major_axis"] = gkd.geog_semi_major_axis
|
|
203
|
+
if gkd.geog_inv_flattening is not None:
|
|
204
|
+
ellipsoid["inverse_flattening"] = gkd.geog_inv_flattening
|
|
205
|
+
elif gkd.geog_semi_minor_axis is not None:
|
|
206
|
+
ellipsoid["semi_minor_axis"] = gkd.geog_semi_minor_axis
|
|
207
|
+
|
|
208
|
+
return ellipsoid
|
|
209
|
+
|
|
210
|
+
# Fully user-defined ellipsoid
|
|
211
|
+
semi_major = gkd.geog_semi_major_axis
|
|
212
|
+
if semi_major is None:
|
|
213
|
+
raise ValueError("User-defined ellipsoid requires geog_semi_major_axis")
|
|
214
|
+
|
|
215
|
+
user_ellipsoid: dict = {
|
|
216
|
+
"name": "User-defined",
|
|
217
|
+
"semi_major_axis": semi_major,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if gkd.geog_inv_flattening is not None:
|
|
221
|
+
user_ellipsoid["inverse_flattening"] = gkd.geog_inv_flattening
|
|
222
|
+
elif gkd.geog_semi_minor_axis is not None:
|
|
223
|
+
user_ellipsoid["semi_minor_axis"] = gkd.geog_semi_minor_axis
|
|
224
|
+
else:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
"User-defined ellipsoid requires geog_inv_flattening or geog_semi_minor_axis", # noqa: E501
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return user_ellipsoid
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# GeoTIFF coordinate transformation type codes (GeoKey 3075)
|
|
233
|
+
# Ref: http://geotiff.maptools.org/spec/geotiff6.html#6.3.3.3
|
|
234
|
+
CT_TRANSVERSE_MERCATOR = 1
|
|
235
|
+
CT_TRANSVERSE_MERCATOR_SOUTH = 2
|
|
236
|
+
CT_OBLIQUE_MERCATOR = 3
|
|
237
|
+
CT_OBLIQUE_MERCATOR_LABORDE = 4
|
|
238
|
+
CT_OBLIQUE_MERCATOR_ROSENMUND = 5
|
|
239
|
+
CT_OBLIQUE_MERCATOR_SPHERICAL = 6
|
|
240
|
+
CT_MERCATOR = 7
|
|
241
|
+
CT_LAMBERT_CONFORMAL_CONIC_2SP = 8
|
|
242
|
+
CT_LAMBERT_CONFORMAL_CONIC_1SP = 9
|
|
243
|
+
CT_LAMBERT_AZIMUTHAL_EQUAL_AREA = 10
|
|
244
|
+
CT_ALBERS_EQUAL_AREA = 11
|
|
245
|
+
CT_AZIMUTHAL_EQUIDISTANT = 12
|
|
246
|
+
CT_STEREOGRAPHIC = 14
|
|
247
|
+
CT_POLAR_STEREOGRAPHIC = 15
|
|
248
|
+
CT_OBLIQUE_STEREOGRAPHIC = 16
|
|
249
|
+
CT_EQUIRECTANGULAR = 17
|
|
250
|
+
CT_CASSINI_SOLDNER = 18
|
|
251
|
+
CT_ORTHOGRAPHIC = 21
|
|
252
|
+
CT_POLYCONIC = 22
|
|
253
|
+
CT_SINUSOIDAL = 24
|
|
254
|
+
CT_NEW_ZEALAND_MAP_GRID = 26
|
|
255
|
+
CT_TRANSVERSE_MERCATOR_SOUTH_ORIENTED = 27
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _build_conversion(gkd: GeoKeyDirectory) -> dict: # noqa: C901, PLR0912, PLR0915
|
|
259
|
+
"""Build a PROJ JSON conversion (coordinate operation) from geo keys."""
|
|
260
|
+
ct = gkd.proj_coord_trans
|
|
261
|
+
if ct is None:
|
|
262
|
+
raise ValueError("User-defined projected CRS requires proj_coord_trans")
|
|
263
|
+
|
|
264
|
+
# Helper to build a projection parameter with unit
|
|
265
|
+
def _angular(name: str, value: float | None, default: float = 0.0) -> dict:
|
|
266
|
+
return {
|
|
267
|
+
"name": name,
|
|
268
|
+
"value": value if value is not None else default,
|
|
269
|
+
"unit": "degree",
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
def _linear(name: str, value: float | None, default: float = 0.0) -> dict:
|
|
273
|
+
return {
|
|
274
|
+
"name": name,
|
|
275
|
+
"value": value if value is not None else default,
|
|
276
|
+
"unit": "metre",
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
def _scale(name: str, value: float | None, default: float = 1.0) -> dict:
|
|
280
|
+
return {
|
|
281
|
+
"name": name,
|
|
282
|
+
"value": value if value is not None else default,
|
|
283
|
+
"unit": "unity",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
name = "User-defined"
|
|
287
|
+
method: dict
|
|
288
|
+
parameters: list[dict]
|
|
289
|
+
|
|
290
|
+
if ct == CT_TRANSVERSE_MERCATOR:
|
|
291
|
+
name = "Transverse Mercator"
|
|
292
|
+
method = {"name": "Transverse Mercator"}
|
|
293
|
+
parameters = [
|
|
294
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
295
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
296
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_nat_origin),
|
|
297
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
298
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
elif ct == CT_TRANSVERSE_MERCATOR_SOUTH:
|
|
302
|
+
name = "Transverse Mercator (South Orientated)"
|
|
303
|
+
method = {"name": "Transverse Mercator (South Orientated)"}
|
|
304
|
+
parameters = [
|
|
305
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
306
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
307
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_nat_origin),
|
|
308
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
309
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
310
|
+
]
|
|
311
|
+
|
|
312
|
+
elif ct in (
|
|
313
|
+
CT_OBLIQUE_MERCATOR,
|
|
314
|
+
CT_OBLIQUE_MERCATOR_LABORDE,
|
|
315
|
+
CT_OBLIQUE_MERCATOR_ROSENMUND,
|
|
316
|
+
CT_OBLIQUE_MERCATOR_SPHERICAL,
|
|
317
|
+
):
|
|
318
|
+
name = "Hotine Oblique Mercator (variant B)"
|
|
319
|
+
method = {"name": "Hotine Oblique Mercator (variant B)"}
|
|
320
|
+
parameters = [
|
|
321
|
+
_angular("Latitude of projection centre", gkd.proj_center_lat),
|
|
322
|
+
_angular("Longitude of projection centre", gkd.proj_center_long),
|
|
323
|
+
_angular("Azimuth of initial line", gkd.proj_azimuth_angle),
|
|
324
|
+
_angular("Angle from Rectified to Skew Grid", gkd.proj_azimuth_angle),
|
|
325
|
+
_scale("Scale factor on initial line", gkd.proj_scale_at_center),
|
|
326
|
+
_linear("Easting at projection centre", gkd.proj_center_easting),
|
|
327
|
+
_linear("Northing at projection centre", gkd.proj_center_northing),
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
elif ct == CT_MERCATOR:
|
|
331
|
+
name = "Mercator (variant A)"
|
|
332
|
+
method = {"name": "Mercator (variant A)"}
|
|
333
|
+
parameters = [
|
|
334
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
335
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
336
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_nat_origin),
|
|
337
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
338
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
elif ct == CT_LAMBERT_CONFORMAL_CONIC_2SP:
|
|
342
|
+
name = "Lambert Conic Conformal (2SP)"
|
|
343
|
+
method = {"name": "Lambert Conic Conformal (2SP)"}
|
|
344
|
+
parameters = [
|
|
345
|
+
_angular(
|
|
346
|
+
"Latitude of false origin",
|
|
347
|
+
gkd.proj_false_origin_lat or gkd.proj_nat_origin_lat,
|
|
348
|
+
),
|
|
349
|
+
_angular(
|
|
350
|
+
"Longitude of false origin",
|
|
351
|
+
gkd.proj_false_origin_long or gkd.proj_nat_origin_long,
|
|
352
|
+
),
|
|
353
|
+
_angular("Latitude of 1st standard parallel", gkd.proj_std_parallel1),
|
|
354
|
+
_angular("Latitude of 2nd standard parallel", gkd.proj_std_parallel2),
|
|
355
|
+
_linear(
|
|
356
|
+
"Easting at false origin",
|
|
357
|
+
gkd.proj_false_origin_easting or gkd.proj_false_easting,
|
|
358
|
+
),
|
|
359
|
+
_linear(
|
|
360
|
+
"Northing at false origin",
|
|
361
|
+
gkd.proj_false_origin_northing or gkd.proj_false_northing,
|
|
362
|
+
),
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
elif ct == CT_LAMBERT_CONFORMAL_CONIC_1SP:
|
|
366
|
+
name = "Lambert Conic Conformal (1SP)"
|
|
367
|
+
method = {"name": "Lambert Conic Conformal (1SP)"}
|
|
368
|
+
parameters = [
|
|
369
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
370
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
371
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_nat_origin),
|
|
372
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
373
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
374
|
+
]
|
|
375
|
+
|
|
376
|
+
elif ct == CT_LAMBERT_AZIMUTHAL_EQUAL_AREA:
|
|
377
|
+
name = "Lambert Azimuthal Equal Area"
|
|
378
|
+
method = {"name": "Lambert Azimuthal Equal Area"}
|
|
379
|
+
parameters = [
|
|
380
|
+
_angular("Latitude of natural origin", gkd.proj_center_lat),
|
|
381
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
382
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
383
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
elif ct == CT_ALBERS_EQUAL_AREA:
|
|
387
|
+
name = "Albers Equal Area"
|
|
388
|
+
method = {"name": "Albers Equal Area"}
|
|
389
|
+
parameters = [
|
|
390
|
+
_angular(
|
|
391
|
+
"Latitude of false origin",
|
|
392
|
+
gkd.proj_false_origin_lat or gkd.proj_nat_origin_lat,
|
|
393
|
+
),
|
|
394
|
+
_angular(
|
|
395
|
+
"Longitude of false origin",
|
|
396
|
+
gkd.proj_false_origin_long or gkd.proj_nat_origin_long,
|
|
397
|
+
),
|
|
398
|
+
_angular("Latitude of 1st standard parallel", gkd.proj_std_parallel1),
|
|
399
|
+
_angular("Latitude of 2nd standard parallel", gkd.proj_std_parallel2),
|
|
400
|
+
_linear(
|
|
401
|
+
"Easting at false origin",
|
|
402
|
+
gkd.proj_false_origin_easting or gkd.proj_false_easting,
|
|
403
|
+
),
|
|
404
|
+
_linear(
|
|
405
|
+
"Northing at false origin",
|
|
406
|
+
gkd.proj_false_origin_northing or gkd.proj_false_northing,
|
|
407
|
+
),
|
|
408
|
+
]
|
|
409
|
+
|
|
410
|
+
elif ct == CT_AZIMUTHAL_EQUIDISTANT:
|
|
411
|
+
name = "Modified Azimuthal Equidistant"
|
|
412
|
+
method = {"name": "Modified Azimuthal Equidistant"}
|
|
413
|
+
parameters = [
|
|
414
|
+
_angular("Latitude of natural origin", gkd.proj_center_lat),
|
|
415
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
416
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
417
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
elif ct == CT_STEREOGRAPHIC:
|
|
421
|
+
name = "Stereographic"
|
|
422
|
+
method = {"name": "Stereographic"}
|
|
423
|
+
parameters = [
|
|
424
|
+
_angular("Latitude of natural origin", gkd.proj_center_lat),
|
|
425
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
426
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_center),
|
|
427
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
428
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
429
|
+
]
|
|
430
|
+
|
|
431
|
+
elif ct == CT_POLAR_STEREOGRAPHIC:
|
|
432
|
+
name = "Polar Stereographic (variant B)"
|
|
433
|
+
method = {"name": "Polar Stereographic (variant B)"}
|
|
434
|
+
parameters = [
|
|
435
|
+
_angular(
|
|
436
|
+
"Latitude of standard parallel",
|
|
437
|
+
gkd.proj_nat_origin_lat or gkd.proj_std_parallel1,
|
|
438
|
+
),
|
|
439
|
+
_angular(
|
|
440
|
+
"Longitude of origin",
|
|
441
|
+
gkd.proj_straight_vert_pole_long or gkd.proj_nat_origin_long,
|
|
442
|
+
),
|
|
443
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
444
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
elif ct == CT_OBLIQUE_STEREOGRAPHIC:
|
|
448
|
+
name = "Oblique Stereographic"
|
|
449
|
+
method = {"name": "Oblique Stereographic"}
|
|
450
|
+
parameters = [
|
|
451
|
+
_angular("Latitude of natural origin", gkd.proj_center_lat),
|
|
452
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
453
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_center),
|
|
454
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
455
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
elif ct == CT_EQUIRECTANGULAR:
|
|
459
|
+
name = "Equidistant Cylindrical"
|
|
460
|
+
method = {"name": "Equidistant Cylindrical"}
|
|
461
|
+
parameters = [
|
|
462
|
+
_angular(
|
|
463
|
+
"Latitude of 1st standard parallel",
|
|
464
|
+
gkd.proj_std_parallel1 or gkd.proj_center_lat,
|
|
465
|
+
),
|
|
466
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
467
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
468
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
469
|
+
]
|
|
470
|
+
|
|
471
|
+
elif ct == CT_CASSINI_SOLDNER:
|
|
472
|
+
name = "Cassini-Soldner"
|
|
473
|
+
method = {"name": "Cassini-Soldner"}
|
|
474
|
+
parameters = [
|
|
475
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
476
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
477
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
478
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
479
|
+
]
|
|
480
|
+
|
|
481
|
+
elif ct == CT_POLYCONIC:
|
|
482
|
+
name = "American Polyconic"
|
|
483
|
+
method = {"name": "American Polyconic"}
|
|
484
|
+
parameters = [
|
|
485
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
486
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
487
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
488
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
elif ct == CT_SINUSOIDAL:
|
|
492
|
+
name = "Sinusoidal"
|
|
493
|
+
method = {"name": "Sinusoidal"}
|
|
494
|
+
parameters = [
|
|
495
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
496
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
497
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
498
|
+
]
|
|
499
|
+
|
|
500
|
+
elif ct == CT_ORTHOGRAPHIC:
|
|
501
|
+
name = "Orthographic"
|
|
502
|
+
method = {"name": "Orthographic"}
|
|
503
|
+
parameters = [
|
|
504
|
+
_angular("Latitude of natural origin", gkd.proj_center_lat),
|
|
505
|
+
_angular("Longitude of natural origin", gkd.proj_center_long),
|
|
506
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
507
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
elif ct == CT_NEW_ZEALAND_MAP_GRID:
|
|
511
|
+
name = "New Zealand Map Grid"
|
|
512
|
+
method = {"name": "New Zealand Map Grid"}
|
|
513
|
+
parameters = [
|
|
514
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
515
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
516
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
517
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
518
|
+
]
|
|
519
|
+
|
|
520
|
+
elif ct == CT_TRANSVERSE_MERCATOR_SOUTH_ORIENTED:
|
|
521
|
+
name = "Transverse Mercator (South Orientated)"
|
|
522
|
+
method = {"name": "Transverse Mercator (South Orientated)"}
|
|
523
|
+
parameters = [
|
|
524
|
+
_angular("Latitude of natural origin", gkd.proj_nat_origin_lat),
|
|
525
|
+
_angular("Longitude of natural origin", gkd.proj_nat_origin_long),
|
|
526
|
+
_scale("Scale factor at natural origin", gkd.proj_scale_at_nat_origin),
|
|
527
|
+
_linear("False easting", gkd.proj_false_easting),
|
|
528
|
+
_linear("False northing", gkd.proj_false_northing),
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
else:
|
|
532
|
+
raise ValueError(f"Unsupported coordinate transformation type: {ct}")
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
"name": name,
|
|
536
|
+
"method": method,
|
|
537
|
+
"parameters": parameters,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
ANGULAR_UNIT_RADIAN = 9101
|
|
542
|
+
"""EPSG code for radian angular unit."""
|
|
543
|
+
|
|
544
|
+
ANGULAR_UNIT_DEGREE = 9102
|
|
545
|
+
"""EPSG code for degree angular unit."""
|
|
546
|
+
|
|
547
|
+
ANGULAR_UNIT_GRAD = 9105
|
|
548
|
+
"""EPSG code for grad angular unit."""
|
|
549
|
+
|
|
550
|
+
ANGULAR_UNIT: dict[int | None, str] = {
|
|
551
|
+
ANGULAR_UNIT_DEGREE: "degree",
|
|
552
|
+
ANGULAR_UNIT_RADIAN: "radian",
|
|
553
|
+
ANGULAR_UNIT_GRAD: "grad",
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def _geographic_cs(gkd: GeoKeyDirectory) -> dict:
|
|
558
|
+
"""Build a geographic coordinate system JSON dict from geo keys."""
|
|
559
|
+
angular_unit = ANGULAR_UNIT.get(
|
|
560
|
+
gkd.geog_angular_units,
|
|
561
|
+
"degree",
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
"subtype": "ellipsoidal",
|
|
566
|
+
"axis": [
|
|
567
|
+
{
|
|
568
|
+
"name": "Latitude",
|
|
569
|
+
"abbreviation": "lat",
|
|
570
|
+
"direction": "north",
|
|
571
|
+
"unit": angular_unit,
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
"name": "Longitude",
|
|
575
|
+
"abbreviation": "lon",
|
|
576
|
+
"direction": "east",
|
|
577
|
+
"unit": angular_unit,
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
LINEAR_UNIT_METRE = 9001
|
|
584
|
+
LINEAR_UNIT_FOOT = 9002
|
|
585
|
+
LINEAR_UNIT_US_SURVEY_FOOT = 9003
|
|
586
|
+
|
|
587
|
+
LINEAR_UNIT: dict[int | None, str | dict[str, str | float]] = {
|
|
588
|
+
LINEAR_UNIT_METRE: "metre",
|
|
589
|
+
LINEAR_UNIT_FOOT: "foot",
|
|
590
|
+
LINEAR_UNIT_US_SURVEY_FOOT: {
|
|
591
|
+
"type": "LinearUnit",
|
|
592
|
+
"name": "US survey foot",
|
|
593
|
+
"conversion_factor": 0.30480060960121924,
|
|
594
|
+
},
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def _projected_cs(gkd: GeoKeyDirectory) -> dict:
|
|
599
|
+
"""Build a projected coordinate system JSON dict from geo keys."""
|
|
600
|
+
linear_unit: str | dict[str, str | float] = LINEAR_UNIT.get(
|
|
601
|
+
gkd.proj_linear_units,
|
|
602
|
+
"metre",
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
"subtype": "Cartesian",
|
|
607
|
+
"axis": [
|
|
608
|
+
{
|
|
609
|
+
"name": "Easting",
|
|
610
|
+
"abbreviation": "E",
|
|
611
|
+
"direction": "east",
|
|
612
|
+
"unit": linear_unit,
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
"name": "Northing",
|
|
616
|
+
"abbreviation": "N",
|
|
617
|
+
"direction": "north",
|
|
618
|
+
"unit": linear_unit,
|
|
619
|
+
},
|
|
620
|
+
],
|
|
621
|
+
}
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import TYPE_CHECKING, Literal, Self
|
|
6
|
+
|
|
7
|
+
from affine import Affine
|
|
8
|
+
from async_tiff import TIFF
|
|
9
|
+
from async_tiff.enums import PhotometricInterpretation
|
|
10
|
+
|
|
11
|
+
from async_geotiff._crs import crs_from_geo_keys
|
|
12
|
+
from async_geotiff._overview import Overview
|
|
13
|
+
from async_geotiff.enums import Compression, Interleaving
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
|
|
18
|
+
import pyproj
|
|
19
|
+
from async_tiff import GeoKeyDirectory, ImageFileDirectory, ObspecInput
|
|
20
|
+
from async_tiff.store import ObjectStore # type: ignore # noqa: PGH003
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True, init=False, kw_only=True, repr=False)
|
|
24
|
+
class GeoTIFF:
|
|
25
|
+
"""A class representing a GeoTIFF image."""
|
|
26
|
+
|
|
27
|
+
_tiff: TIFF
|
|
28
|
+
"""The underlying async-tiff TIFF instance that we wrap.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_primary_ifd: ImageFileDirectory = field(init=False)
|
|
32
|
+
"""The primary (first) IFD of the GeoTIFF.
|
|
33
|
+
|
|
34
|
+
Some tags, like most geo tags, only exist on the primary IFD.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_gkd: GeoKeyDirectory = field(init=False)
|
|
38
|
+
"""The GeoKeyDirectory of the primary IFD.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
_overviews: list[Overview] = field(init=False)
|
|
42
|
+
"""A list of overviews for the GeoTIFF.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, tiff: TIFF) -> None:
|
|
46
|
+
"""Create a GeoTIFF from an existing TIFF instance."""
|
|
47
|
+
first_ifd = tiff.ifds[0]
|
|
48
|
+
gkd = first_ifd.geo_key_directory
|
|
49
|
+
|
|
50
|
+
# Validate that this is indeed a GeoTIFF
|
|
51
|
+
if gkd is None:
|
|
52
|
+
raise ValueError("TIFF does not contain GeoTIFF keys")
|
|
53
|
+
|
|
54
|
+
if len(tiff.ifds) == 0:
|
|
55
|
+
raise ValueError("TIFF does not contain any IFDs")
|
|
56
|
+
|
|
57
|
+
# We use object.__setattr__ because the dataclass is frozen
|
|
58
|
+
object.__setattr__(self, "_tiff", tiff)
|
|
59
|
+
object.__setattr__(self, "_primary_ifd", first_ifd)
|
|
60
|
+
object.__setattr__(self, "_gkd", gkd)
|
|
61
|
+
|
|
62
|
+
# Skip the first IFD, since it's the primary image
|
|
63
|
+
ifd_idx = 1
|
|
64
|
+
overviews: list[Overview] = []
|
|
65
|
+
while True:
|
|
66
|
+
try:
|
|
67
|
+
data_ifd = (ifd_idx, tiff.ifds[ifd_idx])
|
|
68
|
+
except IndexError:
|
|
69
|
+
# No more IFDs
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
ifd_idx += 1
|
|
73
|
+
|
|
74
|
+
mask_ifd = None
|
|
75
|
+
next_ifd = None
|
|
76
|
+
try:
|
|
77
|
+
next_ifd = tiff.ifds[ifd_idx]
|
|
78
|
+
except IndexError:
|
|
79
|
+
# No more IFDs
|
|
80
|
+
pass
|
|
81
|
+
finally:
|
|
82
|
+
if next_ifd is not None and is_mask_ifd(next_ifd):
|
|
83
|
+
mask_ifd = (ifd_idx, next_ifd)
|
|
84
|
+
ifd_idx += 1
|
|
85
|
+
|
|
86
|
+
ovr = Overview._create( # noqa: SLF001
|
|
87
|
+
geotiff=self,
|
|
88
|
+
gkd=gkd,
|
|
89
|
+
ifd=data_ifd,
|
|
90
|
+
mask_ifd=mask_ifd,
|
|
91
|
+
)
|
|
92
|
+
overviews.append(ovr)
|
|
93
|
+
|
|
94
|
+
object.__setattr__(self, "_overviews", overviews)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
async def open(
|
|
98
|
+
cls,
|
|
99
|
+
path: str,
|
|
100
|
+
*,
|
|
101
|
+
store: ObjectStore | ObspecInput,
|
|
102
|
+
prefetch: int = 32768,
|
|
103
|
+
multiplier: float = 2.0,
|
|
104
|
+
) -> Self:
|
|
105
|
+
"""Open a new GeoTIFF.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
path: The path within the store to read from.
|
|
109
|
+
store: The backend to use for data fetching.
|
|
110
|
+
prefetch: The number of initial bytes to read up front.
|
|
111
|
+
multiplier: The multiplier to use for readahead size growth. Must be
|
|
112
|
+
greater than 1.0. For example, for a value of `2.0`, the first metadata
|
|
113
|
+
read will be of size `prefetch`, and then the next read will be of size
|
|
114
|
+
`prefetch * 2`.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
A TIFF instance.
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
tiff = await TIFF.open(
|
|
121
|
+
path=path,
|
|
122
|
+
store=store,
|
|
123
|
+
prefetch=prefetch,
|
|
124
|
+
multiplier=multiplier,
|
|
125
|
+
)
|
|
126
|
+
return cls(tiff)
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def block_shapes(self) -> list[tuple[int, int]]:
|
|
130
|
+
"""An ordered list of block shapes for each bands.
|
|
131
|
+
|
|
132
|
+
Shapes are tuples and have the same ordering as the dataset's shape:
|
|
133
|
+
|
|
134
|
+
- (count of image rows, count of image columns).
|
|
135
|
+
"""
|
|
136
|
+
raise NotImplementedError
|
|
137
|
+
|
|
138
|
+
def block_size(self, bidx: int, i: int, j: int) -> int:
|
|
139
|
+
"""Return the size in bytes of a particular block.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
bidx: Band index, starting with 1.
|
|
143
|
+
i: Row index of the block, starting with 0.
|
|
144
|
+
j: Column index of the block, starting with 0.
|
|
145
|
+
|
|
146
|
+
"""
|
|
147
|
+
raise NotImplementedError
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
150
|
+
def bounds(self) -> tuple[float, float, float, float]:
|
|
151
|
+
"""Return the bounds of the dataset in the units of its CRS.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
(lower left x, lower left y, upper right x, upper right y)
|
|
155
|
+
|
|
156
|
+
"""
|
|
157
|
+
transform = self.transform
|
|
158
|
+
|
|
159
|
+
# Hopefully types will be fixed with affine 3.0
|
|
160
|
+
(left, top) = transform * (0, 0) # type: ignore # noqa: PGH003
|
|
161
|
+
(right, bottom) = transform * (self.width, self.height) # type: ignore # noqa: PGH003
|
|
162
|
+
|
|
163
|
+
return (left, bottom, right, top)
|
|
164
|
+
|
|
165
|
+
@property
|
|
166
|
+
def colorinterp(self) -> list[str]:
|
|
167
|
+
"""The color interpretation of each band in index order."""
|
|
168
|
+
# TODO: we should return an enum here. The enum should match rasterio.
|
|
169
|
+
# https://github.com/developmentseed/async-geotiff/issues/12
|
|
170
|
+
raise NotImplementedError
|
|
171
|
+
|
|
172
|
+
def colormap(self, bidx: int) -> dict[int, tuple[int, int, int]]:
|
|
173
|
+
"""Return a dict containing the colormap for a band.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
bidx: The 1-based index of the band whose colormap will be returned.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Mapping of color index value (starting at 0) to RGBA color as a
|
|
180
|
+
4-element tuple.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
ValueError
|
|
184
|
+
If no colormap is found for the specified band (NULL color table).
|
|
185
|
+
IndexError
|
|
186
|
+
If no band exists for the provided index.
|
|
187
|
+
|
|
188
|
+
"""
|
|
189
|
+
raise NotImplementedError
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def compression(self) -> Compression:
|
|
193
|
+
"""The compression algorithm used for the dataset."""
|
|
194
|
+
# TODO: should return an enum. The enum should match rasterio.
|
|
195
|
+
# https://github.com/developmentseed/async-geotiff/issues/12
|
|
196
|
+
# Also, is there ever a case where overviews have a different compression from
|
|
197
|
+
# the base image?
|
|
198
|
+
# Should we diverge from rasterio and not have this as a property returning a
|
|
199
|
+
# single string?
|
|
200
|
+
raise NotImplementedError
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def count(self) -> int:
|
|
204
|
+
"""The number of raster bands in the full image."""
|
|
205
|
+
raise NotImplementedError
|
|
206
|
+
|
|
207
|
+
@cached_property
|
|
208
|
+
def crs(self) -> pyproj.CRS:
|
|
209
|
+
"""The dataset's coordinate reference system."""
|
|
210
|
+
return crs_from_geo_keys(self._gkd)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def dtypes(self) -> list[str]:
|
|
214
|
+
"""The data types of each band in index order."""
|
|
215
|
+
# TODO: not sure what the return type should be. Perhaps we should define a
|
|
216
|
+
# `DataType` enum?
|
|
217
|
+
# https://github.com/developmentseed/async-geotiff/issues/20
|
|
218
|
+
raise NotImplementedError
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def height(self) -> int:
|
|
222
|
+
"""The height (number of rows) of the full image."""
|
|
223
|
+
return self._primary_ifd.image_height
|
|
224
|
+
|
|
225
|
+
def index(
|
|
226
|
+
self,
|
|
227
|
+
x: float,
|
|
228
|
+
y: float,
|
|
229
|
+
op: Callable[[float, float], tuple[int, int]] | None = None,
|
|
230
|
+
) -> tuple[int, int]:
|
|
231
|
+
"""Get the (row, col) index of the pixel containing (x, y).
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
x: x value in coordinate reference system
|
|
235
|
+
y: y value in coordinate reference system
|
|
236
|
+
op: function, optional (default: numpy.floor)
|
|
237
|
+
Function to convert fractional pixels to whole numbers
|
|
238
|
+
(floor, ceiling, round)
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
(row index, col index)
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
raise NotImplementedError
|
|
245
|
+
|
|
246
|
+
def indexes(self) -> list[int]:
|
|
247
|
+
"""Return the 1-based indexes of each band in the dataset.
|
|
248
|
+
|
|
249
|
+
For a 3-band dataset, this property will be [1, 2, 3].
|
|
250
|
+
"""
|
|
251
|
+
return list(range(1, self._primary_ifd.samples_per_pixel + 1))
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def interleaving(self) -> Interleaving:
|
|
255
|
+
"""The interleaving scheme of the dataset."""
|
|
256
|
+
# TODO: Should return an enum.
|
|
257
|
+
# https://rasterio.readthedocs.io/en/stable/api/rasterio.enums.html#rasterio.enums.Interleaving
|
|
258
|
+
raise NotImplementedError
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def is_tiled(self) -> bool:
|
|
262
|
+
"""Check if the dataset is tiled."""
|
|
263
|
+
raise NotImplementedError
|
|
264
|
+
|
|
265
|
+
@property
|
|
266
|
+
def nodata(self) -> float | None:
|
|
267
|
+
"""The dataset's single nodata value."""
|
|
268
|
+
nodata = self._primary_ifd.gdal_nodata
|
|
269
|
+
if nodata is None:
|
|
270
|
+
return None
|
|
271
|
+
|
|
272
|
+
return float(nodata)
|
|
273
|
+
|
|
274
|
+
@property
|
|
275
|
+
def overviews(self) -> list[Overview]:
|
|
276
|
+
"""A list of overview levels for the dataset."""
|
|
277
|
+
return self._overviews
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def photometric(self) -> PhotometricInterpretation | None:
|
|
281
|
+
"""The photometric interpretation of the dataset."""
|
|
282
|
+
# TODO: should return enum
|
|
283
|
+
# https://rasterio.readthedocs.io/en/stable/api/rasterio.enums.html#rasterio.enums.PhotometricInterp
|
|
284
|
+
raise NotImplementedError
|
|
285
|
+
|
|
286
|
+
@cached_property
|
|
287
|
+
def res(self) -> tuple[float, float]:
|
|
288
|
+
"""Return the (width, height) of pixels in the units of its CRS."""
|
|
289
|
+
transform = self.transform
|
|
290
|
+
return (transform.a, -transform.e)
|
|
291
|
+
|
|
292
|
+
@property
|
|
293
|
+
def shape(self) -> tuple[int, int]:
|
|
294
|
+
"""Get the shape (height, width) of the full image."""
|
|
295
|
+
return (self.height, self.width)
|
|
296
|
+
|
|
297
|
+
@cached_property
|
|
298
|
+
def transform(self) -> Affine:
|
|
299
|
+
"""Return the dataset's georeferencing transformation matrix.
|
|
300
|
+
|
|
301
|
+
This transform maps pixel row/column coordinates to coordinates in the dataset's
|
|
302
|
+
CRS.
|
|
303
|
+
"""
|
|
304
|
+
if (tie_points := self._primary_ifd.model_tiepoint) and (
|
|
305
|
+
model_scale := self._primary_ifd.model_pixel_scale
|
|
306
|
+
):
|
|
307
|
+
x_origin = tie_points[3]
|
|
308
|
+
y_origin = tie_points[4]
|
|
309
|
+
x_resolution = model_scale[0]
|
|
310
|
+
y_resolution = -model_scale[1]
|
|
311
|
+
|
|
312
|
+
return Affine(x_resolution, 0, x_origin, 0, y_resolution, y_origin)
|
|
313
|
+
|
|
314
|
+
if model_transformation := self._primary_ifd.model_transformation:
|
|
315
|
+
# ModelTransformation is a 4x4 matrix in row-major order
|
|
316
|
+
# [0 1 2 3 ] [a b 0 c]
|
|
317
|
+
# [4 5 6 7 ] = [d e 0 f]
|
|
318
|
+
# [8 9 10 11] [0 0 1 0]
|
|
319
|
+
# [12 13 14 15] [0 0 0 1]
|
|
320
|
+
x_origin = model_transformation[3]
|
|
321
|
+
y_origin = model_transformation[7]
|
|
322
|
+
row_rotation = model_transformation[1]
|
|
323
|
+
col_rotation = model_transformation[4]
|
|
324
|
+
|
|
325
|
+
# TODO: confirm these are correct
|
|
326
|
+
# Why does geotiff.js square and then square-root them?
|
|
327
|
+
# https://github.com/developmentseed/async-geotiff/issues/7
|
|
328
|
+
x_resolution = model_transformation[0]
|
|
329
|
+
y_resolution = -model_transformation[5]
|
|
330
|
+
|
|
331
|
+
return Affine(
|
|
332
|
+
model_transformation[0],
|
|
333
|
+
row_rotation,
|
|
334
|
+
x_origin,
|
|
335
|
+
col_rotation,
|
|
336
|
+
model_transformation[5],
|
|
337
|
+
y_origin,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
raise ValueError("The image does not have an affine transformation.")
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def width(self) -> int:
|
|
344
|
+
"""The width (number of columns) of the full image."""
|
|
345
|
+
return self._primary_ifd.image_width
|
|
346
|
+
|
|
347
|
+
def xy(
|
|
348
|
+
self,
|
|
349
|
+
row: int,
|
|
350
|
+
col: int,
|
|
351
|
+
offset: Literal["center", "ul", "ur", "ll", "lr"] | str = "center",
|
|
352
|
+
) -> tuple[float, float]:
|
|
353
|
+
"""Get the coordinates x, y of a pixel at row, col.
|
|
354
|
+
|
|
355
|
+
The pixel's center is returned by default, but a corner can be returned
|
|
356
|
+
by setting `offset` to one of `"ul"`, `"ur"`, `"ll"`, `"lr"`.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
row: Pixel row.
|
|
360
|
+
col: Pixel column.
|
|
361
|
+
offset: Determines if the returned coordinates are for the center of the
|
|
362
|
+
pixel or for a corner.
|
|
363
|
+
|
|
364
|
+
"""
|
|
365
|
+
transform = self.transform
|
|
366
|
+
|
|
367
|
+
if offset == "center":
|
|
368
|
+
c = col + 0.5
|
|
369
|
+
r = row + 0.5
|
|
370
|
+
elif offset == "ul":
|
|
371
|
+
c = col
|
|
372
|
+
r = row
|
|
373
|
+
elif offset == "ur":
|
|
374
|
+
c = col + 1
|
|
375
|
+
r = row
|
|
376
|
+
elif offset == "ll":
|
|
377
|
+
c = col
|
|
378
|
+
r = row + 1
|
|
379
|
+
elif offset == "lr":
|
|
380
|
+
c = col + 1
|
|
381
|
+
r = row + 1
|
|
382
|
+
else:
|
|
383
|
+
raise ValueError(f"Invalid offset value: {offset}")
|
|
384
|
+
|
|
385
|
+
return transform * (c, r)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def has_geokeys(ifd: ImageFileDirectory) -> bool:
|
|
389
|
+
"""Check if an IFD has GeoTIFF keys.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
ifd: The IFD to check.
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
return ifd.geo_key_directory is not None
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def is_mask_ifd(ifd: ImageFileDirectory) -> bool:
|
|
399
|
+
"""Check if an IFD is a mask IFD."""
|
|
400
|
+
return (
|
|
401
|
+
ifd.compression == Compression.deflate
|
|
402
|
+
and ifd.new_subfile_type is not None
|
|
403
|
+
and ifd.new_subfile_type & 4 != 0
|
|
404
|
+
and ifd.photometric_interpretation == PhotometricInterpretation.TransparencyMask
|
|
405
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from affine import Affine
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from async_tiff import GeoKeyDirectory, ImageFileDirectory
|
|
11
|
+
|
|
12
|
+
from async_geotiff import GeoTIFF
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(init=False, frozen=True, kw_only=True, eq=False, repr=False)
|
|
16
|
+
class Overview:
|
|
17
|
+
"""An overview level of a Cloud-Optimized GeoTIFF image."""
|
|
18
|
+
|
|
19
|
+
_geotiff: GeoTIFF
|
|
20
|
+
"""A reference to the parent GeoTIFF object.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_gkd: GeoKeyDirectory
|
|
24
|
+
"""The GeoKeyDirectory of the primary IFD.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_ifd: tuple[int, ImageFileDirectory]
|
|
28
|
+
"""The IFD for this overview level.
|
|
29
|
+
|
|
30
|
+
(positional index of the IFD in the TIFF file, IFD object)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
_mask_ifd: tuple[int, ImageFileDirectory] | None
|
|
34
|
+
"""The IFD for the mask associated with this overview level, if any.
|
|
35
|
+
|
|
36
|
+
(positional index of the IFD in the TIFF file, IFD object)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def _create(
|
|
41
|
+
cls,
|
|
42
|
+
*,
|
|
43
|
+
geotiff: GeoTIFF,
|
|
44
|
+
gkd: GeoKeyDirectory,
|
|
45
|
+
ifd: tuple[int, ImageFileDirectory],
|
|
46
|
+
mask_ifd: tuple[int, ImageFileDirectory] | None,
|
|
47
|
+
) -> Overview:
|
|
48
|
+
instance = cls.__new__(cls)
|
|
49
|
+
|
|
50
|
+
# We use object.__setattr__ because the dataclass is frozen
|
|
51
|
+
object.__setattr__(instance, "_geotiff", geotiff)
|
|
52
|
+
object.__setattr__(instance, "_gkd", gkd)
|
|
53
|
+
object.__setattr__(instance, "_ifd", ifd)
|
|
54
|
+
object.__setattr__(instance, "_mask_ifd", mask_ifd)
|
|
55
|
+
|
|
56
|
+
return instance
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def height(self) -> int:
|
|
60
|
+
"""The height of the overview in pixels."""
|
|
61
|
+
return self._ifd[1].image_height
|
|
62
|
+
|
|
63
|
+
@cached_property
|
|
64
|
+
def transform(self) -> Affine:
|
|
65
|
+
"""The affine transform mapping pixel coordinates to geographic coordinates.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Affine: The affine transform.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
full_transform = self._geotiff.transform
|
|
72
|
+
|
|
73
|
+
overview_width = self._ifd[1].image_width
|
|
74
|
+
full_width = self._geotiff.width
|
|
75
|
+
overview_height = self._ifd[1].image_height
|
|
76
|
+
full_height = self._geotiff.height
|
|
77
|
+
|
|
78
|
+
scale_x = full_width / overview_width
|
|
79
|
+
scale_y = full_height / overview_height
|
|
80
|
+
|
|
81
|
+
return full_transform * Affine.scale(scale_x, scale_y)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def width(self) -> int:
|
|
85
|
+
"""The width of the overview in pixels."""
|
|
86
|
+
return self._ifd[1].image_width
|
async_geotiff/enums.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Enums used by async_geotiff."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum, IntEnum
|
|
4
|
+
|
|
5
|
+
# ruff: noqa: D101
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# https://github.com/rasterio/rasterio/blob/2d79e5f3a00e919ecaa9573adba34a78274ce48c/rasterio/enums.py#L153-L174
|
|
9
|
+
class Compression(Enum):
|
|
10
|
+
"""Available compression algorithms for GeoTIFFs.
|
|
11
|
+
|
|
12
|
+
Note that compression options for EXR, MRF, etc are not included
|
|
13
|
+
in this enum.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
jpeg = "JPEG"
|
|
17
|
+
lzw = "LZW"
|
|
18
|
+
packbits = "PACKBITS"
|
|
19
|
+
deflate = "DEFLATE"
|
|
20
|
+
ccittrle = "CCITTRLE"
|
|
21
|
+
ccittfax3 = "CCITTFAX3"
|
|
22
|
+
ccittfax4 = "CCITTFAX4"
|
|
23
|
+
lzma = "LZMA"
|
|
24
|
+
none = "NONE"
|
|
25
|
+
zstd = "ZSTD"
|
|
26
|
+
lerc = "LERC"
|
|
27
|
+
lerc_deflate = "LERC_DEFLATE"
|
|
28
|
+
lerc_zstd = "LERC_ZSTD"
|
|
29
|
+
webp = "WEBP"
|
|
30
|
+
jpeg2000 = "JPEG2000"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# https://github.com/rasterio/rasterio/blob/2d79e5f3a00e919ecaa9573adba34a78274ce48c/rasterio/enums.py#L177-L182
|
|
34
|
+
class Interleaving(Enum):
|
|
35
|
+
pixel = "PIXEL"
|
|
36
|
+
line = "LINE"
|
|
37
|
+
band = "BAND"
|
|
38
|
+
#: tile requires GDAL 3.11+
|
|
39
|
+
tile = "TILE"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# https://github.com/rasterio/rasterio/blob/2d79e5f3a00e919ecaa9573adba34a78274ce48c/rasterio/enums.py#L185-L189
|
|
43
|
+
class MaskFlags(IntEnum):
|
|
44
|
+
all_valid = 1
|
|
45
|
+
per_dataset = 2
|
|
46
|
+
alpha = 4
|
|
47
|
+
nodata = 8
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# https://github.com/rasterio/rasterio/blob/2d79e5f3a00e919ecaa9573adba34a78274ce48c/rasterio/enums.py#L192-L200
|
|
51
|
+
class PhotometricInterp(Enum):
|
|
52
|
+
black = "MINISBLACK"
|
|
53
|
+
white = "MINISWHITE"
|
|
54
|
+
rgb = "RGB"
|
|
55
|
+
cmyk = "CMYK"
|
|
56
|
+
ycbcr = "YCbCr"
|
|
57
|
+
cielab = "CIELAB"
|
|
58
|
+
icclab = "ICCLAB"
|
|
59
|
+
itulab = "ITULAB"
|
async_geotiff/py.typed
ADDED
|
File without changes
|
async_geotiff/tms.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Generate a Tile Matrix Set from a GeoTIFF file, using morecantile."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from morecantile.commons import BoundingBox
|
|
10
|
+
from morecantile.models import (
|
|
11
|
+
CRS,
|
|
12
|
+
CRSWKT,
|
|
13
|
+
CRSUri,
|
|
14
|
+
TileMatrix,
|
|
15
|
+
TileMatrixSet,
|
|
16
|
+
TMSBoundingBox,
|
|
17
|
+
)
|
|
18
|
+
from morecantile.utils import meters_per_unit
|
|
19
|
+
from pydantic import AnyUrl
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from typing import Literal
|
|
23
|
+
|
|
24
|
+
import pyproj
|
|
25
|
+
|
|
26
|
+
from async_geotiff import GeoTIFF
|
|
27
|
+
|
|
28
|
+
_SCREEN_PIXEL_SIZE = 0.28e-3
|
|
29
|
+
|
|
30
|
+
__all__ = ["generate_tms"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_tms(
|
|
34
|
+
geotiff: GeoTIFF,
|
|
35
|
+
*,
|
|
36
|
+
id: str = str(uuid4()), # noqa: A002
|
|
37
|
+
) -> TileMatrixSet:
|
|
38
|
+
"""Generate a Tile Matrix Set from a GeoTIFF file.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
geotiff: The GeoTIFF file to generate the TMS from.
|
|
42
|
+
|
|
43
|
+
Keyword Args:
|
|
44
|
+
id: The ID to assign to the Tile Matrix Set.
|
|
45
|
+
|
|
46
|
+
"""
|
|
47
|
+
bounds = geotiff.bounds
|
|
48
|
+
crs = geotiff.crs
|
|
49
|
+
tr = geotiff.transform
|
|
50
|
+
blockxsize = geotiff._primary_ifd.tile_width # noqa: SLF001
|
|
51
|
+
blockysize = geotiff._primary_ifd.tile_height # noqa: SLF001
|
|
52
|
+
|
|
53
|
+
if blockxsize is None or blockysize is None:
|
|
54
|
+
raise ValueError("GeoTIFF must be tiled to generate a TMS.")
|
|
55
|
+
|
|
56
|
+
mpu = meters_per_unit(crs)
|
|
57
|
+
|
|
58
|
+
corner_of_origin: Literal["bottomLeft", "topLeft"] = (
|
|
59
|
+
"bottomLeft" if tr.e > 0 else "topLeft"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
tile_matrices: list[TileMatrix] = []
|
|
63
|
+
|
|
64
|
+
for idx, overview in enumerate(reversed(geotiff.overviews)):
|
|
65
|
+
overview_tr = overview.transform
|
|
66
|
+
blockxsize = overview._ifd[1].tile_width # noqa: SLF001
|
|
67
|
+
blockysize = overview._ifd[1].tile_height # noqa: SLF001
|
|
68
|
+
|
|
69
|
+
if blockxsize is None or blockysize is None:
|
|
70
|
+
raise ValueError("GeoTIFF overviews must be tiled to generate a TMS.")
|
|
71
|
+
|
|
72
|
+
tile_matrices.append(
|
|
73
|
+
TileMatrix(
|
|
74
|
+
id=str(idx),
|
|
75
|
+
scaleDenominator=overview_tr.a * mpu / _SCREEN_PIXEL_SIZE,
|
|
76
|
+
cellSize=overview_tr.a,
|
|
77
|
+
cornerOfOrigin=corner_of_origin,
|
|
78
|
+
pointOfOrigin=(overview_tr.c, overview_tr.f),
|
|
79
|
+
tileWidth=blockxsize,
|
|
80
|
+
tileHeight=blockysize,
|
|
81
|
+
matrixWidth=math.ceil(overview.width / blockxsize),
|
|
82
|
+
matrixHeight=math.ceil(overview.height / blockysize),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Add the full-resolution level last
|
|
87
|
+
tile_matrices.append(
|
|
88
|
+
TileMatrix(
|
|
89
|
+
id=str(len(geotiff.overviews)),
|
|
90
|
+
scaleDenominator=tr.a * mpu / _SCREEN_PIXEL_SIZE,
|
|
91
|
+
cellSize=tr.a,
|
|
92
|
+
cornerOfOrigin=corner_of_origin,
|
|
93
|
+
pointOfOrigin=(tr.c, tr.f),
|
|
94
|
+
tileWidth=blockxsize,
|
|
95
|
+
tileHeight=blockysize,
|
|
96
|
+
matrixWidth=math.ceil(geotiff.width / blockxsize),
|
|
97
|
+
matrixHeight=math.ceil(geotiff.height / blockysize),
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
bbox = BoundingBox(*bounds)
|
|
102
|
+
tms_crs = _parse_crs(crs)
|
|
103
|
+
|
|
104
|
+
return TileMatrixSet(
|
|
105
|
+
title="Generated TMS",
|
|
106
|
+
id=id,
|
|
107
|
+
crs=tms_crs,
|
|
108
|
+
boundingBox=TMSBoundingBox(
|
|
109
|
+
lowerLeft=(bbox.left, bbox.bottom),
|
|
110
|
+
upperRight=(bbox.right, bbox.top),
|
|
111
|
+
crs=tms_crs,
|
|
112
|
+
),
|
|
113
|
+
tileMatrices=tile_matrices,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _parse_crs(
|
|
118
|
+
crs: pyproj.CRS,
|
|
119
|
+
) -> CRS:
|
|
120
|
+
"""Parse a pyproj CRS into a morecantile CRSUri or CRSWKT.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
crs: The pyproj CRS to parse.
|
|
124
|
+
|
|
125
|
+
"""
|
|
126
|
+
if authority_code := crs.to_authority(min_confidence=20):
|
|
127
|
+
authority, code = authority_code
|
|
128
|
+
version = "0"
|
|
129
|
+
# if we have a version number in the authority, split it out
|
|
130
|
+
if "_" in authority:
|
|
131
|
+
authority, version = authority.split("_")
|
|
132
|
+
|
|
133
|
+
return CRS(
|
|
134
|
+
CRSUri(
|
|
135
|
+
uri=AnyUrl(
|
|
136
|
+
f"http://www.opengis.net/def/crs/{authority}/{version}/{code}",
|
|
137
|
+
),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return CRS(CRSWKT(wkt=crs.to_json_dict()))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: async-geotiff
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Async GeoTIFF reader for Python
|
|
5
|
+
Author: Kyle Barron
|
|
6
|
+
Author-email: Kyle Barron <kyle@developmentseed.org>
|
|
7
|
+
Requires-Dist: affine>=2.4.0
|
|
8
|
+
Requires-Dist: async-tiff>=0.4.0
|
|
9
|
+
Requires-Dist: numpy>2
|
|
10
|
+
Requires-Dist: pyproj>=3.7.2
|
|
11
|
+
Requires-Dist: morecantile>=7.0,<8.0 ; extra == 'morecantile'
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Provides-Extra: morecantile
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# async-geotiff
|
|
17
|
+
|
|
18
|
+
Async GeoTIFF and [Cloud-Optimized GeoTIFF][cogeo] (COG) reader for Python, wrapping [`async-tiff`].
|
|
19
|
+
|
|
20
|
+
[`async-tiff`]: https://github.com/developmentseed/async-tiff
|
|
21
|
+
[cogeo]: https://cogeo.org/
|
|
22
|
+
|
|
23
|
+
## Project Goals:
|
|
24
|
+
|
|
25
|
+
- Support only for GeoTIFF and Cloud-Optimized GeoTIFF (COG) formats
|
|
26
|
+
- Support for reading only, no writing support
|
|
27
|
+
- Full type hinting.
|
|
28
|
+
- API similar to rasterio where possible.
|
|
29
|
+
- We won't support the full rasterio API, but we'll try to when it's possible to implement rasterio APIs with straightforward maintenance requirements.
|
|
30
|
+
- For methods where we do intentionally try to match with rasterio, the tests should match against rasterio.
|
|
31
|
+
- Initially, we'll try to support a core set of GeoTIFF formats. Obscure GeoTIFF files may not be supported.
|
|
32
|
+
|
|
33
|
+
## References
|
|
34
|
+
|
|
35
|
+
- aiocogeo: https://github.com/geospatial-jeff/aiocogeo
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
async_geotiff/__init__.py,sha256=Z_g9Z04lgDNeIOI4Z3lXn74G_7MOntwcPgjWBYLIoUU,259
|
|
2
|
+
async_geotiff/_crs.py,sha256=upyEGDiuHyWniTD--blzujYHlXBvfHLCSPzgekvO26Q,22105
|
|
3
|
+
async_geotiff/_geotiff.py,sha256=hEC0hR5qTB0DGEe4Cvoaz_56D-zfZeT9UR4lmjJD77w,13123
|
|
4
|
+
async_geotiff/_overview.py,sha256=dsdbKpix3aebjtibfJSgLUbqSAY92tlMMcg3fVLFlfY,2423
|
|
5
|
+
async_geotiff/_version.py,sha256=WOBh553JMD6VV-k_R8km_U4yt5mzyrWhi4OeTSXNoJI,171
|
|
6
|
+
async_geotiff/enums.py,sha256=j-_K1UtI0tBzZ945auvhWZdktO1bgZp7Nf-K9c208IQ,1493
|
|
7
|
+
async_geotiff/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
async_geotiff/tms.py,sha256=55nXenfss_KX7I5bmcMwtTHZWzNYAwSqhfjqSQju7OI,3922
|
|
9
|
+
async_geotiff-0.1.0b1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
10
|
+
async_geotiff-0.1.0b1.dist-info/METADATA,sha256=gvy7Kywt1fjDiFl-HWxfmHHi3knCrHrC5MPzzoshMUc,1300
|
|
11
|
+
async_geotiff-0.1.0b1.dist-info/RECORD,,
|