• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

johntruckenbrodt / spatialist / 14332609845

08 Apr 2025 11:41AM UTC coverage: 80.084% (+0.2%) from 79.933%
14332609845

Pull #55

github

johntruckenbrodt
[docs] add geopandas intersphinx mapping
Pull Request #55: [Vector.to_geopandas] new method

16 of 19 new or added lines in 2 files covered. (84.21%)

63 existing lines in 2 files now uncovered.

1914 of 2390 relevant lines covered (80.08%)

0.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.65
/spatialist/vector.py
1
# -*- coding: utf-8 -*-
2
################################################################
3
# OGR wrapper for convenient vector data handling and processing
4
# John Truckenbrodt 2015-2025
5
################################################################
6

7

8
import os
1✔
9
import yaml
1✔
10
from osgeo import ogr, osr, gdal
1✔
11
from osgeo.gdalconst import GDT_Byte
1✔
12
from .auxil import crsConvert
1✔
13
from .ancillary import parse_literal
1✔
14
from .sqlite_util import sqlite_setup
1✔
15

16
import geopandas as gpd
1✔
17
from shapely.wkb import loads as wkb_loads
1✔
18

19
ogr.UseExceptions()
1✔
20
osr.UseExceptions()
1✔
21

22

23
class Vector(object):
1✔
24
    """
25
    This is intended as a vector meta information handler with options for reading and writing vector data in a
26
    convenient manner by simplifying the numerous options provided by the OGR python binding.
27

28
    Parameters
29
    ----------
30
    filename: str or None
31
        the vector file to read; if filename is `None`, a new in-memory Vector object is created.
32
        In this case `driver` is overridden and set to 'Memory'. The following file extensions are auto-detected:
33
        
34
        .. list_drivers:: vector
35
        
36
    driver: str
37
        the vector file format; needs to be defined if the format cannot be auto-detected from the filename extension
38
    """
39
    
40
    def __init__(self, filename=None, driver=None):
1✔
41
        
42
        if filename is None:
1✔
43
            driver = 'Memory'
1✔
44
        elif isinstance(filename, str):
1✔
45
            if not os.path.isfile(filename):
1✔
46
                raise OSError('file does not exist')
1✔
47
            if driver is None:
1✔
48
                driver = self.__driver_autodetect(filename)
1✔
49
        else:
UNCOV
50
            raise TypeError('filename must either be str or None')
×
51
        
52
        self.filename = filename
1✔
53
        
54
        self.driver = ogr.GetDriverByName(driver)
1✔
55
        
56
        self.vector = self.driver.CreateDataSource('out') if driver == 'Memory' else self.driver.Open(filename)
1✔
57
        
58
        nlayers = self.vector.GetLayerCount()
1✔
59
        if nlayers > 1:
1✔
UNCOV
60
            raise RuntimeError('multiple layers are currently not supported')
×
61
        elif nlayers == 1:
1✔
62
            self.init_layer()
1✔
63
    
64
    def __getitem__(self, expression):
1✔
65
        """
66
        subset the vector object by index or attribute.
67

68
        Parameters
69
        ----------
70
        expression: int or str
71
            the key or expression to be used for subsetting.
72
            See :meth:`osgeo.ogr.Layer.SetAttributeFilter` for details on the expression syntax.
73

74
        Returns
75
        -------
76
        Vector
77
            a vector object matching the specified criteria
78
        
79
        Examples
80
        --------
81
        Assuming we have a shapefile called `testsites.shp`, which has an attribute `sitename`,
82
        we can subset individual sites and write them to new files like so:
83
        
84
        >>> from spatialist import Vector
85
        >>> filename = 'testsites.shp'
86
        >>> with Vector(filename)["sitename='site1'"] as site1:
87
        >>>     site1.write('site1.shp')
88
        """
89
        if not isinstance(expression, (int, str)):
1✔
90
            raise RuntimeError('expression must be of type int or str')
1✔
91
        expression = parse_literal(expression) if isinstance(expression, str) else expression
1✔
92
        if isinstance(expression, int):
1✔
UNCOV
93
            feat = self.getFeatureByIndex(expression)
×
94
        else:
95
            self.layer.SetAttributeFilter(expression)
1✔
96
            feat = self.getfeatures()
1✔
97
            feat = feat if len(feat) > 0 else None
1✔
98
            self.layer.SetAttributeFilter('')
1✔
99
        if feat is None:
1✔
UNCOV
100
            return None
×
101
        else:
102
            return feature2vector(feat, ref=self)
1✔
103
    
104
    def __enter__(self):
1✔
105
        return self
1✔
106
    
107
    def __exit__(self, exc_type, exc_val, exc_tb):
1✔
108
        self.close()
1✔
109
    
110
    def __str__(self):
1✔
UNCOV
111
        vals = dict()
×
112
        vals['proj4'] = self.proj4
×
113
        vals.update(self.extent)
×
114
        vals['filename'] = self.filename if self.filename is not None else 'memory'
×
115
        vals['geomtype'] = ', '.join(list(set(self.geomTypes)))
×
116
        
UNCOV
117
        info = 'class         : spatialist Vector object\n' \
×
118
               'geometry type : {geomtype}\n' \
119
               'extent        : {xmin:.3f}, {xmax:.3f}, {ymin:.3f}, {ymax:.3f} (xmin, xmax, ymin, ymax)\n' \
120
               'coord. ref.   : {proj4}\n' \
121
               'data source   : {filename}'.format(**vals)
UNCOV
122
        return info
×
123
    
124
    @staticmethod
1✔
125
    def __driver_autodetect(filename):
1✔
126
        path = os.path.dirname(os.path.realpath(__file__))
1✔
127
        drivers = yaml.safe_load(open(os.path.join(path, 'drivers_vector.yml')))
1✔
128
        extension = os.path.splitext(filename)[1][1:]
1✔
129
        if extension not in drivers.keys():
1✔
UNCOV
130
            message = "the file extension '{}' is not supported. " \
×
131
                      "Please provide the OGR format descriptor via " \
132
                      "parameter 'driver' or use one of the supported extensions:\n- .{}"
UNCOV
133
            message = message.format(extension, '\n- .'.join(drivers.keys()))
×
134
            raise RuntimeError(message)
×
135
        else:
136
            return drivers[extension]
1✔
137
    
138
    def addfeature(self, geometry, fields=None):
1✔
139
        """
140
        add a feature to the vector object from a geometry
141

142
        Parameters
143
        ----------
144
        geometry: osgeo.ogr.Geometry
145
            the geometry to add as a feature
146
        fields: dict or None
147
            the field names and values to assign to the new feature
148

149
        Returns
150
        -------
151

152
        """
153
        
154
        feature = ogr.Feature(self.layerdef)
1✔
155
        feature.SetGeometry(geometry)
1✔
156
        
157
        if fields is not None:
1✔
158
            for fieldname, value in fields.items():
1✔
159
                if fieldname not in self.fieldnames:
1✔
UNCOV
160
                    raise IOError('field "{}" is missing'.format(fieldname))
×
161
                try:
1✔
162
                    feature.SetField(fieldname, value)
1✔
UNCOV
163
                except NotImplementedError as e:
×
164
                    fieldindex = self.fieldnames.index(fieldname)
×
165
                    fieldtype = feature.GetFieldDefnRef(fieldindex).GetTypeName()
×
166
                    message = str(e) + '\ntrying to set field {} (type {}) to value {} (type {})'
×
167
                    message = message.format(fieldname, fieldtype, value, type(value))
×
168
                    raise (NotImplementedError(message))
×
169
        
170
        self.layer.CreateFeature(feature)
1✔
171
        feature = None
1✔
172
        self.init_features()
1✔
173
    
174
    def addfield(self, name, type, width=10, values=None):
1✔
175
        """
176
        add a field to the vector layer
177

178
        Parameters
179
        ----------
180
        name: str
181
            the field name
182
        type: int
183
            the OGR Field Type (OFT), e.g. ogr.OFTString.
184
            See :class:`osgeo.ogr.FieldDefn`.
185
        width: int
186
            the width of the new field (only for ogr.OFTString fields)
187
        values: list
188
            an optional list with values for each feature to assign to the new field.
189
            The length must be identical to the number of features.
190

191
        Returns
192
        -------
193

194
        """
195
        type_name = ogr.GetFieldTypeName(type)
1✔
196
        field_defn = ogr.FieldDefn(name, type)
1✔
197
        if type == ogr.OFTString:
1✔
198
            field_defn.SetWidth(width)
1✔
199
        self.layer.CreateField(field_defn)
1✔
200
        if type_name in ['String', 'Integer', 'Real', 'Binary']:
1✔
201
            method_name = 'SetField'
1✔
202
        elif type_name in ['StringList', 'DoubleList', 'IntegerList',
1✔
203
                           'Integer64', 'Integer64List']:
204
            method_name = f'SetField{type_name}'
1✔
205
        elif type_name == 'RealList':
1✔
206
            method_name = 'SetFieldDoubleList'
1✔
207
        else:
UNCOV
208
            raise ValueError(f'Unsupported field type: {type_name}')
×
209
        if values is not None:
1✔
210
            if len(values) != self.nfeatures:
1✔
UNCOV
211
                raise RuntimeError('number of values does not match number of features')
×
212
            for i, feature in enumerate(self.layer):
1✔
213
                index = feature.GetFieldIndex(name)
1✔
214
                method = getattr(feature, method_name)
1✔
215
                method(index, values[i])
1✔
216
                self.layer.SetFeature(feature)
1✔
217
    
218
    def addlayer(self, name, srs, geomType):
1✔
219
        """
220
        add a layer to the vector layer
221

222
        Parameters
223
        ----------
224
        name: str
225
            the layer name
226
        srs: int or str or osgeo.osr.SpatialReference
227
            the spatial reference system. See :func:`spatialist.auxil.crsConvert` for options.
228
        geomType: int
229
            an OGR well-known binary data type.
230
            See `Module ogr <https://gdal.org/python/osgeo.ogr-module.html>`_.
231

232
        Returns
233
        -------
234

235
        """
236
        self.vector.CreateLayer(name, crsConvert(srs, 'osr'), geomType)
1✔
237
        self.init_layer()
1✔
238
    
239
    def addvector(self, vec):
1✔
240
        """
241
        add a vector object to the layer of the current Vector object
242

243
        Parameters
244
        ----------
245
        vec: Vector
246
            the vector object to add
247
        merge: bool
248
            merge overlapping polygons?
249

250
        Returns
251
        -------
252

253
        """
254
        vec.layer.ResetReading()
1✔
255
        for feature in vec.layer:
1✔
256
            self.layer.CreateFeature(feature)
1✔
257
        self.init_features()
1✔
258
        vec.layer.ResetReading()
1✔
259
    
260
    def bbox(self, outname=None, driver=None, overwrite=True):
1✔
261
        """
262
        create a bounding box from the extent of the Vector object
263

264
        Parameters
265
        ----------
266
        outname: str or None
267
            the name of the vector file to be written; if None, a Vector object is returned
268
        driver: str
269
            the name of the file format to write
270
        overwrite: bool
271
            overwrite an already existing file?
272

273
        Returns
274
        -------
275
        Vector or None
276
            if outname is None, the bounding box Vector object
277
        """
278
        if outname is None:
1✔
279
            return bbox(self.extent, self.srs)
1✔
280
        else:
UNCOV
281
            bbox(self.extent, self.srs, outname=outname, driver=driver, overwrite=overwrite)
×
282
    
283
    def clone(self):
1✔
284
        return feature2vector(self.getfeatures(), ref=self)
1✔
285
    
286
    def close(self):
1✔
287
        """
288
        closes the OGR vector file connection
289

290
        Returns
291
        -------
292

293
        """
294
        self.vector = None
1✔
295
        for feature in self.__features:
1✔
296
            if feature is not None:
1✔
UNCOV
297
                feature = None
×
298
    
299
    def convert2wkt(self, set3D=True):
1✔
300
        """
301
        export the geometry of each feature as a wkt string
302

303
        Parameters
304
        ----------
305
        set3D: bool
306
            keep the third (height) dimension?
307

308
        Returns
309
        -------
310

311
        """
UNCOV
312
        features = self.getfeatures()
×
313
        for feature in features:
×
314
            try:
×
315
                feature.geometry().Set3D(set3D)
×
316
            except AttributeError:
×
317
                dim = 3 if set3D else 2
×
318
                feature.geometry().SetCoordinateDimension(dim)
×
319
        
UNCOV
320
        return [feature.geometry().ExportToWkt() for feature in features]
×
321
    
322
    @property
1✔
323
    def extent(self):
1✔
324
        """
325
        the extent of the vector object
326

327
        Returns
328
        -------
329
        dict
330
            a dictionary with keys `xmin`, `xmax`, `ymin`, `ymax`
331
        """
332
        return dict(zip(['xmin', 'xmax', 'ymin', 'ymax'], self.layer.GetExtent()))
1✔
333
    
334
    @property
1✔
335
    def fieldDefs(self):
1✔
336
        """
337

338
        Returns
339
        -------
340
        list[osgeo.ogr.FieldDefn]
341
            the field definition for each field of the Vector object
342
        """
343
        return [self.layerdef.GetFieldDefn(x) for x in range(0, self.nfields)]
1✔
344
    
345
    @property
1✔
346
    def fieldnames(self):
1✔
347
        """
348

349
        Returns
350
        -------
351
        list of str
352
            the names of the fields
353
        """
354
        return sorted([field.GetName() for field in self.fieldDefs])
1✔
355
    
356
    @property
1✔
357
    def geomType(self):
1✔
358
        """
359

360
        Returns
361
        -------
362
        int
363
            the layer geometry type
364
        """
365
        return self.layerdef.GetGeomType()
1✔
366
    
367
    @property
1✔
368
    def geomTypes(self):
1✔
369
        """
370

371
        Returns
372
        -------
373
        list
374
            the geometry type of each feature
375
        """
376
        return [feat.GetGeometryRef().GetGeometryName() for feat in self.getfeatures()]
1✔
377
    
378
    def getArea(self):
1✔
379
        """
380

381
        Returns
382
        -------
383
        float
384
            the area of the vector geometries
385
        """
386
        return sum([x.GetGeometryRef().GetArea() for x in self.getfeatures()])
1✔
387
    
388
    def getFeatureByAttribute(self, fieldname, attribute):
1✔
389
        """
390
        get features by field attribute
391

392
        Parameters
393
        ----------
394
        fieldname: str
395
            the name of the queried field
396
        attribute: int or str
397
            the field value of interest
398

399
        Returns
400
        -------
401
        list[osgeo.ogr.Feature] or osgeo.ogr.Feature
402
            the feature(s) matching the search query
403
        """
404
        attr = attribute.strip() if isinstance(attribute, str) else attribute
1✔
405
        if fieldname not in self.fieldnames:
1✔
406
            raise KeyError('invalid field name')
1✔
407
        out = []
1✔
408
        self.layer.ResetReading()
1✔
409
        for feature in self.layer:
1✔
410
            field = feature.GetField(fieldname)
1✔
411
            field = field.strip() if isinstance(field, str) else field
1✔
412
            if field == attr:
1✔
413
                out.append(feature.Clone())
1✔
414
        self.layer.ResetReading()
1✔
415
        if len(out) == 0:
1✔
UNCOV
416
            return None
×
417
        elif len(out) == 1:
1✔
418
            return out[0]
1✔
419
        else:
UNCOV
420
            return out
×
421
    
422
    def getFeatureByIndex(self, index):
1✔
423
        """
424
        get features by numerical (positional) index
425

426
        Parameters
427
        ----------
428
        index: int
429
            the queried index
430

431
        Returns
432
        -------
433
        osgeo.ogr.Feature
434
            the requested feature
435
        """
436
        feature = self.layer[index]
1✔
437
        if feature is None:
1✔
UNCOV
438
            feature = self.getfeatures()[index]
×
439
        return feature
1✔
440
    
441
    def getfeatures(self):
1✔
442
        """
443

444
        Returns
445
        -------
446
        list[osgeo.ogr.Feature]
447
            a list of cloned features
448
        """
449
        self.layer.ResetReading()
1✔
450
        features = [x.Clone() for x in self.layer]
1✔
451
        self.layer.ResetReading()
1✔
452
        return features
1✔
453
    
454
    def getProjection(self, type):
1✔
455
        """
456
        get the CRS of the Vector object. See :func:`spatialist.auxil.crsConvert`.
457

458
        Parameters
459
        ----------
460
        type: str
461
            the type of projection required.
462

463
        Returns
464
        -------
465
        int or str or osgeo.osr.SpatialReference
466
            the output CRS
467
        """
468
        return crsConvert(self.layer.GetSpatialRef(), type)
1✔
469
    
470
    def getUniqueAttributes(self, fieldname):
1✔
471
        """
472

473
        Parameters
474
        ----------
475
        fieldname: str
476
            the name of the field of interest
477

478
        Returns
479
        -------
480
        list of str or int
481
            the unique attributes of the field
482
        """
483
        self.layer.ResetReading()
1✔
484
        attributes = list(set([x.GetField(fieldname) for x in self.layer]))
1✔
485
        self.layer.ResetReading()
1✔
486
        return sorted(attributes)
1✔
487
    
488
    def init_features(self):
1✔
489
        """
490
        delete all in-memory features
491

492
        Returns
493
        -------
494

495
        """
496
        del self.__features
1✔
497
        self.__features = [None] * self.nfeatures
1✔
498
    
499
    def init_layer(self):
1✔
500
        """
501
        initialize a layer object
502

503
        Returns
504
        -------
505

506
        """
507
        self.layer = self.vector.GetLayer()
1✔
508
        self.__features = [None] * self.nfeatures
1✔
509
    
510
    @property
1✔
511
    def layerdef(self):
1✔
512
        """
513

514
        Returns
515
        -------
516
        osgeo.ogr.FeatureDefn
517
            the layer's feature definition
518
        """
519
        return self.layer.GetLayerDefn()
1✔
520
    
521
    @property
1✔
522
    def layername(self):
1✔
523
        """
524

525
        Returns
526
        -------
527
        str
528
            the name of the layer
529
        """
530
        return self.layer.GetName()
1✔
531
    
532
    def load(self):
1✔
533
        """
534
        load all feature into memory
535

536
        Returns
537
        -------
538

539
        """
UNCOV
540
        self.layer.ResetReading()
×
541
        for i in range(self.nfeatures):
×
542
            if self.__features[i] is None:
×
543
                self.__features[i] = self.layer[i]
×
544
    
545
    @property
1✔
546
    def nfeatures(self):
1✔
547
        """
548

549
        Returns
550
        -------
551
        int
552
            the number of features
553
        """
554
        return len(self.layer)
1✔
555
    
556
    @property
1✔
557
    def nfields(self):
1✔
558
        """
559

560
        Returns
561
        -------
562
        int
563
            the number of fields
564
        """
565
        return self.layerdef.GetFieldCount()
1✔
566
    
567
    @property
1✔
568
    def nlayers(self):
1✔
569
        """
570

571
        Returns
572
        -------
573
        int
574
            the number of layers
575
        """
576
        return self.vector.GetLayerCount()
1✔
577
    
578
    @property
1✔
579
    def proj4(self):
1✔
580
        """
581

582
        Returns
583
        -------
584
        str
585
            the CRS in PRO4 format
586
        """
587
        return self.srs.ExportToProj4().strip()
1✔
588
    
589
    def reproject(self, projection):
1✔
590
        """
591
        in-memory reprojection
592

593
        Parameters
594
        ----------
595
        projection: int or str or osgeo.osr.SpatialReference
596
            the target CRS. See :func:`spatialist.auxil.crsConvert`.
597

598
        Returns
599
        -------
600

601
        """
602
        srs_out = crsConvert(projection, 'osr')
1✔
603
        
604
        # the following check was found to not work in GDAL 3.0.1; likely a bug
605
        # if self.srs.IsSame(srs_out) == 0:
606
        if self.getProjection('epsg') != crsConvert(projection, 'epsg'):
1✔
607
            
608
            # create the CoordinateTransformation
609
            coordTrans = osr.CoordinateTransformation(self.srs, srs_out)
1✔
610
            
611
            layername = self.layername
1✔
612
            geomType = self.geomType
1✔
613
            features = self.getfeatures()
1✔
614
            feat_def = features[0].GetDefnRef()
1✔
615
            fields = [feat_def.GetFieldDefn(x) for x in range(0, feat_def.GetFieldCount())]
1✔
616
            
617
            self.__init__()
1✔
618
            self.addlayer(layername, srs_out, geomType)
1✔
619
            self.layer.CreateFields(fields)
1✔
620
            
621
            for feature in features:
1✔
622
                geom = feature.GetGeometryRef()
1✔
623
                geom.Transform(coordTrans)
1✔
624
                newfeature = feature.Clone()
1✔
625
                newfeature.SetGeometry(geom)
1✔
626
                self.layer.CreateFeature(newfeature)
1✔
627
                newfeature = None
1✔
628
            self.init_features()
1✔
629
    
630
    def setCRS(self, crs):
1✔
631
        """
632
        directly reset the spatial reference system of the vector object.
633
        This is not going to reproject the Vector object, see :meth:`reproject` instead.
634

635
        Parameters
636
        ----------
637
        crs: int or str or osgeo.osr.SpatialReference
638
            the input CRS
639

640
        Returns
641
        -------
642

643
        Example
644
        -------
645
        >>> site = Vector('shape.shp')
646
        >>> site.setCRS('+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs ')
647

648
        """
649
        # try to convert the input crs to osr.SpatialReference
UNCOV
650
        srs_out = crsConvert(crs, 'osr')
×
651
        
652
        # save all relevant info from the existing vector object
UNCOV
653
        layername = self.layername
×
654
        geomType = self.geomType
×
655
        layer_definition = ogr.Feature(self.layer.GetLayerDefn())
×
656
        fields = [layer_definition.GetFieldDefnRef(x) for x in range(layer_definition.GetFieldCount())]
×
657
        features = self.getfeatures()
×
658
        
659
        # initialize a new vector object and create a layer
UNCOV
660
        self.__init__()
×
661
        self.addlayer(layername, srs_out, geomType)
×
662
        
663
        # add the fields to new layer
UNCOV
664
        self.layer.CreateFields(fields)
×
665
        
666
        # add the features to the newly created layer
NEW
667
        for feat in features:
×
NEW
668
            self.layer.CreateFeature(feat)
×
NEW
669
        self.init_features()
×
670
    
671
    @property
1✔
672
    def srs(self):
1✔
673
        """
674

675
        Returns
676
        -------
677
        osgeo.osr.SpatialReference
678
            the geometry's spatial reference system
679
        """
680
        return self.layer.GetSpatialRef()
1✔
681
    
682
    def to_geopandas(self):
1✔
683
        """
684
        Convert the object to a geopandas GeoDataFrame
685
        
686
        Returns
687
        -------
688
        geopandas.GeoDataFrame
689
        """
690
        features = []
1✔
691
        self.layer.ResetReading()
1✔
692
        for feature in self.layer:
1✔
693
            geom = feature.GetGeometryRef()
1✔
694
            geom_wkb = geom.ExportToWkb()
1✔
695
            properties = feature.items()
1✔
696
            properties["geometry"] = wkb_loads(bytes(geom_wkb))
1✔
697
            features.append(properties)
1✔
698
        self.layer.ResetReading()
1✔
699
        gdf = gpd.GeoDataFrame(features, crs=self.srs.ExportToWkt())
1✔
700
        return gdf
1✔
701
    
702
    def write(self, outfile, driver=None, overwrite=True):
1✔
703
        """
704
        write the Vector object to a file
705

706
        Parameters
707
        ----------
708
        outfile:
709
            the name of the file to write; the following extensions are automatically detected
710
            for determining the format driver:
711
            
712
            .. list_drivers:: vector
713
            
714
        driver: str or None
715
            the output file format; default None: try to autodetect from the file name extension
716
        overwrite: bool
717
            overwrite an already existing file?
718

719
        Returns
720
        -------
721

722
        """
723
        
724
        if driver is None:
1✔
725
            driver = self.__driver_autodetect(outfile)
1✔
726
        
727
        driver = ogr.GetDriverByName(driver)
1✔
728
        
729
        if os.path.exists(outfile):
1✔
UNCOV
730
            if overwrite:
×
UNCOV
731
                driver.DeleteDataSource(outfile)
×
732
            else:
UNCOV
733
                raise RuntimeError('target file already exists')
×
734
        
735
        ds_out = driver.CreateDataSource(outfile)
1✔
736
        ds_out.CopyLayer(self.layer, self.layer.GetName())
1✔
737
        ds_out = driver = None
1✔
738

739

740
def bbox(coordinates, crs, outname=None, driver=None, overwrite=True):
1✔
741
    """
742
    create a bounding box vector object or file.
743
    The CRS can be in either WKT, EPSG or PROJ4 format
744
    
745
    Parameters
746
    ----------
747
    coordinates: dict
748
        a dictionary containing numerical variables with keys `xmin`, `xmax`, `ymin` and `ymax`
749
    crs: int or str or osgeo.osr.SpatialReference
750
        the coordinate reference system of the `coordinates`. See :func:`~spatialist.auxil.crsConvert` for options.
751
    outname: str
752
        the file to write to. If `None`, the bounding box is returned as :class:`~spatialist.vector.Vector` object
753
    driver: str
754
        the output file format; needs to be defined if the format cannot
755
            be auto-detected from the filename extension
756
    overwrite: bool
757
        overwrite an existing file?
758
    
759
    Returns
760
    -------
761
    Vector or None
762
        the bounding box Vector object
763
    """
764
    srs = crsConvert(crs, 'osr')
1✔
765
    ring = ogr.Geometry(ogr.wkbLinearRing)
1✔
766
    
767
    ring.AddPoint(coordinates['xmin'], coordinates['ymin'])
1✔
768
    ring.AddPoint(coordinates['xmin'], coordinates['ymax'])
1✔
769
    ring.AddPoint(coordinates['xmax'], coordinates['ymax'])
1✔
770
    ring.AddPoint(coordinates['xmax'], coordinates['ymin'])
1✔
771
    ring.CloseRings()
1✔
772
    
773
    geom = ogr.Geometry(ogr.wkbPolygon)
1✔
774
    geom.AddGeometry(ring)
1✔
775
    
776
    geom.FlattenTo2D()
1✔
777
    
778
    bbox = Vector(driver='Memory')
1✔
779
    bbox.addlayer('bbox', srs, geom.GetGeometryType())
1✔
780
    bbox.addfield('area', ogr.OFTReal)
1✔
781
    bbox.addfeature(geom, fields={'area': geom.Area()})
1✔
782
    geom = None
1✔
783
    if outname is None:
1✔
784
        return bbox
1✔
785
    else:
786
        bbox.write(outfile=outname, driver=driver, overwrite=overwrite)
1✔
787

788

789
def boundary(vectorobject, expression=None, outname=None):
1✔
790
    """
791
    Get the boundary of the largest geometry as new vector object. The following steps are performed:
792
    
793
     - find the largest geometry matching the expression
794
     - compute the geometry's boundary using :meth:`osgeo.ogr.Geometry.Boundary`, returning a MULTILINE geometry
795
     - select the longest line of the MULTILINE geometry
796
     - create a closed linear ring from this longest line
797
     - create a polygon from this linear ring
798
     - create a new :class:`Vector` object and add the newly created polygon
799

800
    Parameters
801
    ----------
802
    vectorobject: Vector
803
        the vector object containing multiple polygon geometries, e.g. all geometries with a certain value in one field.
804
    expression: str or None
805
        the SQL expression to select the candidates for the largest geometry.
806
    outname: str or None
807
        the name of the output vector file; if None, an in-memory object of type :class:`Vector` is returned.
808

809
    Returns
810
    -------
811
    Vector or None
812
        if `outname` is `None`, a vector object pointing to an in-memory dataset else `None`
813
    """
UNCOV
814
    largest = None
×
UNCOV
815
    area = None
×
UNCOV
816
    vectorobject.layer.ResetReading()
×
UNCOV
817
    if expression is not None:
×
UNCOV
818
        vectorobject.layer.SetAttributeFilter(expression)
×
819
    for feat in vectorobject.layer:
×
820
        geom = feat.GetGeometryRef()
×
821
        geom_area = geom.GetArea()
×
822
        if (largest is None) or (geom_area > area):
×
823
            largest = feat.GetFID()
×
824
            area = geom_area
×
825
    if expression is not None:
×
826
        vectorobject.layer.SetAttributeFilter('')
×
827
    vectorobject.layer.ResetReading()
×
828
    
829
    feat_major = vectorobject.layer.GetFeature(largest)
×
830
    major = feat_major.GetGeometryRef()
×
831
    
832
    boundary = major.Boundary()
×
UNCOV
833
    if boundary.GetGeometryName() == 'LINESTRING':
×
834
        longest = boundary
×
835
    else:  # MULTILINESTRING
UNCOV
836
        longest = None
×
837
        for line in boundary:
×
838
            if (longest is None) or (line.Length() > longest.Length()):
×
839
                longest = line
×
840
    
841
    points = longest.GetPoints()
×
842
    ring = ogr.Geometry(ogr.wkbLinearRing)
×
843
    
844
    for point in points:
×
UNCOV
845
        ring.AddPoint_2D(*point)
×
846
    ring.CloseRings()
×
847
    
UNCOV
848
    poly = ogr.Geometry(ogr.wkbPolygon)
×
849
    poly.AddGeometry(ring)
×
850
    
851
    vec_out = Vector(driver='Memory')
×
UNCOV
852
    vec_out.addlayer('layer',
×
853
                     vectorobject.layer.GetSpatialRef(),
854
                     poly.GetGeometryType())
UNCOV
855
    vec_out.addfield('area', ogr.OFTReal)
×
856
    fields = {'area': poly.Area()}
×
857
    vec_out.addfeature(poly, fields=fields)
×
858
    
UNCOV
859
    ring = None
×
860
    geom = None
×
861
    line = None
×
862
    longest = None
×
UNCOV
863
    poly = None
×
864
    boundary = None
×
865
    major = None
×
866
    feat_major = None
×
867
    
868
    if outname is not None:
×
869
        vec_out.write(outname)
×
870
        vec_out.close()
×
871
    else:
UNCOV
872
        return vec_out
×
873

874

875
def centerdist(obj1, obj2):
1✔
UNCOV
876
    if not isinstance(obj1, Vector) or isinstance(obj2, Vector):
×
877
        raise IOError('both objects must be of type Vector')
×
878
    
UNCOV
879
    feature1 = obj1.getFeatureByIndex(0)
×
UNCOV
880
    geometry1 = feature1.GetGeometryRef()
×
881
    center1 = geometry1.Centroid()
×
882
    
UNCOV
883
    feature2 = obj2.getFeatureByIndex(0)
×
884
    geometry2 = feature2.GetGeometryRef()
×
885
    center2 = geometry2.Centroid()
×
886
    
UNCOV
887
    return center1.Distance(center2)
×
888

889

890
def dissolve(infile, outfile, field, layername=None):
1✔
891
    """
892
    dissolve the polygons of a vector file by an attribute field
893
    Parameters
894
    ----------
895
    infile: str
896
        the input vector file
897
    outfile: str
898
        the output shapefile
899
    field: str
900
        the field name to merge the polygons by
901
    layername: str
902
        the name of the output vector layer;
903
        If set to None the layername will be the basename of infile without extension
904

905
    Returns
906
    -------
907

908
    """
909
    with Vector(infile) as vec:
1✔
910
        srs = vec.srs
1✔
911
        feat = vec.layer[0]
1✔
912
        d = feat.GetFieldDefnRef(field)
1✔
913
        width = d.width
1✔
914
        type = d.type
1✔
915
        feat = None
1✔
916
    
917
    layername = layername if layername is not None else os.path.splitext(os.path.basename(infile))[0]
1✔
918
    
919
    # the following can be used if GDAL was compiled with the spatialite extension
920
    # not tested, might need some additional/different lines
921
    # with Vector(infile) as vec:
922
    #     vec.vector.ExecuteSQL('SELECT ST_Union(geometry), {0} FROM {1} GROUP BY {0}'.format(field, vec.layername),
923
    #                          dialect='SQLITE')
924
    #     vec.write(outfile)
925
    
926
    conn = sqlite_setup(extensions=['spatialite', 'gdal'])
1✔
927
    conn.execute('CREATE VIRTUAL TABLE merge USING VirtualOGR("{}");'.format(infile))
1✔
928
    select = conn.execute('SELECT {0},asText(ST_Union(geometry)) as geometry FROM merge GROUP BY {0};'.format(field))
1✔
929
    fetch = select.fetchall()
1✔
930
    with Vector(driver='Memory') as merge:
1✔
931
        merge.addlayer(layername, srs, ogr.wkbPolygon)
1✔
932
        merge.addfield(field, type=type, width=width)
1✔
933
        for i in range(len(fetch)):
1✔
934
            merge.addfeature(ogr.CreateGeometryFromWkt(fetch[i][1]), {field: fetch[i][0]})
1✔
935
        merge.write(outfile)
1✔
936
    conn.close()
1✔
937

938

939
def feature2vector(feature, ref, layername=None):
1✔
940
    """
941
    create a Vector object from ogr features
942

943
    Parameters
944
    ----------
945
    feature: list[osgeo.ogr.Feature] or osgeo.ogr.Feature
946
        a single feature or a list of features
947
    ref: Vector
948
        a reference Vector object to retrieve geo information from
949
    layername: str or None
950
        the name of the output layer; retrieved from `ref` if `None`
951

952
    Returns
953
    -------
954
    Vector
955
        the new Vector object
956
    """
957
    features = feature if isinstance(feature, list) else [feature]
1✔
958
    layername = layername if layername is not None else ref.layername
1✔
959
    vec = Vector(driver='Memory')
1✔
960
    vec.addlayer(layername, ref.srs, ref.geomType)
1✔
961
    feat_def = features[0].GetDefnRef()
1✔
962
    fields = [feat_def.GetFieldDefn(x) for x in range(0, feat_def.GetFieldCount())]
1✔
963
    vec.layer.CreateFields(fields)
1✔
964
    for feat in features:
1✔
965
        feat2 = ogr.Feature(vec.layer.GetLayerDefn())
1✔
966
        feat2.SetFrom(feat)
1✔
967
        vec.layer.CreateFeature(feat2)
1✔
968
    vec.init_features()
1✔
969
    return vec
1✔
970

971

972
def intersect(obj1, obj2):
1✔
973
    """
974
    intersect two Vector objects
975

976
    Parameters
977
    ----------
978
    obj1: Vector
979
        the first vector object; this object is reprojected to the CRS of obj2 if necessary
980
    obj2: Vector
981
        the second vector object
982

983
    Returns
984
    -------
985
    Vector or None
986
        the intersection of obj1 and obj2 if both intersect and None otherwise
987
    """
988
    if not isinstance(obj1, Vector) or not isinstance(obj2, Vector):
1✔
UNCOV
989
        raise RuntimeError('both objects must be of type Vector')
×
990
    
991
    obj1 = obj1.clone()
1✔
992
    obj2 = obj2.clone()
1✔
993
    
994
    obj1.reproject(obj2.srs)
1✔
995
    
996
    #######################################################
997
    # create basic overlap
998
    
999
    def union(vector):
1✔
1000
        out = ogr.Geometry(ogr.wkbMultiPolygon)
1✔
1001
        for feat in vector.layer:
1✔
1002
            geom = feat.GetGeometryRef()
1✔
1003
            type = geom.GetGeometryName()
1✔
1004
            if type == 'MULTIPOLYGON':
1✔
UNCOV
1005
                for subgeom in geom:
×
UNCOV
1006
                    out.AddGeometry(subgeom)
×
1007
            else:
1008
                out.AddGeometry(geom)
1✔
1009
        vector.layer.ResetReading()
1✔
1010
        out.Simplify(0)
1✔
1011
        return out
1✔
1012
    
1013
    # get the union of all polygons of the two layers
1014
    union1 = union(obj1)
1✔
1015
    union2 = union(obj2)
1✔
1016
    
1017
    # intersection
1018
    intersect_base = union1.Intersection(union2)
1✔
1019
    
1020
    union1 = None
1✔
1021
    union2 = None
1✔
1022
    #######################################################
1023
    # compute detailed per-geometry overlaps
1024
    if intersect_base.GetArea() > 0:
1✔
1025
        intersection = Vector(driver='Memory')
1✔
1026
        intersection.addlayer('intersect', obj1.srs, ogr.wkbPolygon)
1✔
1027
        fieldmap = []
1✔
1028
        for index, fielddef in enumerate([obj1.fieldDefs, obj2.fieldDefs]):
1✔
1029
            for field in fielddef:
1✔
1030
                name = field.GetName()
1✔
1031
                i = 2
1✔
1032
                while name in intersection.fieldnames:
1✔
1033
                    name = '{}_{}'.format(field.GetName(), i)
1✔
1034
                    i += 1
1✔
1035
                fieldmap.append((index, field.GetName(), name))
1✔
1036
                intersection.addfield(name, type=field.GetType(), width=field.GetWidth())
1✔
1037
        
1038
        for feature1 in obj1.layer:
1✔
1039
            geom1 = feature1.GetGeometryRef()
1✔
1040
            if geom1.Intersects(intersect_base):
1✔
1041
                for feature2 in obj2.layer:
1✔
1042
                    geom2 = feature2.GetGeometryRef()
1✔
1043
                    # select only the intersections
1044
                    if geom2.Intersects(intersect_base):
1✔
1045
                        intersect = geom2.Intersection(geom1)
1✔
1046
                        fields = {}
1✔
1047
                        for item in fieldmap:
1✔
1048
                            if item[0] == 0:
1✔
1049
                                fields[item[2]] = feature1.GetField(item[1])
1✔
1050
                            else:
1051
                                fields[item[2]] = feature2.GetField(item[1])
1✔
1052
                        intersection.addfeature(intersect, fields)
1✔
1053
        intersect_base = None
1✔
1054
        return intersection
1✔
1055

1056

1057
def wkt2vector(wkt, srs, layername='wkt'):
1✔
1058
    """
1059
    convert well-known text geometries to a Vector object.
1060

1061
    Parameters
1062
    ----------
1063
    wkt: str or list[str]
1064
        the well-known text description(s). Each geometry will be placed in a separate feature.
1065
    srs: int or str
1066
        the spatial reference system; see :func:`spatialist.auxil.crsConvert` for options.
1067
    layername: str
1068
        the name of the internal :class:`osgeo.ogr.Layer` object.
1069

1070
    Returns
1071
    -------
1072
    Vector
1073
        the vector representation
1074

1075
    Examples
1076
    --------
1077
    >>> from spatialist.vector import wkt2vector
1078
    >>> wkt1 = 'POLYGON ((0. 0., 0. 1., 1. 1., 1. 0., 0. 0.))'
1079
    >>> with wkt2vector(wkt1, srs=4326) as vec:
1080
    >>>     print(vec.getArea())
1081
    1.0
1082
    >>> wkt2 = 'POLYGON ((1. 1., 1. 2., 2. 2., 2. 1., 1. 1.))'
1083
    >>> with wkt2vector([wkt1, wkt2], srs=4326) as vec:
1084
    >>>     print(vec.getArea())
1085
    2.0
1086
    """
1087
    if isinstance(wkt, str):
1✔
1088
        wkt = [wkt]
1✔
1089
    srs = crsConvert(srs, 'osr')
1✔
1090
    vec = Vector(driver='Memory')
1✔
1091
    area = []
1✔
1092
    for item in wkt:
1✔
1093
        geom = ogr.CreateGeometryFromWkt(item)
1✔
1094
        geom.FlattenTo2D()
1✔
1095
        if not hasattr(vec, 'layer'):
1✔
1096
            vec.addlayer(layername, srs, geom.GetGeometryType())
1✔
1097
        if geom.GetGeometryName() != 'POINT':
1✔
1098
            area.append(geom.Area())
1✔
1099
        else:
UNCOV
1100
            area.append(None)
×
1101
        vec.addfeature(geom)
1✔
1102
        geom = None
1✔
1103
    vec.addfield('area', ogr.OFTReal, values=area)
1✔
1104
    return vec
1✔
1105

1106

1107
def vectorize(target, reference, outname=None, layername='layer', fieldname='value', driver=None):
1✔
1108
    """
1109
    Vectorization of an array using :func:`osgeo.gdal.Polygonize`.
1110
    
1111
    Parameters
1112
    ----------
1113
    target: numpy.ndarray
1114
        the input array. Each identified object of pixels with the same value will be converted into a vector feature.
1115
    outname: str or None
1116
        the name of the vector file. If `None` a vector object is returned.
1117
    reference: Raster
1118
        a reference Raster object to retrieve geo information and extent from.
1119
    layername: str
1120
        the name of the vector object layer.
1121
    fieldname: str
1122
        the name of the field to contain the raster value for the respective vector feature.
1123
    driver: str or None
1124
        the vector file type of `outname`. Several extensions are read automatically (see :meth:`Vector.write`).
1125
        Is ignored if ``outname=None``.
1126

1127
    Returns
1128
    -------
1129

1130
    """
UNCOV
1131
    cols = reference.cols
×
UNCOV
1132
    rows = reference.rows
×
UNCOV
1133
    meta = reference.raster.GetMetadata()
×
UNCOV
1134
    geo = reference.raster.GetGeoTransform()
×
UNCOV
1135
    proj = reference.raster.GetProjection()
×
1136
    
1137
    tmp_driver = gdal.GetDriverByName('MEM')
×
1138
    tmp = tmp_driver.Create(layername, cols, rows, 1, GDT_Byte)
×
1139
    tmp.SetMetadata(meta)
×
1140
    tmp.SetGeoTransform(geo)
×
UNCOV
1141
    tmp.SetProjection(proj)
×
1142
    outband = tmp.GetRasterBand(1)
×
1143
    outband.WriteArray(target, 0, 0)
×
1144
    
1145
    try:
×
1146
        with Vector(driver='Memory') as vec:
×
1147
            vec.addlayer(name=layername, srs=proj,
×
1148
                         geomType=ogr.wkbPolygon)
UNCOV
1149
            vec.addfield(fieldname, ogr.OFTInteger)
×
1150
            
1151
            gdal.Polygonize(srcBand=outband, maskBand=None,
×
1152
                            outLayer=vec.layer, iPixValField=0)
UNCOV
1153
            if outname is not None:
×
1154
                vec.write(outfile=outname, driver=driver)
×
1155
            else:
1156
                return vec.clone()
×
UNCOV
1157
    except Exception as e:
×
1158
        raise e
×
1159
    finally:
UNCOV
1160
        outband = None
×
1161
        tmp_driver = None
×
1162
        out = None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc