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

johntruckenbrodt / spatialist / 14352435847

09 Apr 2025 08:21AM UTC coverage: 80.084% (+0.2%) from 79.933%
14352435847

Pull #55

github

johntruckenbrodt
[Vector.to_geopandas] convert DateTime fields to pandas datetime
Pull Request #55: [Vector.to_geopandas] new method

23 of 24 new or added lines in 2 files covered. (95.83%)

3 existing lines in 2 files now uncovered.

1918 of 2395 relevant lines covered (80.08%)

0.8 hits per line

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

70.75
/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 pandas as pd
1✔
17
import geopandas as gpd
1✔
18
from shapely.wkb import loads as wkb_loads
1✔
19

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

23

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

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

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

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

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

150
        Returns
151
        -------
152

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

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

192
        Returns
193
        -------
194

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

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

233
        Returns
234
        -------
235

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

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

251
        Returns
252
        -------
253

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

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

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

291
        Returns
292
        -------
293

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

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

309
        Returns
310
        -------
311

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

493
        Returns
494
        -------
495

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

504
        Returns
505
        -------
506

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

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

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

537
        Returns
538
        -------
539

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

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

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

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

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

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

599
        Returns
600
        -------
601

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

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

641
        Returns
642
        -------
643

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

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

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

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

724
        Returns
725
        -------
726

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

744

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

793

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

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

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

879

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

894

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

910
    Returns
911
    -------
912

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

943

944
def feature2vector(feature, ref, layername=None):
1✔
945
    """
946
    create a Vector object from ogr features
947

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

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

976

977
def intersect(obj1, obj2):
1✔
978
    """
979
    intersect two Vector objects
980

981
    Parameters
982
    ----------
983
    obj1: Vector
984
        the first vector object; this object is reprojected to the CRS of obj2 if necessary
985
    obj2: Vector
986
        the second vector object
987

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

1061

1062
def wkt2vector(wkt, srs, layername='wkt'):
1✔
1063
    """
1064
    convert well-known text geometries to a Vector object.
1065

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

1075
    Returns
1076
    -------
1077
    Vector
1078
        the vector representation
1079

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

1111

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

1132
    Returns
1133
    -------
1134

1135
    """
1136
    cols = reference.cols
×
1137
    rows = reference.rows
×
1138
    meta = reference.raster.GetMetadata()
×
1139
    geo = reference.raster.GetGeoTransform()
×
1140
    proj = reference.raster.GetProjection()
×
1141
    
1142
    tmp_driver = gdal.GetDriverByName('MEM')
×
1143
    tmp = tmp_driver.Create(layername, cols, rows, 1, GDT_Byte)
×
1144
    tmp.SetMetadata(meta)
×
1145
    tmp.SetGeoTransform(geo)
×
1146
    tmp.SetProjection(proj)
×
1147
    outband = tmp.GetRasterBand(1)
×
1148
    outband.WriteArray(target, 0, 0)
×
1149
    
1150
    try:
×
1151
        with Vector(driver='Memory') as vec:
×
1152
            vec.addlayer(name=layername, srs=proj,
×
1153
                         geomType=ogr.wkbPolygon)
1154
            vec.addfield(fieldname, ogr.OFTInteger)
×
1155
            
1156
            gdal.Polygonize(srcBand=outband, maskBand=None,
×
1157
                            outLayer=vec.layer, iPixValField=0)
1158
            if outname is not None:
×
1159
                vec.write(outfile=outname, driver=driver)
×
1160
            else:
1161
                return vec.clone()
×
1162
    except Exception as e:
×
1163
        raise e
×
1164
    finally:
1165
        outband = None
×
1166
        tmp_driver = None
×
1167
        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