Coverage for gws-app/gws/lib/gdalx/__init__.py: 89%

473 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-23 20:06 +0200

1"""GDAL/OGR wrapper. 

2 

3This package provides a thin layer over the GDAL Python bindings (``osgeo.gdal``, ``osgeo.ogr``) 

4for reading and writing raster and vector data sets. 

5 

6Data sets are opened with ``open_raster`` or ``open_vector``, or created in memory 

7from an image with ``open_from_image`` or from encoded image bytes with ``open_from_bytes``. If no driver name is given, the driver is chosen 

8by the file extension. Data sets are context managers that flush and close themselves on exit. 

9 

10- ``RasterDataSet`` reads a raster into a ``gws.Image``, reports its size and bounds, 

11 warps it to an image or a file (``gdal.Warp``), and saves copies in other formats. 

12- ``VectorDataSet`` gives access to ``VectorLayer`` objects, creates new layers and runs transactions. 

13- ``VectorLayer`` describes its columns, reads features as ``gws.FeatureRecord`` objects and inserts records. 

14 

15Attribute values are converted between OGR field types and ``gws.AttributeType``, 

16geometries between OGR and ``gws.Shape``. Geo-transforms always use the 

17easting/longitude-first axis order. 

18 

19Example:: 

20 

21 import gws.lib.gdalx 

22 

23 with gws.lib.gdalx.open_raster('/data/ortho.tif') as ds: 

24 bounds = ds.bounds() 

25 img = ds.to_image() 

26 

27 with gws.lib.gdalx.open_vector('/data/out.gpkg', 'w') as ds: 

28 la = ds.create_layer('roads', {'name': gws.AttributeType.str}, gws.GeometryType.linestring, crs) 

29 la.insert(records) 

30""" 

31 

32from typing import Any, Optional, Iterable, cast 

33 

34import datetime 

35import decimal 

36import contextlib 

37import numpy as np 

38 

39from osgeo import gdal 

40from osgeo import ogr 

41from osgeo import osr 

42 

43import gws 

44import gws.lib.shape 

45import gws.lib.crs 

46import gws.lib.bounds 

47import gws.lib.image 

48import gws.lib.datetimex as datetimex 

49 

50 

51class Error(gws.Error): 

52 """GDAL error.""" 

53 

54 pass 

55 

56 

57class DriverInfo(gws.Data): 

58 """Information about a GDAL driver.""" 

59 

60 index: int 

61 """Driver index in GDAL.""" 

62 name: str 

63 """Short driver name, like ``GTiff``.""" 

64 longName: str 

65 """Long driver name, like ``GeoTIFF``.""" 

66 extensions: list[str] 

67 """File extensions supported by the driver.""" 

68 metaData: dict 

69 """Driver metadata.""" 

70 

71 

72def get_drivers() -> list[DriverInfo]: 

73 """Enumerate GDAL drivers. 

74 

75 Returns: 

76 Information about all available drivers. 

77 """ 

78 

79 di = gws.u.get_app_global('gdal_driver_infos', _fetch_driver_infos) 

80 return di.infos 

81 

82 

83def get_driver(name: str) -> Optional[DriverInfo]: 

84 """Get driver info by name. 

85 

86 Args: 

87 name: Short driver name, like ``GTiff``. 

88 

89 Returns: 

90 Driver information, or ``None`` if the driver is not found. 

91 """ 

92 

93 for di in get_drivers(): 

94 if di.name == name: 

95 return di 

96 

97 

98def supported_attribute_types(): 

99 """Get attribute types that can be written to vector data sets. 

100 

101 Returns: 

102 A list of ``gws.AttributeType`` values. 

103 """ 

104 return list(_ATTR_TO_OGR.keys()) 

105 

106 

107@contextlib.contextmanager 

108def gdal_config(options: dict): 

109 """Context manager that temporarily sets GDAL config options. 

110 

111 The previous values are restored on exit. 

112 

113 Args: 

114 options: GDAL config options, like ``{'GDAL_CACHEMAX': '512'}``. 

115 """ 

116 

117 prev = {} 

118 for key, value in options.items(): 

119 prev[key] = gdal.GetConfigOption(key) 

120 gdal.SetConfigOption(key, value) 

121 

122 try: 

123 yield 

124 finally: 

125 for key, value in prev.items(): 

126 gdal.SetConfigOption(key, value) 

127 

128 

129def open_raster( 

130 path: str, 

131 driver: str = '', 

132 default_crs: Optional[gws.Crs] = None, 

133 options: dict = None, 

134) -> 'RasterDataSet': 

135 """Open a raster data set for reading. 

136 

137 Raster data sets are read-only. To write a raster, use ``RasterDataSet.save_as`` 

138 or ``RasterDataSet.warp_to_path``. 

139 

140 Args: 

141 path: File path. 

142 driver: Driver name. If omitted, the driver is chosen by the path extension. 

143 default_crs: CRS to use if the data set has none, web mercator by default. 

144 options: Driver-specific open options. 

145 

146 Returns: 

147 The raster data set. 

148 

149 Raises: 

150 ``Error``: If no suitable raster driver is found or the data set cannot be opened. 

151 """ 

152 

153 dso = _DataSetOptions( 

154 path=path, 

155 mode='r', 

156 driver=driver, 

157 defaultCrs=default_crs, 

158 gdalOpts=options or {}, 

159 ) 

160 

161 return cast(RasterDataSet, _open(dso, need_raster=True)) 

162 

163 

164def open_vector( 

165 path: str, 

166 mode: str = 'r', 

167 driver: str = '', 

168 encoding: Optional[str] = 'utf8', 

169 default_crs: Optional[gws.Crs] = None, 

170 geometry_as_text: bool = False, 

171 options: dict = None, 

172) -> 'VectorDataSet': 

173 """Open a vector data set. 

174 

175 Args: 

176 path: File path. 

177 mode: ``r`` (read), ``a`` (update) or ``w`` (create). 

178 driver: Driver name. If omitted, the driver is chosen by the path extension. 

179 encoding: Encoding of string attributes. If set, strings are decoded when reading, 

180 otherwise they are returned as bytes. 

181 default_crs: CRS for geometries without one, web mercator by default. 

182 geometry_as_text: Do not convert geometries to shapes, return them as EWKT in ``FeatureRecord.ewkt``. 

183 options: Driver-specific open or creation options. 

184 

185 Returns: 

186 The vector data set. 

187 

188 Raises: 

189 ``Error``: If the mode is invalid, no suitable vector driver is found, or the data set cannot be opened or created. 

190 """ 

191 

192 dso = _DataSetOptions( 

193 path=path, 

194 mode=mode, 

195 driver=driver, 

196 defaultCrs=default_crs, 

197 encoding=encoding, 

198 geometryAsText=geometry_as_text, 

199 gdalOpts=options or {}, 

200 ) 

201 

202 return cast(VectorDataSet, _open(dso, need_raster=False)) 

203 

204 

205def open_from_image( 

206 image: gws.Image, 

207 bounds: gws.Bounds, 

208 rotation: gws.Size = None, 

209 options: dict = None, 

210) -> 'RasterDataSet': 

211 """Create an in-memory raster data set from an image. 

212 

213 Args: 

214 image: Image object. 

215 bounds: Bounds of the image. 

216 rotation: Geo-transform rotation terms ``(x, y)``, no rotation by default. 

217 options: Driver-specific creation options. 

218 

219 Returns: 

220 The raster data set. 

221 """ 

222 

223 gdal.UseExceptions() 

224 

225 drv = gdal.GetDriverByName('MEM') 

226 img_array = image.to_array() 

227 band_count = img_array.shape[2] 

228 

229 gd = drv.Create( 

230 '', 

231 xsize=img_array.shape[1], 

232 ysize=img_array.shape[0], 

233 bands=band_count, 

234 eType=gdal.GDT_Byte, 

235 options=_option_list(options), 

236 ) 

237 for band in range(band_count): 

238 gd.GetRasterBand(band + 1).WriteArray(img_array[:, :, band]) 

239 

240 gt = _bounds_to_geotransform(bounds, (gd.RasterXSize, gd.RasterYSize), rotation) 

241 

242 gd.SetGeoTransform(gt) 

243 gd.SetSpatialRef(_srs_from_srid(bounds.crs.srid)) 

244 

245 dso = _DataSetOptions(path='') 

246 return RasterDataSet(dso, gd) 

247 

248 

249def open_from_bytes(content: bytes, bounds: gws.Bounds) -> 'RasterDataSet': 

250 """Create a raster data set from an encoded image. 

251 

252 The image is kept in GDAL's memory file system and decoded only when the data set is read. 

253 Paletted images are expanded to RGBA. 

254 

255 Args: 

256 content: Encoded image, in any format GDAL can read. 

257 bounds: Bounds of the image. 

258 

259 Returns: 

260 The raster data set. 

261 

262 Raises: 

263 ``Error``: If the image cannot be opened. 

264 """ 

265 

266 gdal.UseExceptions() 

267 

268 uid = gws.u.random_string(32) 

269 src_path = f'/vsimem/{uid}.img' 

270 vrt_path = f'/vsimem/{uid}.vrt' 

271 

272 gdal.FileFromMemBuffer(src_path, content) 

273 

274 try: 

275 src = gdal.Open(src_path) 

276 except RuntimeError as exc: 

277 gdal.Unlink(src_path) 

278 raise Error('cannot open image') from exc 

279 

280 ext = bounds.extent 

281 opts = dict( 

282 format='VRT', 

283 outputBounds=[ext[0], ext[3], ext[2], ext[1]], 

284 outputSRS=bounds.crs.epsg, 

285 ) 

286 if src.GetRasterBand(1).GetColorTable(): 

287 opts['rgbExpand'] = 'rgba' 

288 

289 gd = gdal.Translate(vrt_path, src, **opts) 

290 src = None 

291 

292 dso = _DataSetOptions(path=vrt_path, memPaths=[src_path, vrt_path]) 

293 return RasterDataSet(dso, gd) 

294 

295 

296## 

297 

298 

299class _DriverInfoCache(gws.Data): 

300 """Cached information about GDAL drivers.""" 

301 

302 infos: list[DriverInfo] 

303 """All drivers.""" 

304 extToName: dict 

305 """Map of file extensions to lists of driver names.""" 

306 vectorNames: set[str] 

307 """Names of vector drivers.""" 

308 rasterNames: set[str] 

309 """Names of raster drivers.""" 

310 

311 

312class _DataSetOptions(gws.Data): 

313 """Options a data set was opened with.""" 

314 

315 path: str 

316 """File path.""" 

317 mode: str 

318 """Open mode.""" 

319 driver: str 

320 """Driver name.""" 

321 encoding: str 

322 """Encoding of string attributes.""" 

323 defaultCrs: gws.Crs 

324 """CRS to use if the data has none.""" 

325 geometryAsText: bool 

326 """Return geometries as EWKT instead of shapes.""" 

327 gdalOpts: dict 

328 """Driver-specific options.""" 

329 memPaths: list[str] 

330 """Files in GDAL's memory file system, removed on close.""" 

331 

332 

333def _open(dso: _DataSetOptions, need_raster): 

334 if not dso.mode: 

335 dso.mode = 'r' 

336 if dso.mode not in 'rwa': 

337 raise Error(f'invalid open mode {dso.mode!r}') 

338 

339 gdal.UseExceptions() 

340 

341 drv = _driver_from_args(dso.path, dso.driver, need_raster) 

342 dso.defaultCrs = dso.defaultCrs or gws.lib.crs.WEBMERCATOR 

343 

344 if dso.mode == 'w': 

345 gd = drv.CreateDataSource(dso.path, _option_list(dso.gdalOpts)) 

346 if gd is None: 

347 raise Error(f'cannot create {dso.path!r}') 

348 return VectorDataSet(dso, gd) 

349 

350 flags = gdal.OF_VERBOSE_ERROR 

351 if dso.mode == 'r': 

352 flags += gdal.OF_READONLY 

353 if dso.mode == 'a': 

354 flags += gdal.OF_UPDATE 

355 if need_raster: 

356 flags += gdal.OF_RASTER 

357 else: 

358 flags += gdal.OF_VECTOR 

359 

360 gd = gdal.OpenEx(dso.path, flags, open_options=_option_list(dso.gdalOpts)) 

361 if gd is None: 

362 raise Error(f'cannot open {dso.path!r}') 

363 

364 if need_raster: 

365 return RasterDataSet(dso, gd) 

366 return VectorDataSet(dso, gd) 

367 

368 

369class _DataSet: 

370 """Base class for GDAL data sets.""" 

371 

372 gdDataset: gdal.Dataset 

373 """Underlying GDAL data set.""" 

374 gdDriver: gdal.Driver 

375 """Underlying GDAL driver.""" 

376 dso: _DataSetOptions 

377 """Options the data set was opened with.""" 

378 driverName: str 

379 """Driver name.""" 

380 

381 def __init__(self, dso: _DataSetOptions, gd_dataset): 

382 """Wrap a GDAL data set. 

383 

384 Args: 

385 dso: Options the data set was opened with. 

386 gd_dataset: GDAL data set. 

387 """ 

388 self.gdDataset = gd_dataset 

389 self.gdDriver = self.gdDataset.GetDriver() 

390 self.driverName = self.gdDriver.GetDescription() 

391 self.dso = dso 

392 

393 def __enter__(self): 

394 return self 

395 

396 def __exit__(self, exc_type, exc_val, exc_tb): 

397 self.close() 

398 return False 

399 

400 def close(self): 

401 """Flush the data set and release it.""" 

402 self.gdDataset.FlushCache() 

403 setattr(self, 'gdDataset', None) 

404 for p in self.dso.memPaths or []: 

405 gdal.Unlink(p) 

406 

407 def crs(self) -> Optional[gws.Crs]: 

408 """Get the CRS of the data set. 

409 

410 Returns: 

411 The CRS, or ``None`` if the data set has no CRS or it is unknown. 

412 """ 

413 srid = _srid_from_srs(self.gdDataset.GetSpatialRef()) 

414 return gws.lib.crs.get(srid) if srid else None 

415 

416 def set_crs(self, crs: gws.Crs): 

417 """Set the CRS of the data set. 

418 

419 Args: 

420 crs: The CRS. 

421 """ 

422 srs = _srs_from_srid(crs.srid) 

423 self.gdDataset.SetSpatialRef(srs) 

424 

425 

426class RasterDataSet(_DataSet): 

427 """Raster data set.""" 

428 

429 def to_image(self) -> gws.Image: 

430 """Convert the raster data set to an image. 

431 

432 Each raster band becomes an image channel, values are read as 8-bit. 

433 

434 Returns: 

435 The image. 

436 """ 

437 

438 band_count = self.gdDataset.RasterCount 

439 x_size = self.gdDataset.RasterXSize 

440 y_size = self.gdDataset.RasterYSize 

441 

442 arr_shape = (y_size, x_size, band_count) 

443 arr = np.zeros(arr_shape, dtype=np.uint8) 

444 

445 for band in range(band_count): 

446 gd_band = self.gdDataset.GetRasterBand(band + 1) 

447 arr[:, :, band] = gd_band.ReadAsArray(0, 0, x_size, y_size) 

448 

449 return gws.lib.image.from_array(arr) 

450 

451 def warp_to_image(self, options: dict) -> gws.Image: 

452 """Warp the data set in memory and return the result as an image. 

453 

454 See https://gdal.org/en/stable/api/python/utilities.html#osgeo.gdal.WarpOptions 

455 and https://gdal.org/en/stable/programs/gdalwarp.html for the options. 

456 

457 Args: 

458 options: Keyword arguments for ``gdal.Warp``. The ``format`` option is ignored. 

459 

460 Returns: 

461 The warped image. 

462 

463 Raises: 

464 ``Error``: If the warp fails. 

465 """ 

466 

467 gdal.UseExceptions() 

468 

469 options = dict(options) 

470 options['format'] = 'MEM' 

471 

472 gd = gdal.Warp('', self.gdDataset, **options) 

473 if gd is None: 

474 raise Error(f'warp failed') 

475 

476 return RasterDataSet(_DataSetOptions(path=''), gd).to_image() 

477 

478 def warp_to_path(self, path: str, options: dict): 

479 """Warp the data set and store it at the given path. 

480 

481 See https://gdal.org/en/stable/api/python/utilities.html#osgeo.gdal.WarpOptions 

482 and https://gdal.org/en/stable/programs/gdalwarp.html for the options. 

483 

484 Args: 

485 path: Destination path. 

486 options: Keyword arguments for ``gdal.Warp``. If ``format`` is not given, it is chosen by the path extension. 

487 

488 Raises: 

489 ``Error``: If no driver is found for the path or the warp fails. 

490 """ 

491 

492 gdal.UseExceptions() 

493 

494 if 'format' not in options: 

495 options = dict(options) 

496 options['format'] = _driver_from_args(path, '', True).GetDescription() 

497 

498 gd = gdal.Warp(path, self.gdDataset, **options) 

499 if gd is None: 

500 raise Error(f'warp failed') 

501 gd.FlushCache() 

502 gd = None 

503 

504 def save_as(self, path: str, driver: str = '', strict=False, options: dict = None): 

505 """Save a copy of the data set, including its metadata. 

506 

507 Args: 

508 path: Destination path. 

509 driver: Driver name. If omitted, the driver is chosen by the path extension. 

510 strict: Fail if the copy cannot be made exactly, for example, if the format does not support some data. 

511 options: Driver-specific creation options. 

512 

513 Raises: 

514 ``Error``: If no suitable raster driver is found. 

515 """ 

516 

517 gdal.UseExceptions() 

518 

519 drv = _driver_from_args(path, driver, need_raster=True) 

520 gd = drv.CreateCopy( 

521 path, 

522 self.gdDataset, 

523 strict=1 if strict else 0, 

524 options=_option_list(options), 

525 ) 

526 gd.SetMetadata(self.gdDataset.GetMetadata()) 

527 gd.FlushCache() 

528 gd = None 

529 

530 def size(self) -> gws.Size: 

531 """Get the raster size. 

532 

533 Returns: 

534 A ``(width, height)`` tuple in pixels. 

535 """ 

536 return (self.gdDataset.RasterXSize, self.gdDataset.RasterYSize) 

537 

538 def bounds(self) -> gws.Bounds: 

539 """Get the bounds of the raster, computed from its geo-transform. 

540 

541 Returns: 

542 The bounds, in the data set CRS or the default CRS. 

543 """ 

544 return _geotransform_to_bounds( 

545 self.gdDataset.GetGeoTransform(), 

546 (self.gdDataset.RasterXSize, self.gdDataset.RasterYSize), 

547 self.crs() or self.dso.defaultCrs, 

548 ) 

549 

550 

551class VectorDataSet(_DataSet): 

552 """Vector data set.""" 

553 

554 @contextlib.contextmanager 

555 def transaction(self): 

556 """Context manager that runs a transaction. 

557 

558 The transaction is committed on success and rolled back on an exception. 

559 

560 Yields: 

561 This data set. 

562 """ 

563 self.gdDataset.StartTransaction() 

564 try: 

565 yield self 

566 self.gdDataset.CommitTransaction() 

567 except: 

568 self.gdDataset.RollbackTransaction() 

569 raise 

570 

571 def create_layer( 

572 self, 

573 name: str, 

574 columns: dict[str, gws.AttributeType], 

575 geometry_type: gws.GeometryType = None, 

576 crs: gws.Crs = None, 

577 overwrite=False, 

578 options: dict = None, 

579 ) -> 'VectorLayer': 

580 """Create a new layer. 

581 

582 For Shapefiles, the data set encoding is passed to the driver. 

583 

584 Args: 

585 name: Layer name. 

586 columns: Map of column names to attribute types. 

587 geometry_type: Geometry type. If omitted, the layer has no geometry. 

588 crs: CRS for geometries, the default CRS of the data set by default. 

589 overwrite: Overwrite an existing layer. 

590 options: Driver-specific layer creation options. 

591 

592 Returns: 

593 The new layer. 

594 """ 

595 

596 opts = dict(options or {}) 

597 if overwrite: 

598 opts['OVERWRITE'] = 'YES' 

599 enc = (self.dso.encoding or '').upper() 

600 if enc: 

601 driver = self.gdDriver.GetName() 

602 if 'Shapefile' in driver: 

603 opts['ENCODING'] = enc 

604 

605 geom_type = ogr.wkbUnknown 

606 srs = None 

607 

608 if geometry_type: 

609 geom_type = _GEOM_TO_OGR.get(geometry_type) 

610 if not geom_type: 

611 gws.log.warning(f'gdal: unsupported {geometry_type=}') 

612 geom_type = ogr.wkbUnknown 

613 crs = crs or self.dso.defaultCrs 

614 srs = _srs_from_srid(crs.srid) 

615 

616 gd_layer = self.gdDataset.CreateLayer( 

617 name, 

618 geom_type=geom_type, 

619 srs=srs, 

620 options=_option_list(opts), 

621 ) 

622 for col_name, col_type in columns.items(): 

623 fd = ogr.FieldDefn(col_name, _ATTR_TO_OGR[col_type]) 

624 if col_type == gws.AttributeType.bool: 

625 fd.SetSubType(ogr.OFSTBoolean) 

626 gd_layer.CreateField(fd) 

627 

628 return VectorLayer(self, gd_layer) 

629 

630 def layers(self) -> list['VectorLayer']: 

631 """Get all layers. 

632 

633 Returns: 

634 A list of layers. 

635 """ 

636 

637 cnt = self.gdDataset.GetLayerCount() 

638 return [VectorLayer(self, self.gdDataset.GetLayerByIndex(n)) for n in range(cnt)] 

639 

640 def layer(self, name_or_index: str | int) -> Optional['VectorLayer']: 

641 """Get a layer by name or index. 

642 

643 Args: 

644 name_or_index: Layer name or index. 

645 

646 Returns: 

647 The layer, or ``None`` if not found. 

648 """ 

649 

650 gd_layer = None 

651 if isinstance(name_or_index, int): 

652 gd_layer = self.gdDataset.GetLayerByIndex(name_or_index) 

653 elif isinstance(name_or_index, str): 

654 gd_layer = self.gdDataset.GetLayerByName(name_or_index) 

655 return VectorLayer(self, gd_layer) if gd_layer else None 

656 

657 def require_layer(self, name_or_index: str | int) -> 'VectorLayer': 

658 """Get a layer by name or index, and fail if it is not found. 

659 

660 Args: 

661 name_or_index: Layer name or index. 

662 

663 Returns: 

664 The layer. 

665 

666 Raises: 

667 ``Error``: If the layer is not found. 

668 """ 

669 

670 la = self.layer(name_or_index) 

671 if la: 

672 return la 

673 raise Error(f'layer {name_or_index} not found') 

674 

675 

676class VectorLayer: 

677 """Layer of a vector data set.""" 

678 

679 name: str 

680 """Layer name.""" 

681 dso: _DataSetOptions 

682 """Options of the data set.""" 

683 gdLayer: ogr.Layer 

684 """Underlying OGR layer.""" 

685 gdDefn: ogr.FeatureDefn 

686 """Underlying OGR feature definition.""" 

687 

688 def __init__(self, ds: VectorDataSet, gd_layer: ogr.Layer): 

689 """Wrap an OGR layer. 

690 

691 Args: 

692 ds: Data set the layer belongs to. 

693 gd_layer: OGR layer. 

694 """ 

695 self.gdLayer = gd_layer 

696 self.gdDefn = self.gdLayer.GetLayerDefn() 

697 self.name = self.gdDefn.GetName() 

698 self.dso = ds.dso 

699 

700 def describe(self) -> gws.DataSetDescription: 

701 """Describe the layer columns. 

702 

703 The description includes the FID column (as the primary key), attribute columns 

704 of supported types, and geometry columns. If there are several geometry columns, 

705 the last one is used as the layer geometry. 

706 

707 Returns: 

708 The layer description. 

709 """ 

710 desc = gws.DataSetDescription( 

711 columns=[], 

712 columnMap={}, 

713 fullName=self.name, 

714 geometryName='', 

715 geometrySrid=0, 

716 geometryType='', 

717 name=self.name, 

718 schema='', 

719 ) 

720 

721 cols = [] 

722 

723 fid_col = self.gdLayer.GetFIDColumn() 

724 if fid_col: 

725 cols.append( 

726 gws.ColumnDescription( 

727 name=fid_col, 

728 type=_OGR_TO_ATTR[ogr.OFTInteger], 

729 nativeType=ogr.OFTInteger, 

730 isPrimaryKey=True, 

731 columnIndex=0, 

732 ) 

733 ) 

734 

735 for i in range(self.gdDefn.GetFieldCount()): 

736 fdef = self.gdDefn.GetFieldDefn(i) 

737 typ = fdef.GetType() 

738 if typ not in _OGR_TO_ATTR: 

739 continue 

740 attr_type = _OGR_TO_ATTR[typ] 

741 if fdef.GetSubType() == ogr.OFSTBoolean: 

742 attr_type = gws.AttributeType.bool 

743 cols.append( 

744 gws.ColumnDescription( 

745 name=fdef.GetName(), 

746 type=attr_type, 

747 nativeType=typ, 

748 columnIndex=i, 

749 ) 

750 ) 

751 

752 for i in range(self.gdDefn.GetGeomFieldCount()): 

753 fdef = self.gdDefn.GetGeomFieldDefn(i) 

754 typ = fdef.GetType() 

755 cols.append( 

756 gws.ColumnDescription( 

757 name=fdef.GetName() or 'geom', 

758 type=gws.AttributeType.geometry, 

759 nativeType=typ, 

760 columnIndex=i, 

761 geometryType=_OGR_TO_GEOM.get(typ) or gws.GeometryType.geometry, 

762 geometrySrid=_srid_from_srs(fdef.GetSpatialRef()) or self.dso.defaultCrs.srid, 

763 ) 

764 ) 

765 

766 desc.columns = cols 

767 desc.columnMap = {c.name: c for c in cols} 

768 

769 for c in cols: 

770 # NB take the last geom 

771 if c.geometryType: 

772 desc.geometryName = c.name 

773 desc.geometryType = c.geometryType 

774 desc.geometrySrid = c.geometrySrid 

775 

776 return desc 

777 

778 def insert(self, records: list[gws.FeatureRecord]) -> list[int]: 

779 """Insert feature records into the layer. 

780 

781 Integer record uids are used as feature ids. Attributes that are ``None`` 

782 or have no matching column are skipped. 

783 

784 Args: 

785 records: Feature records. 

786 

787 Returns: 

788 Feature ids of the inserted features. 

789 

790 Raises: 

791 ``Error``: If an attribute value cannot be set. 

792 """ 

793 desc = self.describe() 

794 fids = [] 

795 

796 for rec in records: 

797 gd_feature = ogr.Feature(self.gdDefn) 

798 if desc.geometryType and rec.shape: 

799 gd_feature.SetGeometry( 

800 ogr.CreateGeometryFromWkt( 

801 rec.shape.to_wkt(), 

802 _srs_from_srid(rec.shape.crs.srid), 

803 ) 

804 ) 

805 

806 if rec.uid and isinstance(rec.uid, int): 

807 gd_feature.SetFID(rec.uid) 

808 

809 for col in desc.columns: 

810 if col.geometryType or col.isPrimaryKey: 

811 continue 

812 val = rec.attributes.get(col.name) 

813 if val is None: 

814 continue 

815 try: 

816 _attr_to_ogr(gd_feature, int(col.nativeType), col.columnIndex, val, self.dso.encoding) 

817 except Exception as exc: 

818 raise Error(f'field cannot be set: {col.name=} {val=}') from exc 

819 

820 self.gdLayer.CreateFeature(gd_feature) 

821 fids.append(gd_feature.GetFID()) 

822 

823 return fids 

824 

825 def count(self, force=False): 

826 """Count features in the layer. 

827 

828 Args: 

829 force: Count features even if this is expensive for the driver. 

830 

831 Returns: 

832 The number of features, or ``-1`` if the count is not available without ``force``. 

833 """ 

834 return self.gdLayer.GetFeatureCount(force=1 if force else 0) 

835 

836 def get_all(self) -> list[gws.FeatureRecord]: 

837 """Read all features. 

838 

839 Returns: 

840 A list of feature records. 

841 """ 

842 return list(self.iter_features()) 

843 

844 def iter_features(self) -> Iterable[gws.FeatureRecord]: 

845 """Iterate over all features. 

846 

847 Yields: 

848 Feature records. The record uid is the feature id as a string, 

849 ``meta['layerName']`` is the layer name. 

850 """ 

851 self.gdLayer.ResetReading() 

852 

853 while True: 

854 gd_feature = self.gdLayer.GetNextFeature() 

855 if not gd_feature: 

856 break 

857 yield self._feature_record(gd_feature) 

858 

859 def get(self, fid: int) -> Optional[gws.FeatureRecord]: 

860 """Read a feature by its id. 

861 

862 Args: 

863 fid: Feature id. 

864 

865 Returns: 

866 The feature record, or ``None`` if not found. 

867 """ 

868 gd_feature = self.gdLayer.GetFeature(fid) 

869 if gd_feature: 

870 return self._feature_record(gd_feature) 

871 

872 def _feature_record(self, gd_feature): 

873 rec = gws.FeatureRecord( 

874 attributes={}, 

875 shape=None, 

876 meta={'layerName': self.name}, 

877 uid=str(gd_feature.GetFID()), 

878 ) 

879 

880 for i in range(gd_feature.GetFieldCount()): 

881 fdef = gd_feature.GetFieldDefnRef(i) 

882 val = _attr_from_ogr(gd_feature, fdef.GetType(), fdef.GetSubType(), i, self.dso.encoding) 

883 rec.attributes[fdef.GetName()] = val 

884 

885 cnt = gd_feature.GetGeomFieldCount() 

886 if cnt > 0: 

887 # NB take the last geom 

888 # @TODO multigeometry support 

889 fdef = gd_feature.GetGeomFieldRef(cnt - 1) 

890 if fdef: 

891 srid = _srid_from_srs(fdef.GetSpatialReference()) or self.dso.defaultCrs.srid 

892 if self.dso.geometryAsText: 

893 rec.ewkt = f'SRID={srid};{fdef.ExportToWkt()}' 

894 else: 

895 rec.shape = gws.lib.shape.from_wkb(bytes(fdef.ExportToIsoWkb()), gws.lib.crs.get(srid)) 

896 

897 return rec 

898 

899 

900## 

901 

902 

903def _driver_from_args(path, driver_name, need_raster): 

904 di = gws.u.get_app_global('gdal_driver_infos', _fetch_driver_infos) 

905 

906 if not driver_name: 

907 ext = path.split('.')[-1] 

908 names = di.extToName.get(ext) 

909 if not names: 

910 raise Error(f'no default driver found for {path!r}') 

911 if len(names) == 1: 

912 driver_name = names[0] 

913 elif ext in _DEFAULT_DRIVERS: 

914 driver_name = _DEFAULT_DRIVERS[ext] 

915 else: 

916 raise Error(f'multiple drivers found for {path!r}: {names}') 

917 

918 is_vector = driver_name in di.vectorNames 

919 is_raster = driver_name in di.rasterNames 

920 

921 if need_raster: 

922 if not is_raster: 

923 raise Error(f'driver {driver_name!r} is not raster') 

924 return gdal.GetDriverByName(driver_name) 

925 

926 if not is_vector: 

927 raise Error(f'driver {driver_name!r} is not vector') 

928 return ogr.GetDriverByName(driver_name) 

929 

930 

931_DEFAULT_DRIVERS = { 

932 'gif': 'GIF', 

933 'gml': 'GML', 

934 'kml': 'KML', 

935 'tif': 'GTiff', 

936 'tiff': 'GTiff', 

937} 

938 

939 

940def _fetch_driver_infos() -> _DriverInfoCache: 

941 dic = _DriverInfoCache( 

942 infos=[], 

943 extToName={}, 

944 vectorNames=set(), 

945 rasterNames=set(), 

946 ) 

947 

948 for n in range(gdal.GetDriverCount()): 

949 drv = gdal.GetDriver(n) 

950 di = DriverInfo( 

951 index=n, 

952 name=str(drv.ShortName), 

953 longName=str(drv.LongName), 

954 extensions=[], 

955 metaData=dict(drv.GetMetadata() or {}), 

956 ) 

957 dic.infos.append(di) 

958 

959 for e in di.metaData.get(gdal.DMD_EXTENSIONS, '').split(): 

960 dic.extToName.setdefault(e, []).append(di.name) 

961 di.extensions.append(e) 

962 if di.metaData.get('DCAP_VECTOR') == 'YES': 

963 dic.vectorNames.add(di.name) 

964 if di.metaData.get('DCAP_RASTER') == 'YES': 

965 dic.rasterNames.add(di.name) 

966 

967 return dic 

968 

969 

970_name_to_srid = {} 

971 

972 

973def _srs_from_srid(srid): 

974 # our geotransforms are always x=easting/longitude, so the SRS must use the 

975 # traditional axis order, not the authority order (northing/latitude first 

976 # for e.g. 4326, 3035, 31466-31469, 3044-3045). 

977 

978 srs = osr.SpatialReference() 

979 srs.ImportFromEPSG(srid) 

980 srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER) 

981 return srs 

982 

983 

984def _srid_from_srs(srs): 

985 if not srs: 

986 return 0 

987 

988 name = srs.GetName() 

989 if not name: 

990 wkt = srs.ExportToWkt() 

991 gws.log.warning(f'gdalx: no name for SRS {wkt!r}') 

992 return 0 

993 

994 if name in _name_to_srid: 

995 return _name_to_srid[name] 

996 

997 srid = srs.GetAuthorityCode(None) 

998 if not srid: 

999 wkt = srs.ExportToWkt() 

1000 gws.log.warning(f'gdalx: no srid for SRS {wkt!r}') 

1001 srid = 0 

1002 

1003 _name_to_srid[name] = srid 

1004 return srid 

1005 

1006 

1007def _attr_from_ogr(gd_feature: ogr.Feature, gtype: int, gsubtype: int, idx: int, encoding: str): 

1008 if gd_feature.IsFieldNull(idx): 

1009 return None 

1010 

1011 if gtype == ogr.OFTString: 

1012 b = gd_feature.GetFieldAsBinary(idx) 

1013 if encoding: 

1014 return b.decode(encoding) 

1015 return bytes(b) 

1016 

1017 # GetFieldAsDateTime uses float seconds: 

1018 # GetFieldAsDateTime(int i, int *pnYear, int *pnMonth, int *pnDay, int *pnHour, int *pnMinute, float *pfSecond, int *pnTZFlag) 

1019 

1020 if gtype == ogr.OFTDate: 

1021 v = gd_feature.GetFieldAsDateTime(idx) 

1022 return datetime.date(v[0], v[1], v[2]) 

1023 

1024 if gtype == ogr.OFTTime: 

1025 v = gd_feature.GetFieldAsDateTime(idx) 

1026 sec, fsec = divmod(v[5], 1) 

1027 return datetime.time(v[3], v[4], int(sec), int(fsec * 1e6)) 

1028 

1029 if gtype == ogr.OFTDateTime: 

1030 v = gd_feature.GetFieldAsDateTime(idx) 

1031 sec, fsec = divmod(v[5], 1) 

1032 return datetimex.new(v[0], v[1], v[2], v[3], v[4], int(sec), int(fsec * 1e6), tz=_tzflag_to_tz(v[6])) 

1033 

1034 if gtype in {ogr.OFTIntegerList, ogr.OFTInteger64List}: 

1035 return gd_feature.GetFieldAsIntegerList(idx) 

1036 if gtype == ogr.OFTRealList: 

1037 return gd_feature.GetFieldAsDoubleList(idx) 

1038 if gtype == ogr.OFTStringList: 

1039 return list(gd_feature.GetFieldAsStringList(idx)) 

1040 if gtype in {ogr.OFTInteger, ogr.OFTInteger64}: 

1041 if gsubtype == ogr.OFSTBoolean: 

1042 return gd_feature.GetFieldAsInteger(idx) != 0 

1043 return gd_feature.GetFieldAsInteger(idx) 

1044 if gtype == ogr.OFTReal: 

1045 return gd_feature.GetFieldAsDouble(idx) 

1046 if gtype == ogr.OFTBinary: 

1047 return gd_feature.GetFieldAsBinary(idx) 

1048 

1049 

1050def _tzflag_to_tz(tzflag): 

1051 # see gdal/ogr/ogrutils.cpp OGRGetISO8601DateTime 

1052 

1053 if tzflag == 0 or tzflag == 1: 

1054 return '' 

1055 if tzflag == 100: 

1056 return 'UTC' 

1057 if tzflag % 4 != 0: 

1058 # @TODO 

1059 raise Error(f'unsupported timezone {tzflag=}') 

1060 hrs = (100 - tzflag) // 4 

1061 return f'Etc/GMT{hrs:+}' 

1062 

1063 

1064def _attr_to_ogr(gd_feature: ogr.Feature, gtype: int, idx: int, value: Any, encoding): 

1065 if isinstance(value, decimal.Decimal): 

1066 value = float(value) 

1067 

1068 if gtype == ogr.OFTDate: 

1069 return gd_feature.SetField(idx, datetimex.to_iso_date_string(value)) 

1070 if gtype == ogr.OFTTime: 

1071 return gd_feature.SetField(idx, datetimex.to_iso_time_string(value)) 

1072 if gtype == ogr.OFTDateTime: 

1073 return gd_feature.SetField(idx, datetimex.to_iso_string(datetimex.to_utc(value), with_tz='Z')) 

1074 if gtype in {ogr.OFTInteger, ogr.OFTInteger64}: 

1075 return gd_feature.SetField(idx, int(bool(value) if isinstance(value, bool) else value)) 

1076 if gtype in {ogr.OFTIntegerList, ogr.OFTInteger64List}: 

1077 return gd_feature.SetFieldIntegerList(idx, [int(x) for x in value]) 

1078 if gtype == ogr.OFTRealList: 

1079 return gd_feature.SetFieldDoubleList(idx, [float(x) for x in value]) 

1080 if gtype == ogr.OFTReal: 

1081 return gd_feature.SetField(idx, float(value)) 

1082 if gtype == ogr.OFTString: 

1083 if isinstance(value, bytes): 

1084 return gd_feature.SetField(idx, value.decode(encoding or 'utf8')) 

1085 return gd_feature.SetField(idx, str(value)) 

1086 if gtype == ogr.OFTStringList: 

1087 return gd_feature.SetFieldStringList(idx, [str(x) for x in value]) 

1088 if gtype == ogr.OFTBinary: 

1089 return gd_feature.SetFieldBinaryFromHexString(idx, value.hex() if isinstance(value, bytes) else value) 

1090 

1091 return gd_feature.SetField(idx, value) 

1092 

1093 

1094def _bounds_to_geotransform(bounds: gws.Bounds, px_size: gws.Size, rotation: gws.Size | None) -> tuple[float, float, float, float, float, float]: 

1095 ext = bounds.extent 

1096 res_x = (ext[2] - ext[0]) / px_size[0] 

1097 res_y = (ext[1] - ext[3]) / px_size[1] 

1098 xr = rotation[0] if rotation else 0.0 

1099 yr = rotation[1] if rotation else 0.0 

1100 return (ext[0], res_x, xr, ext[3], yr, res_y) 

1101 

1102 

1103def _geotransform_to_bounds(gt: tuple[float, float, float, float, float, float], px_size: gws.Size, crs: gws.Crs) -> gws.Bounds: 

1104 x0 = gt[0] 

1105 x1 = x0 + gt[1] * px_size[0] 

1106 y1 = gt[3] 

1107 y0 = y1 + gt[5] * px_size[1] 

1108 return gws.lib.bounds.from_extent((x0, y0, x1, y1), crs, always_xy=True) 

1109 

1110 

1111def _option_list(opts: dict | None) -> list[str]: 

1112 if not opts: 

1113 return [] 

1114 return [f'{k}={v}' for k, v in opts.items()] 

1115 

1116 

1117_ATTR_TO_OGR = { 

1118 gws.AttributeType.bool: ogr.OFTInteger, 

1119 gws.AttributeType.bytes: ogr.OFTBinary, 

1120 gws.AttributeType.date: ogr.OFTDate, 

1121 gws.AttributeType.datetime: ogr.OFTDateTime, 

1122 gws.AttributeType.float: ogr.OFTReal, 

1123 gws.AttributeType.floatlist: ogr.OFTRealList, 

1124 gws.AttributeType.int: ogr.OFTInteger, 

1125 gws.AttributeType.intlist: ogr.OFTIntegerList, 

1126 gws.AttributeType.str: ogr.OFTString, 

1127 gws.AttributeType.strlist: ogr.OFTStringList, 

1128 gws.AttributeType.time: ogr.OFTTime, 

1129} 

1130 

1131_OGR_TO_ATTR = { 

1132 ogr.OFTBinary: gws.AttributeType.bytes, 

1133 ogr.OFTDate: gws.AttributeType.date, 

1134 ogr.OFTDateTime: gws.AttributeType.datetime, 

1135 ogr.OFTReal: gws.AttributeType.float, 

1136 ogr.OFTRealList: gws.AttributeType.floatlist, 

1137 ogr.OFTInteger: gws.AttributeType.int, 

1138 ogr.OFTIntegerList: gws.AttributeType.intlist, 

1139 ogr.OFTInteger64: gws.AttributeType.int, 

1140 ogr.OFTInteger64List: gws.AttributeType.intlist, 

1141 ogr.OFTString: gws.AttributeType.str, 

1142 ogr.OFTStringList: gws.AttributeType.strlist, 

1143 ogr.OFTTime: gws.AttributeType.time, 

1144} 

1145 

1146_GEOM_TO_OGR = { 

1147 gws.GeometryType.curve: ogr.wkbCurve, 

1148 gws.GeometryType.geometrycollection: ogr.wkbGeometryCollection, 

1149 gws.GeometryType.linestring: ogr.wkbLineString, 

1150 gws.GeometryType.multicurve: ogr.wkbMultiCurve, 

1151 gws.GeometryType.multilinestring: ogr.wkbMultiLineString, 

1152 gws.GeometryType.multipoint: ogr.wkbMultiPoint, 

1153 gws.GeometryType.multipolygon: ogr.wkbMultiPolygon, 

1154 gws.GeometryType.multisurface: ogr.wkbMultiSurface, 

1155 gws.GeometryType.point: ogr.wkbPoint, 

1156 gws.GeometryType.polygon: ogr.wkbPolygon, 

1157 gws.GeometryType.polyhedralsurface: ogr.wkbPolyhedralSurface, 

1158 gws.GeometryType.surface: ogr.wkbSurface, 

1159} 

1160 

1161_OGR_TO_GEOM = { 

1162 ogr.wkbCurve: gws.GeometryType.curve, 

1163 ogr.wkbGeometryCollection: gws.GeometryType.geometrycollection, 

1164 ogr.wkbLineString: gws.GeometryType.linestring, 

1165 ogr.wkbMultiCurve: gws.GeometryType.multicurve, 

1166 ogr.wkbMultiLineString: gws.GeometryType.multilinestring, 

1167 ogr.wkbMultiPoint: gws.GeometryType.multipoint, 

1168 ogr.wkbMultiPolygon: gws.GeometryType.multipolygon, 

1169 ogr.wkbMultiSurface: gws.GeometryType.multisurface, 

1170 ogr.wkbPoint: gws.GeometryType.point, 

1171 ogr.wkbPolygon: gws.GeometryType.polygon, 

1172 ogr.wkbPolyhedralSurface: gws.GeometryType.polyhedralsurface, 

1173 ogr.wkbSurface: gws.GeometryType.surface, 

1174}