Skip to content

gk

A convenience module which automatically imports all of geokit modules into the same namespace.

Functions:

  • KernelProcessor

    A decorator which automates the production of kernel processors for use

  • applyBuffer

    This function applies a buffer to any geom, avoiding edge issues with geoms

  • applyGeopandasMethod

    Applies an arbitrary function to the data frames provided.

  • box

    Make an ogr polygon object from extents.

  • centeredLAEA

    Load a Lambert-Azimuthal-Equal_Area spatial reference system (SRS) centered

  • combineSimilarRasters

    Combines several similar raster files into one single raster file.

  • compare_geoms

    Compare two lists of geometries and return a list of booleans indicating whether each pair of geometries are equal.

  • contours

    Create contour geometries at specified edges for the given raster data.

  • convertGeoJson

    Make a geometry from a well known text (WKT) string.

  • convertWKT

    Make a geometry from a well known text (WKT) string.

  • countFeatures

    Returns the number of features found in the given source and within a

  • createDataFrameFromGeoDataFrame

    Creates a geokit-style dataframe from a geopandas geodataframe.

  • createGeoDataFrame

    Creates a gdf from an Reskit shape pd.DataFrame.

  • createGeoJson

    Convert a set of geometries to a geoJSON object.

  • createRaster

    Create a raster file.

  • createRasterLike

    Create a raster described by the given raster info (as returned from a

  • createVector

    Create a vector on disk from geometries or a DataFrame with 'geom' column.

  • divideMultipolygonIntoEasternAndWesternPart

    Multipolygons spanning the antimeridian (this includes polygons that are

  • drawGeoms

    Draw geometries onto a matplotlib figure.

  • drawImage

    Draw a matrix as an image on a matplotlib canvas.

  • drawRaster

    Draw a raster as an image on a matplotlib canvas.

  • drawSmopyMap

    Draws a basemap using the "smopy" python package.

  • empty

    Make a generic OGR geometry of a desired type.

  • extractAndClipFeatures

    Extracts features from a source and clips them to the boundaries of a given geom.

  • extractAsDataFrame

    Convenience function calling extractFeatures and structuring the output as

  • extractFeature

    Convenience function calling extractFeatures which assumes there is only

  • extractFeatures

    Creates a generator which extract the features contained within the source.

  • extractMatrix

    Extract all or part of a raster's band as a numpy matrix.

  • extractValues

    Extracts the value of a raster at a given point or collection of points.

  • extractVerticies

    Get all vertices found on the geometry as a Nx2 numpy.ndarray.

  • fixOutOfBoundsGeoms

    This function allows to deal with polygons that protrude over the SRS bounds

  • flatten

    Flatten a list of geometries into a single geometry object.

  • gradient

    Calculate a raster's gradient and return as a new dataset or simply a matrix.

  • indexToCoord

    Convert the index of a raster to coordinate values.

  • interpolateValues

    Interpolates the value of a raster at a given point or collection of points.

  • isRaster

    Test if loadRaster fails for the given input.

  • isVector

    Test if loadVector fails for the given input.

  • line

    Creates an OGR Line object from a given set of points.

  • listLayers

    Returns the layer names for each layer that is stored in a geopackage.

  • loadRaster

    Load a raster dataset from a path to a file on disc.

  • loadSRS

    Load a spatial reference system (SRS) from various sources.

  • loadVector

    Load a vector dataset from a path to a file on disc.

  • makeBox

    Alias for geokit.geom.box(...).

  • makeEmpty

    Alias for geokit.geom.empty(...).

  • makeLine

    Alias for geokit.geom.line(...).

  • makePoint

    Alias for geokit.geom.point(...).

  • makePolygon

    Alias for geokit.geom.polygon(...).

  • mutateRaster

    Process all pixels in a raster according to a given function. The boundaries

  • mutateVector

    Process a vector dataset according to an arbitrary function.

  • ogrType

    Tries to determine the corresponding OGR type according to the input.

  • partition

    Partition a Polygon into some number of pieces whose areas should be close

  • point

    Make a simple point geometry.

  • polygon

    Creates an OGR Polygon object from a given set of points.

  • polygonizeMask

    Create a geometry set from a matrix mask.

  • polygonizeMatrix

    Create a geometry set from a matrix of integer values.

  • polygonizeRaster

    Polygonize a raster or an integer-valued data matrix.

  • rasterCellNo

    Returns the raster cell number for one or multiple points defined by geometry or lon/lat. Cell numeration

  • rasterInfo

    Returns a named tuple containing information relating to the input raster.

  • rasterStats

    Compute basic statistics of the values contained in a raster dataset.

  • rasterize

    Rasterize a vector datasource onto a raster context.

  • saveRasterAsTif

    Write a osgeo.gdal.Dataset in memory to a GeoTiff file to disk.

  • scaleMatrix

    Scale a 2-dimensional matrix. For example, a 2x2 matrix, with a scale of 2,

  • shift

    Shift a polygon in longitudinal and/or latitudinal direction.

  • sieve

    Removes raster polygons smaller than a provided threshold size (in pixels) and

  • subTiles

    Generate a collection of tiles which encompass the passed geometry.

  • tile

    Generates a box corresponding to a tile used for "slippery maps".

  • tileAt

    Generates a box corresponding to a tile at the coordinates 'x' and 'y'

  • tileIndexAt

    Get the "slippery tile" index at the given zoom, around the

  • tileize

    Deconstruct a given geometry into a set of tiled geometries.

  • transform

    Transform a geometry, or a list of geometries, from one SRS to another.

  • vectorInfo

    Extract general information about a vector source.

  • warp

    Warps a given raster source to another context.

  • warpLike

    Convenience function to warp a raster to the context of another raster

  • xyTransform

    Transform xy points between coordinate systems.

KernelProcessor

KernelProcessor(
    size, edgeValue=0, outputType=None, passIndex=False
)

A decorator which automates the production of kernel processors for use in mutateRaster (although it could really used for processing any matrix).

Parameters:

  • size

    (int) –

    The number of pixels to expand around a center pixel * A 'size' of 0 would make a processing matrix with size 1x1. As in, just the value at each point. This would be silly to call... * A 'size' of 1 would make a processing matrix of size 3x3. As in, one pixel around the center pixel in all directions * Processed matrix size is equal to 2*size+1

  • edgeValue

    (numeric; optional, default: 0 ) –

    The value to apply to the edges of the matrix before applying the kernel * Will be factored into the kernelling when processing near the edges

  • outputType

    (np.dtype; optional, default: None ) –

    The datatype of the processed values * Only useful if the output type of the kerneling step is different from the matrix input type

  • passIndex

    (bool, default: False ) –

    Whether or not to pass the x and y index to the processing function * If True, the decorated function must accept an input called 'xi' and 'yi' in addition to the matrix * The xi and yi correspond to the index of the center pixel in the original matrix

Returns:

  • function
Example:
  • Say we want to make a processor which calculates the average of pixels which are within a distance of 2 indices. In other words, we want the average of a 5x5 matrix centered around each pixel.
  • Assume that we can use the value -9999 as a no data value

@KernelProcessor(2, edgeValue=-9999) def getMean( mat ): # Get only good values goodValues = mat[mat!=-9999]

 # Return the mean
 return goodValues.mean()
Source code in geokit/core/util.py
def KernelProcessor(size, edgeValue=0, outputType=None, passIndex=False):
    """A decorator which automates the production of kernel processors for use
    in mutateRaster (although it could really used for processing any matrix).

    Parameters
    ----------
    size : int
        The number of pixels to expand around a center pixel
        * A 'size' of 0 would make a processing matrix with size 1x1. As in,
          just the value at each point. This would be silly to call...
        * A 'size' of 1 would make a processing matrix of size 3x3. As in, one
          pixel around the center pixel in all directions
        * Processed matrix size is equal to 2*size+1

    edgeValue : numeric; optional
        The value to apply to the edges of the matrix before applying the kernel
        * Will be factored into the kernelling when processing near the edges

    outputType : np.dtype; optional
        The datatype of the processed values
        * Only useful if the output type of the kerneling step is different from
          the matrix input type

    passIndex : bool
        Whether or not to pass the x and y index to the processing function
        * If True, the decorated function must accept an input called 'xi' and
          'yi' in addition to the matrix
        * The xi and yi correspond to the index of the center pixel in the
          original matrix

    Returns
    -------
    function

    Example:
    --------
    * Say we want to make a processor which calculates the average of pixels
      which are within a distance of 2 indices. In other words, we want the
      average of a 5x5 matrix centered around each pixel.
    * Assume that we can use the value -9999 as a no data value

    >>>  @KernelProcessor(2, edgeValue=-9999)
    >>>  def getMean( mat ):
    >>>      # Get only good values
    >>>      goodValues = mat[mat!=-9999]
    >>>
    >>>      # Return the mean
    >>>      return goodValues.mean()
    """
    pass

    def wrapper1(kernel):
        pass

        def wrapper2(matrix):
            # get the original matrix sizes
            yN, xN = matrix.shape

            # make a padded version of the matrix

            # paddedMatrix = (
            #     np.ones((yN + 2 * size, xN + 2 * size),) * edgeValue
            # )
            paddedMatrix = np.full(shape=(yN + 2 * size, xN + 2 * size), fill_value=edgeValue)
            paddedMatrix[size:-size, size:-size] = matrix

            # apply kernel to each pixel
            output = np.zeros((yN, xN), dtype=matrix.dtype if outputType is None else outputType)
            for yi in range(yN):
                for xi in range(xN):
                    slicedMatrix = paddedMatrix[yi : 2 * size + yi + 1, xi : 2 * size + xi + 1]

                    if passIndex:
                        output[yi, xi] = kernel(slicedMatrix, xi=xi, yi=yi)
                    else:
                        output[yi, xi] = kernel(slicedMatrix)

            # done!
            return output

        return wrapper2

    return wrapper1

applyBuffer

applyBuffer(geom, buffer, srs=None, split='shift')

This function applies a buffer to any geom, avoiding edge issues with geoms near the SRS bounds. By shifting the geom to a zero longitude, geometry distortions are avoided when the buffered geom exceeds the bounds (i.e. antimeridian or latitudes of +/-90° e.g. in the case of EPSG:4326). Buffered geom areas extending over the bounds can either be clipped off or shifted to the respective "other end of the map". If the buffer is applied in a different (e.g. metric) EPSG, latitudinal overlaps will always be clipped.

geom : osgeo.ogr.Geometry Geometry to be buffered. buffer : int, float The buffer value to be applied to the geom, in unit of the SRS unless 'bufferInEPSG6933' is True, then always in meters. srs : Anything acceptable to geokit.srs.loadSRS(); optional Allows to specify an EPSG integer code or an osgeo.osr.SpatialReference instance to define the SRS in which the buffer will be applied, then in the unit of the specified EPSG. If e.g. 6933 is given, the buffer will be applied in meters in a metric system. Passing "laea" (case-insensitive) will generate a geometry-centered, metric Lambert Azimuthal Equal Area system in which the buffer will be applied. By default False, i.e. the original SRS of the geom will be used. split : str, optional 'shift' : shift areas that exceed the antimeridian line to the other end (default) 'clip' : remove/clip polygon parts that exceed the antimeridian 'none' : do not split geoms at all that cross the antimeridian

Source code in geokit/core/geom.py
def applyBuffer(geom, buffer, srs=None, split="shift"):
    """
    This function applies a buffer to any geom, avoiding edge issues with geoms
    near the SRS bounds. By shifting the geom to a zero longitude, geometry
    distortions are avoided when the buffered geom exceeds the bounds (i.e.
    antimeridian or latitudes of +/-90° e.g. in the case of EPSG:4326). Buffered
    geom areas extending over the bounds can either be clipped off or shifted to
    the respective "other end of the map". If the buffer is applied in a
    different (e.g. metric) EPSG, latitudinal overlaps will always be clipped.

    geom : osgeo.ogr.Geometry
        Geometry to be buffered.
    buffer : int, float
        The buffer value to be applied to the geom, in unit of the SRS unless
        'bufferInEPSG6933' is True, then always in meters.
    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        Allows to specify an EPSG integer code or an osgeo.osr.SpatialReference
        instance to define the SRS in which the buffer will be applied, then in
        the unit of the specified EPSG. If e.g. 6933 is given, the buffer will
        be applied in meters in a metric system. Passing "laea" (case-insensitive)
        will generate a geometry-centered, metric Lambert Azimuthal Equal Area
        system in which the buffer will be applied. By default False, i.e. the
        original SRS of the geom will be used.
    split : str, optional
        'shift' : shift areas that exceed the antimeridian line to the other end (default)
        'clip' : remove/clip polygon parts that exceed the antimeridian
        'none' : do not split geoms at all that cross the antimeridian
    """
    # create copy and extract SRS
    _srs_orig = geom.GetSpatialReference()
    _geom = geom.Clone()

    if srs is not None:
        # generate own geom-centered LAEA or load SRS
        try:
            # allow LAEA or EPSG or SRS object
            srs = SRS.loadSRS(srs, geom=_geom)
        except Exception as e:
            raise ValueError(f"Invalid SRS '{srs}': {e}")

        # make sure the poles are far enough to allow the planned buffer
        buffer_mtrs = buffer * srs.GetLinearUnits()
        north_srs = SRS.loadSRS("+proj=aeqd +lat_0=90 +lon_0=0 +datum=WGS84 +units=m +no_defs")
        north_dist = transform(_geom, toSRS=north_srs).Distance(point(0, 0, srs=north_srs))
        south_srs = SRS.loadSRS("+proj=aeqd +lat_0=-90 +lon_0=0 +datum=WGS84 +units=m +no_defs")
        south_dist = transform(_geom, toSRS=south_srs).Distance(point(0, 0, srs=south_srs))
        if min([north_dist, south_dist]) <= buffer_mtrs:
            raise GeoKitGeomError(f"buffered geometry would intersect with North or South Pole.")

        if not srs.IsSame(_srs_orig):
            # convert geom to srs
            _geom = transform(_geom, toSRS=srs)

    else:
        srs = _srs_orig

    # apply buffer
    _geom_buf = _geom.Buffer(buffer)
    assert _geom_buf.IsValid(), f"buffered geom invalid after applying buffer."
    # make sure that the buffered geom was not already partially projected by 360° if crossing antimeridian
    # envelope diffs on either side must be equal to buffer with 1% tolerance
    assert np.isclose(_geom.GetEnvelope()[0] - _geom_buf.GetEnvelope()[0], buffer, atol=0, rtol=0.01) and np.isclose(
        _geom_buf.GetEnvelope()[1] - _geom.GetEnvelope()[1], buffer, atol=0, rtol=0.01
    ), f"buffered geom envelope does not match unbuffered geom envelope plus buffers on both sides."

    # now retransform to original srs if needed
    if srs is not None and not srs.IsSame(_srs_orig):
        _geom_buf = transform(_geom_buf, toSRS=_srs_orig, revert360degProj=True)

    # now split if needed
    if not (split is None or isinstance(split, str) and split.upper() == "NONE"):
        _geom_buf = fixOutOfBoundsGeoms(_geom_buf, how=split)
        assert _geom_buf.IsValid(), f"buffered and re-transformed geom invalid after '{split}' operation"

    return _geom_buf

applyGeopandasMethod

applyGeopandasMethod(geopandasMethod, *dfs, **kwargs)

Applies an arbitrary function to the data frames provided.

Convenience function to apply geopandas methods to a geokit-style dataframe with 'geom' column with osgeo.ogr.Geometry objects. NOTE: All arguments besides **kwargs must be passed as positional arguments.

geopandasMethod : str, executable Geopandas method to apply to the dataframe, either str-formatted method name or method as a callable function. dfs : pd.DataFrames One or multiple comma-separated pd.DataFrames with 'geom' column with osgeo.ogr.Geometry objects. Will be passed to the geopandas method as positional arguments, starting from the first position. *kwargs Will be passed on to the geopandas function.

Source code in geokit/core/vector.py
def applyGeopandasMethod(geopandasMethod, *dfs, **kwargs):
    """
    Applies an arbitrary function to the data frames provided.

    Convenience function to apply geopandas methods to a geokit-style
    dataframe with 'geom' column with osgeo.ogr.Geometry objects.
    NOTE: All arguments besides **kwargs must be passed as positional
    arguments.

    geopandasMethod : str, executable
        Geopandas method to apply to the dataframe, either str-formatted
        method name or method as a callable function.
    *dfs : pd.DataFrames
        One or multiple comma-separated pd.DataFrames with 'geom' column
        with osgeo.ogr.Geometry objects. Will be passed to the geopandas
        method as positional arguments, starting from the first position.
    **kwargs
        Will be passed on to the geopandas function.
    """
    # load geopandas
    # try:
    #     import geopandas as gpd
    # except:
    #     raise ImportError(
    #         "'geopandas' is required for geokit.vector.createGeoDataFrame() but is not installed in the current environment."
    #     )
    # get the method as callable
    if callable(geopandasMethod):
        # we have a callable function already, just make sure its gpd
        if not geopandasMethod.__module__.split(".")[0] == "geopandas":
            raise AttributeError(f"'{geopandasMethod}' is not a callable method from geopandas!")
    else:
        # not an executable yet, get the executable function via its name
        if not isinstance(geopandasMethod, str):
            # must be a str formatted name
            raise TypeError(f"'geopandasMethod' must be str-formatted geopandas method name if not callable.")
        geopandasMethod = getattr(gpd, geopandasMethod, None)

    # create geodataframes from input geokit dfs
    assert all([isinstance(_df, pd.DataFrame) and "geom" in _df.columns for _df in dfs]), (
        f"positional *dfs arguments must be geokit-style pd.DataFrames with 'geom' column."
    )
    gdfs = [createGeoDataFrame(dfGeokit=_df) for _df in dfs]

    # now apply geopandas method onto gdfs
    gdf = geopandasMethod(*gdfs, **kwargs)
    # now convert the result back to geokit style and return
    df = createDataFrameFromGeoDataFrame(gdf)
    return df

box

box(*args, srs: srs_input = 4326) -> Geometry

Make an ogr polygon object from extents.

Parameters:

  • *args

    (4 numeric argument, or one tuple argument with 4 numerics, default: () ) –

    The X_Min, Y_Min, X_Max and Y_Max bounds of the box to create

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: 4326 ) –

    The srs of the point to create * If not given, longitude/latitude is assumed * srs MUST be given as a keyword argument

Returns:

  • Geometry
Example:

box(xMin, yMin, xMax, yMax [, srs]) box( (xMin, yMin, xMax, yMax) [, srs])

Source code in geokit/core/geom.py
def box(*args, srs: srs_input = 4326) -> ogr.Geometry:
    """Make an ogr polygon object from extents.

    Parameters
    ----------
    *args : 4 numeric argument, or one tuple argument with 4 numerics
        The X_Min, Y_Min, X_Max and Y_Max bounds of the box to create

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the point to create
          * If not given, longitude/latitude is assumed
          * srs MUST be given as a keyword argument

    Returns
    -------
    ogr.Geometry

    Example:
    --------
    box(xMin, yMin, xMax, yMax [, srs])
    box( (xMin, yMin, xMax, yMax) [, srs])
    """
    if len(args) == 1:
        xMin, yMin, xMax, yMax = args[0]
    elif len(args) == 4:
        xMin, yMin, xMax, yMax = args
    else:
        raise GeoKitGeomError(
            'Incorrect number positional inputs (only accepts 1 or 4). Did you mean to specify "srs="?'
        )

    # make sure inputs are good
    xMin = float(xMin)
    xMax = float(xMax)
    yMin = float(yMin)
    yMax = float(yMax)

    assert xMin < xMax, f"xMin must be less than xMax"
    assert yMin < yMax, f"yMin must be less than yMax"

    # make box
    outBox = ogr.Geometry(ogr.wkbPolygon)

    ring = ogr.Geometry(ogr.wkbLinearRing)
    for x, y in [(xMin, yMin), (xMax, yMin), (xMax, yMax), (xMin, yMax), (xMin, yMin)]:
        ring.AddPoint(x, y)

    outBox.AddGeometry(ring)
    if not srs is None:
        srs = SRS.loadSRS(srs)
        outBox.AssignSpatialReference(srs)
    return outBox

centeredLAEA

centeredLAEA(
    lon=None, lat=None, name="unnamed_m", geom=None
)

Load a Lambert-Azimuthal-Equal_Area spatial reference system (SRS) centered on a given set of latitude and longitude coordinates. Alternatively, a geom can be passed to center the LAEA on.

Parameters:

  • lon

    (float, default: None ) –

    The longitude of the projection's center. Required if no geom is given.

  • lat

    (float, default: None ) –

    The latitude of the projection's center. Required if no geom is given.

  • geom

    The region shape to center the LAEA in. If given, lat and lon must not be given, instead they will be defined automatically as the coordinates of the region centroid.

Returns:

  • SpatialReference
Source code in geokit/core/srs.py
def centeredLAEA(lon=None, lat=None, name="unnamed_m", geom=None):
    """
    Load a Lambert-Azimuthal-Equal_Area spatial reference system (SRS) centered
    on a given set of latitude and longitude coordinates. Alternatively, a geom
    can be passed to center the LAEA on.

    Parameters
    ----------
    lon : float
        The longitude of the projection's center. Required if no geom is given.

    lat : float
        The latitude of the projection's center. Required if no geom is given.

    geom: osgeo.ogr.Geometry
        The region shape to center the LAEA in. If given, lat and lon must not
        be given, instead they will be defined automatically as the coordinates
        of the region centroid.

    Returns
    -------
    osr.SpatialReference
    """
    if geom is None:
        assert isinstance(lat, numbers.Number) and isinstance(lon, numbers.Number), (
            "If geom is not passed, lat and lon must be given as float values."
        )
    else:
        assert isinstance(geom, ogr.Geometry), "geom must be given as osgeo.ogr.Geometry class object if not None."
        assert lat is None and lon is None, "If geom is given, lat and lon must not be given."

    # check if lat/lon can be used or if it must be extracted from geom first
    if not geom is None:
        # transform to EPSG:4326 in case it not already is lat/lon projection
        geom = GEOM.transform(geom, toSRS=4326)
        # extract lat/lon centroid coordinates to center LAEA upon
        lon = geom.Centroid().GetX()
        lat = geom.Centroid().GetY()

    srs = osr.SpatialReference()
    srs.ImportFromWkt(
        'PROJCS["{}",GEOGCS["GRS 1980(IUGG, 1980)",DATUM["unknown",SPHEROID["GRS80",6378137,298.257222101],TOWGS84[0,0,0,0,0,0,0]],PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],PROJECTION["Lambert_Azimuthal_Equal_Area"],PARAMETER["latitude_of_center",{}],PARAMETER["longitude_of_center",{}],PARAMETER["false_easting",0],PARAMETER["false_northing",0],UNIT["Meter",1]]'.format(
            name, float(lat), float(lon)
        )
    )

    if gdal.__version__ >= "3.0.0":
        srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)

    # assert that the srs is valid (may be invalid if e.g. wrong integer codes were passed)
    assert srs.Validate() == 0, f"Created srs is invalid."

    return srs

combineSimilarRasters

combineSimilarRasters(
    datasets,
    output=None,
    combiningFunc=None,
    verbose=True,
    updateMeta=False,
    allowNumericMismatch=False,
    **kwargs,
)

Combines several similar raster files into one single raster file.

Parameters:

  • datasets

    (string or list) –

    glob string path describing datasets to combine, alternatively list of gdal.Datasets or iterable object with paths.

  • output

    (string, default: None ) –

    Filepath to output raster file. If it is an existing file, datasets will be added to output. Recommended to create a new file every time though. If None, no output dataset will be loaded or created on disk and output dataset kept in memory only, by default None

  • combiningFunc

    ([type], default: None ) –

    Allows customized functions to combine matrices, by default None

  • verbose

    (bool, default: True ) –

    If True, additional status print stamenets will be issued, by default True

  • updateMeta

    (bool, default: False ) –

    If True, metadata of output dataset will be a combination of all input rasters, by default False. NOTE: In the case of multiple values for the metadata keys, the last dataset metadata will take precedence.

  • allowNumericMismatch

    (bool, default: False ) –

    If True, minor deviations in raster context will be ignored/corrected. By default False, i.e. only exactly similar rasters will be combined.

  • **kwargs

    Will be passed on to geokit.raster.createRaster().

Returns:

  • output dataset: osgeo.gdal.Dataset

    Raster file containing the combined matrices of all input datasets.

Source code in geokit/_algorithms/combineSimilarRasters.py
def combineSimilarRasters(
    datasets,
    output=None,
    combiningFunc=None,
    verbose=True,
    updateMeta=False,
    allowNumericMismatch=False,
    **kwargs,
):
    """
    Combines several similar raster files into one single raster file.

    Parameters
    ----------
    datasets : string or list
        glob string path describing datasets to combine, alternatively list of
        gdal.Datasets or iterable object with paths.
    output : string, optional
        Filepath to output raster file. If it is an existing file, datasets will
        be added to output. Recommended to create a new file every time though.
        If None, no output dataset will be loaded or created on disk and output
        dataset kept in memory only, by default None
    combiningFunc : [type], optional
        Allows customized functions to combine matrices, by default None
    verbose : bool, optional
        If True, additional status print stamenets will be issued, by default
        True
    updateMeta : bool, optional
        If True, metadata of output dataset will be a combination of all input
        rasters, by default False.
        NOTE: In the case of multiple values for the metadata keys, the last
        dataset metadata will take precedence.
    allowNumericMismatch : bool, optional
        If True, minor deviations in raster context will be ignored/corrected.
        By default False, i.e. only exactly similar rasters will be combined.
    **kwargs
        Will be passed on to geokit.raster.createRaster().

    Returns
    -------
    output dataset: osgeo.gdal.Dataset
        Raster file containing the combined matrices of all input datasets.
    """
    if not isinstance(allowNumericMismatch, bool):
        raise TypeError(f"allowNumericMismatch must be boolean.")

    # CHECK AND PREPROCESS INPUT DATASETS

    # Ensure we have a list of raster datasets
    if isinstance(datasets, str):
        datasets = glob(datasets)
        datasets.sort()
    elif isinstance(datasets, gdal.Dataset):
        datasets = [
            datasets,
        ]
    else:  # assume datasets is iterable
        datasets = list(datasets)

    # make sure we do actually have rasters and they are all indeed "similar"
    if len(datasets) == 0:
        raise GeoKitError("No datasets given")
    rtol = 0
    if allowNumericMismatch:
        rtol = 0.0001  # allow a numeric deviation of 0.01%
    datasets = checkSimilarRasters(datasets=datasets, rtol=rtol)

    # GET REFERENCE CONTEXT FOR THE OUTPUT RASTER

    # determine info for all datasets
    raster_info_list = [rasterInfo(sourceDS=d, compute_statistics=True) for d in datasets]

    # get reference srs - are all the same thanks to checkSimilarRasters
    srs_ref = raster_info_list[0].srs

    # get the unique actual dtypes in input rasters
    data_type_list = []
    minimum_and_maximum_values = []
    for current_raster_info in raster_info_list:
        if current_raster_info.dtype is None:
            raise GeoKitError("Input raster has no dtype.")
        data_type_list.append(current_raster_info.data_type_name_str)
        minimum_and_maximum_values.append(current_raster_info.minimum_value)
        minimum_and_maximum_values.append(current_raster_info.maximum_value)

    # now get the most lightweight commonly usable dtype
    gdal_data_type_as_string = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_string(
        list_of_numbers=minimum_and_maximum_values, minimum_gdal_type_list=data_type_list
    )
    # get_common_dtype(dtypes=dtypes, fallback=None)

    # get the reference resolution in x and y dir as the most commonly used value
    dx_ref = statistics.mode([_i.pixelWidth for _i in raster_info_list])
    dy_ref = statistics.mode([_i.pixelHeight for _i in raster_info_list])

    # try to align all bounds to the first matching raster which has correct x_ref and y_ref resolution
    i_match = next(
        (
            i
            for i, (x, y) in enumerate(
                zip(
                    [_i.pixelWidth for _i in raster_info_list],
                    [_i.pixelHeight for _i in raster_info_list],
                )
            )
            if x == dx_ref and y == dy_ref
        ),
        None,
    )
    if i_match is not None:
        # we have a "perfect" raster, use the min. bounds of that one as reference for all other rasters
        boundsXmin_ref = raster_info_list[i_match].bounds[0]
        boundsYmin_ref = raster_info_list[i_match].bounds[2]
    else:
        # we do not have any raster which matches both r_ref any y_ref in its resolution. Simply use the first raster.
        boundsXmin_ref = raster_info_list[0].bounds[0]
        boundsYmin_ref = raster_info_list[0].bounds[2]
        if verbose:
            print(
                datetime.datetime.now(),
                f"NOTE: None of the rasters matches both reference resolutions in x and y direction. Use first raster bounds as reference.",
                flush=True,
            )

    # calculate the possibly adapted bounds for all datasets
    boundsSet = []
    for _info in raster_info_list:
        # calculate the new bounds by aligning bottom left corner with boundsXmin_ref/boundsYmin_ref + multiple of cell size
        _bounds_Xmin = boundsXmin_ref + round((_info.bounds[0] - boundsXmin_ref) / dx_ref) * dx_ref
        _bounds_Ymin = boundsYmin_ref + round((_info.bounds[1] - boundsYmin_ref) / dy_ref) * dy_ref
        _bounds = (
            _bounds_Xmin,
            _bounds_Ymin,
            _bounds_Xmin + _info.xWinSize * dx_ref,
            _bounds_Ymin + _info.yWinSize * dy_ref,
        )
        # make sure the number of cells would remain the same in the new bounds
        assert round(_info.xWinSize * dx_ref / _info.dx, 0) == _info.xWinSize, (
            f"The change in bounds width would lead to a different amount of cell columns in the original raster."
        )
        assert round(_info.yWinSize * dy_ref / _info.dy, 0) == _info.yWinSize, (
            f"The change in bounds height would lead to a different amount of cell rows in the original raster."
        )
        boundsSet.append(_bounds)

    # get summary info about the whole dataset group
    dataXMin = min([i[0] for i in boundsSet])
    dataXMax = max([i[2] for i in boundsSet])
    dataYMin = min([i[1] for i in boundsSet])
    dataYMax = max([i[3] for i in boundsSet])

    # get noData value from kwargs, else take from dataset infos
    noData_ref = kwargs.pop("noData", None)
    if noData_ref is None:
        noDataSet = set([i.noData for i in raster_info_list])
        assert len(noDataSet) == 1  # make sure, is enforced by checkSimilarRasters
        noData_ref = noDataSet.pop()

    # Maybe create a new output dataset
    if isinstance(output, str):
        if not os.path.isfile(output):
            # we will need to create a output source
            createRaster(
                bounds=(dataXMin, dataYMin, dataXMax, dataYMax),
                output=output,
                dtype=gdal_data_type_as_string,
                pixelWidth=dx_ref,
                pixelHeight=dy_ref,
                noData=noData_ref,
                srs=srs_ref,
                fill=noData_ref,
                **kwargs,
            )
        else:
            warn(
                "WARNING: Overwriting existing output file. Sometimes writing to an non empty output fails. Recommended to write to a non existing location instead and include maser into datasets."
            )
    elif output is None:
        # create raster in memory
        outputDS = createRaster(
            bounds=(dataXMin, dataYMin, dataXMax, dataYMax),
            dtype=gdal_data_type_as_string,
            pixelWidth=dx_ref,
            pixelHeight=dy_ref,
            noData=noData_ref,
            srs=srs_ref,
            fill=noData_ref,
            **kwargs,
        )
    else:
        raise TypeError(
            "output must be None or a str formatted file path to an existing output file or a file to be created."
        )

    # open output dataset if required and check parameters
    if not output is None:
        outputDS = gdal.Open(output, gdal.GA_Update)
    mInfo = rasterInfo(outputDS)
    mExtent = Extent(mInfo.bounds, srs=mInfo.srs)

    outputBand = outputDS.GetRasterBand(1)

    # Make a meta container
    if updateMeta:
        meta = outputDS.GetMetadata_Dict()

    # Add each dataset to output
    for i in range(len(datasets)):
        if verbose:
            print(
                datetime.datetime.now(),
                f"Now adding raster No. {i + 1}/{len(datasets)}",
            )
        # create dataset extent
        dExtent = Extent(boundsSet[i], srs=srs_ref)

        # extract the dataset's matrix
        dMatrix = extractMatrix(datasets[i])
        if not raster_info_list[i].yAtTop:
            dMatrix = dMatrix[::-1, :]

        # Calculate starting indices
        idx = mExtent.findWithin(dExtent, (mInfo.dx, mInfo.dy), yAtTop=mInfo.yAtTop)

        # Get output data
        mMatrix = outputBand.ReadAsArray(xoff=idx.xStart, yoff=idx.yStart, win_xsize=idx.xWin, win_ysize=idx.yWin)
        if mMatrix is None:
            raise GeoKitError("mMatrix is None")

        # create selector
        if not combiningFunc is None:
            # update rasterInfo since we might have slightly changed cell/bounds info
            rInfo_dict = raster_info_list[i]._asdict()
            rInfo_dict["bounds"] = boundsSet[i]
            rInfo_dict["pixelWidth"] = dx_ref
            rInfo_dict["pixelHeight"] = dy_ref
            rInfo_dict["dx"] = dx_ref
            rInfo_dict["dy"] = dy_ref
            rInfo_dict["xMin"] = boundsSet[i][0]
            rInfo_dict["yMin"] = boundsSet[i][1]
            rInfo_dict["xMax"] = boundsSet[i][2]
            rInfo_dict["yMax"] = boundsSet[i][3]
            rInfo_upd = RasterInfo(**rInfo_dict)
            writeMatrix = combiningFunc(mMatrix=mMatrix, mInfo=mInfo, dMatrix=dMatrix, dInfo=rInfo_upd)
        elif not raster_info_list[i].noData is None:
            sel = dMatrix != raster_info_list[i].noData
            mMatrix[sel] = dMatrix[sel]
            writeMatrix = mMatrix
        else:
            writeMatrix = dMatrix

        # Add to output
        outputBand.WriteArray(writeMatrix, idx.xStart, idx.yStart)
        outputBand.FlushCache()

        # update metaData, maybe
        if updateMeta:
            meta.update(raster_info_list[i].meta)

    if updateMeta:
        outputDS.SetMetadata(meta)

    # Write final raster
    outputDS.FlushCache()
    outputBand.ComputeRasterMinMax(0)
    outputBand.ComputeBandStats(0)

    return outputDS

compare_geoms

compare_geoms(geoms_1, geoms_2)

Compare two lists of geometries and return a list of booleans indicating whether each pair of geometries are equal. The order of the lists is important, as the first geometry in the first list will be compared to the first geometry in the second list, and so on.

Returns:

  • list

    A list of booleans indicating whether each pair of geometries are equal.

Source code in geokit/core/util.py
def compare_geoms(geoms_1, geoms_2):
    """Compare two lists of geometries and return a list of booleans indicating whether each pair of geometries are equal.
    The order of the lists is important, as the first geometry in the first list will be compared to the first geometry in the second list, and so on.

    Returns
    -------
    list
        A list of booleans indicating whether each pair of geometries are equal.
    """
    equal = map(lambda g1, g2: g1.Equals(g2), geoms_1, geoms_2)

    return list(equal)

contours

contours(
    source: load_raster_input,
    contourEdges: list[float] | None,
    polygonize: bool = True,
    unpack: bool = True,
    raster_band_index: int = 1,
    **kwargs,
) -> DataFrame

Create contour geometries at specified edges for the given raster data.

Notes

This function is similar to geokit.geom.polygonizeMatrix, although it only operates on the user-specified edges AND applies the 'Marching Squares' algorithm

See the gdal function "GDALContourGenerateEx" for more information on the specifics of this algorithm

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource to operate on

  • contourEdges

    (list[float] | None) –

    The edges to search for within the raster dataset * This parameter can be set as "None", in which case an additional argument should be given to specify how the edges should be determined - See the documentation of "GDALContourGenerateEx" - Ex. "LEVEL_INTERVAL=10", contourEdges=None

  • polygons

    (bool) –

    If true, contours are returned as polygons instead of linstrings

  • unpack

    (bool, default: True ) –

    If True, Multipolygon/MultiLinestring objects are decomposed

  • raster_band_index

    (int, default: 1 ) –

    The index of the raster which should be loaded from the raster band.

  • **kwargs

    • All keyword arguments are passed on to a call to gdal.ContourGenerateEx
    • They are used to construct the 'options' parameter
    • Example keys include: LEVEL_INTERVAL, LEVEL_BASE, LEVEL_EXP_BASE, NODATA
    • Do not use the key "ID_FIELD", since this is employed already

Returns:

  • DataFrame
  • * The column 'geom' corresponds to generated geometry objects
  • * The columns 'ID' corresponds to the associated contour edge for each object
Source code in geokit/core/raster.py
def contours(
    source: load_raster_input,
    contourEdges: list[float] | None,
    polygonize: bool = True,
    unpack: bool = True,
    raster_band_index: int = 1,
    **kwargs,
) -> pd.DataFrame:
    """Create contour geometries at specified edges for the given raster data.

    Notes
    -----
    This function is similar to geokit.geom.polygonizeMatrix, although it only
    operates on the user-specified edges AND applies the 'Marching Squares'
    algorithm

    See the gdal function "GDALContourGenerateEx" for more information on the
    specifics of this algorithm

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource to operate on

    contourEdges : list[float] | None
        The edges to search for within the raster dataset
          * This parameter can be set as "None", in which case an additional
            argument should be given to specify how the edges should be determined
            - See the documentation of "GDALContourGenerateEx"
            - Ex. "LEVEL_INTERVAL=10", contourEdges=None

    polygons : bool
        If true, contours are returned as polygons instead of linstrings

    unpack : bool
        If True, Multipolygon/MultiLinestring objects are decomposed

    raster_band_index : int
        The index of the raster which should be loaded from the raster band.

    **kwargs:
        * All keyword arguments are passed on to a call to gdal.ContourGenerateEx
        * They are used to construct the 'options' parameter
        * Example keys include: LEVEL_INTERVAL, LEVEL_BASE, LEVEL_EXP_BASE, NODATA
        * Do not use the key "ID_FIELD", since this is employed already

    Returns
    -------
    pandas.DataFrame

    * The column 'geom' corresponds to generated geometry objects
    * The columns 'ID' corresponds to the associated contour edge for each object
    """
    warnings.warn(
        message="The current behavior of geokits's contours function is deprecated. GDAL has changed"
        " how contours are drawn close to minimum and maximum values, and will discontinue"
        " the current behavior in GDAL 3.11 or 3.12. Geokit will also drop the current"
        " behavior with the next GDAL update. For more information, please see the"
        " following discussion: https://github.com/OSGeo/gdal/issues/12938.",
        category=DeprecationWarning,
    )
    # Open raster
    raster = loadRaster(source)
    band = raster.GetRasterBand(raster_band_index)

    rasterSRS = SRS.loadSRS(raster.GetProjectionRef())

    # Make temporary vector
    driver: Driver = gdal.GetDriverByName("Memory")
    source: gdal.Dataset = driver.Create("", 0, 0, 0, gdal.GDT_Unknown)

    layer: ogr.Layer = source.CreateLayer("", rasterSRS, ogr.wkbPolygon if polygonize else ogr.wkbLineString)
    field = ogr.FieldDefn("DN", ogr.OFTInteger)
    layer.CreateField(field)

    # Setup contour function
    args = ["{}={}".format(a, b) for a, b in kwargs.items()]
    args.append("ID_FIELD=DN")
    if polygonize:
        args.append("POLYGONIZE=YES")
    if contourEdges is not None:
        opt = "FIXED_LEVELS="
        for edge in contourEdges:
            opt += str(edge) + ","
        args.append(opt[:-1])

    result = gdal.ContourGenerateEx(band, layer, options=args)
    if not result == gdal.CE_None:
        raise GeoKitRasterError("Failed to compute raster contours")
    layer.CommitTransaction()

    IDs = []
    geoms = []
    iterator_n = 0
    for ftrid in range(layer.GetFeatureCount()):
        ftr: ogr.Feature = layer.GetFeature(ftrid)
        geom = ftr.GetGeometryRef()
        value = ftr.GetField(0)
        print(iterator_n)
        iterator_n = iterator_n + 1
        if unpack:
            for gi in range(geom.GetGeometryCount()):
                geoms.append(geom.GetGeometryRef(gi).Clone())
                IDs.append(value)
        else:
            geoms.append(geom.Clone())
            IDs.append(value)

    countour_data_frame = pd.DataFrame(dict(geom=geoms, ID=IDs))

    # return geoms
    return countour_data_frame

convertGeoJson

convertGeoJson(geojson, srs=3857)

Make a geometry from a well known text (WKT) string.

TODO: UPDATE!!!

Parameters:

  • wkt

    (str) –

    The WKT string to convert

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: 3857 ) –

    The srs of the geometry to create

Source code in geokit/core/geom.py
def convertGeoJson(geojson, srs=3857):
    """Make a geometry from a well known text (WKT) string.

    TODO: UPDATE!!!

    Parameters
    ----------
    wkt : str
        The WKT string to convert

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometry to create
    """
    geom = ogr.CreateGeometryFromJson(geojson)  # Create new geometry from string
    if geom is None:  # test for success
        raise GeoKitGeomError("Failed to create geometry")
    if srs:
        geom.AssignSpatialReference(SRS.loadSRS(srs))  # Assign the given srs
    return geom

convertWKT

convertWKT(wkt, srs=None)

Make a geometry from a well known text (WKT) string.

Parameters:

  • wkt

    (str) –

    The WKT string to convert

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the geometry to create

Source code in geokit/core/geom.py
def convertWKT(wkt, srs=None):
    """Make a geometry from a well known text (WKT) string.

    Parameters
    ----------
    wkt : str
        The WKT string to convert

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometry to create
    """
    geom = ogr.CreateGeometryFromWkt(wkt)  # Create new geometry from string
    if geom is None:  # test for success
        raise GeoKitGeomError("Failed to create geometry")
    if srs:
        geom.AssignSpatialReference(SRS.loadSRS(srs))  # Assign the given srs
    return geom

countFeatures

countFeatures(source, geom=None, where=None)

Returns the number of features found in the given source and within a given geometry and/or where-statement.

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector datasource to read from

  • geom

    (ogr.Geometry; optional, default: None ) –

    The geometry to search within * All features are extracted which touch this geometry

  • where

    (str; optional, default: None ) –

    An SQL-like where statement to apply to the source * Feature attribute name do not need quotes * String values should be wrapped in 'single quotes' Example: If the source vector has a string attribute called "ISO" and a integer attribute called "POP", you could use....

    where = "ISO='DEU' AND POP>1000"
    

Returns:

  • int
Source code in geokit/core/vector.py
def countFeatures(source, geom=None, where=None):
    """Returns the number of features found in the given source and within a
    given geometry and/or where-statement.

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector datasource to read from

    geom : ogr.Geometry; optional
        The geometry to search within
        * All features are extracted which touch this geometry

    where : str; optional
        An SQL-like where statement to apply to the source
        * Feature attribute name do not need quotes
        * String values should be wrapped in 'single quotes'
        Example: If the source vector has a string attribute called "ISO" and
                 a integer attribute called "POP", you could use....

            where = "ISO='DEU' AND POP>1000"

    Returns
    -------
    int
    """
    ds = loadVector(source)
    layer = ds.GetLayer()
    filterLayer(layer, geom, where)
    return layer.GetFeatureCount()

createDataFrameFromGeoDataFrame

createDataFrameFromGeoDataFrame(
    gdf: GeoDataFrame,
) -> DataFrame

Creates a geokit-style dataframe from a geopandas geodataframe.

Parameters:

  • gdf

    (DataFrame) –

    geopandas-style pd.DataFrame, need a 'geometry' column.

Returns:

  • DataFrame

    Same as the previous, just as an geokit-style dataframe with 'geom' column with osgeo.ogr.Geometry objects.

Source code in geokit/core/vector.py
def createDataFrameFromGeoDataFrame(gdf: gpd.GeoDataFrame) -> pd.DataFrame:
    """Creates a geokit-style dataframe from a geopandas geodataframe.

    Parameters
    ----------
    gdf : pd.DataFrame
        geopandas-style pd.DataFrame, need a 'geometry' column.

    Returns
    -------
    pd.DataFrame
        Same as the previous, just as an geokit-style dataframe with 'geom'
        column with osgeo.ogr.Geometry objects.
    """
    assert isinstance(gdf, pd.DataFrame)
    assert "geometry" in gdf.columns

    # # import the required external packages - these are not part of the requirements.yml and are possibly not installed
    # try:
    #     import geopandas as gpd
    # except:
    #     raise ImportError(
    #         "'geopandas' is required for geokit.vector.createGeoDataFrame() but is not installed in the current environment."
    #     )

    # get the CRS/SRS of the gdf
    crs_wkt = gdf.crs.to_wkt()
    srs = SRS.loadSRS(crs_wkt)
    # create WKT geometry and then convert to osgeo.ogr.Geometry
    gdf["geom"] = gdf["geometry"].apply(lambda x: GEOM.convertWKT(x.wkt, srs=srs))

    # remove geometry column and return geokit df
    df = gdf.drop(columns="geometry")
    return df

createGeoDataFrame

createGeoDataFrame(dfGeokit: DataFrame) -> GeoDataFrame

Creates a gdf from an Reskit shape pd.DataFrame.

Parameters:

  • dfGeokit

    (DataFrame) –

    Reskit shape pd.DataFrame, need a 'geom' column.

Returns:

  • GeoDataFrame

    Same as the previous, just as an GeodataFrame

Source code in geokit/core/vector.py
def createGeoDataFrame(dfGeokit: pd.DataFrame) -> gpd.GeoDataFrame:
    """Creates a gdf from an Reskit shape pd.DataFrame.

    Parameters
    ----------
    dfGeokit : pd.DataFrame
        Reskit shape pd.DataFrame, need a 'geom' column.

    Returns
    -------
    gpd.GeoDataFrame
        Same as the previous, just as an GeodataFrame
    """
    assert isinstance(dfGeokit, pd.DataFrame)
    assert "geom" in dfGeokit.columns

    # import the required external packages - these are not part of the requirements.yml and are possibly not installed
    try:
        import shapely
    except:
        raise ImportError(
            "'shapely' is required for geokit.vector.createGeoDataFrame() but is not installed in the current environment."
        )

    try:
        import geopandas as gpd
    except:
        raise ImportError(
            "'geopandas' is required for geokit.vector.createGeoDataFrame() but is not installed in the current environment."
        )

    # get values
    values = {}
    for col in dfGeokit.columns:
        # geoms need to be converted to shapely
        if col == "geom":
            values["geometry"] = [shapely.wkt.loads(g.ExportToWkt()) for g in dfGeokit.geom]  # this takes some time :/
        # other values are just stored as they are
        else:
            values[col] = list(dfGeokit[col])

    # get srs as Well known text
    crs = dfGeokit.geom.iloc[0].GetSpatialReference().ExportToWkt()

    # create gdf and set index
    gdf = gpd.GeoDataFrame(values, crs=crs)
    gdf = gdf.set_index(dfGeokit.index, drop=True)

    # over and out!
    return gdf

createGeoJson

createGeoJson(
    geoms, output=None, srs=4326, topo=False, fill=""
)

Convert a set of geometries to a geoJSON object.

Source code in geokit/core/vector.py
def createGeoJson(geoms, output=None, srs=4326, topo=False, fill=""):
    """Convert a set of geometries to a geoJSON object."""
    if srs:
        srs = SRS.loadSRS(srs)

    # arrange geom, index, and data
    if isinstance(geoms, ogr.Geometry):  # geoms is a single geometry
        finalGeoms = [
            geoms,
        ]
        data = None
        index = [
            0,
        ]

    elif isinstance(geoms, pd.Series):
        index = geoms.index
        finalGeoms = geoms.values
        data = None

    elif isinstance(geoms, pd.DataFrame):
        index = geoms.index
        finalGeoms = geoms.geom.values
        data = geoms.loc[:, geoms.columns != "geom"]
        data["_index"] = index
    else:
        finalGeoms = list(geoms)
        data = None
        index = list(range(len(finalGeoms)))

    if len(finalGeoms) == 0:
        raise GeoKitVectorError("Empty geometry list given")

    # Transform?
    if not srs is None:
        finalGeoms = GEOM.transform(finalGeoms, toSRS=srs)

    # Make JSON object
    from io import BytesIO

    if not output is None and not isinstance(output, str):
        if not output.writable():
            raise GeoKitVectorError("Output object is not writable")

        if topo:
            fo = BytesIO()
        else:
            fo = output
    elif isinstance(output, str) and not topo:
        fo = open(output, "wb")
    else:
        fo = BytesIO()

    fo.write(bytes('{"type":"FeatureCollection","features":[', encoding="utf-8"))

    for j, i, g in zip(range(len(index)), index, finalGeoms):
        fo.write(bytes('%s{"type":"Feature",' % ("" if j == 0 else ","), encoding="utf-8"))
        if data is None:
            fo.write(bytes('"properties":{"_index":%s},' % str(i), encoding="utf-8"))
        else:
            fo.write(
                bytes(
                    '"properties":%s,' % data.loc[i].fillna(fill).to_json(),
                    encoding="utf-8",
                )
            )

        fo.write(bytes('"geometry":%s}' % g.ExportToJson(), encoding="utf-8"))
        # fo.write(bytes('"geometry": {"type": "Point","coordinates": [125.6, 10.1] }}', encoding='utf-8'))
    fo.write(bytes("]}", encoding="utf-8"))
    fo.flush()

    # Put in the right format
    if topo:
        from io import TextIOWrapper

        from topojson import conversion

        fo.seek(0)
        topo = conversion.convert(TextIOWrapper(fo), object_name="primary")  # automatically closes fo
        topo = str(topo).replace("'", '"')

    # Done!
    if output is None:
        if topo:
            return topo
        else:
            fo.seek(0)
            geojson = fo.read()
            fo.close()
            return geojson.decode("utf-8")

    elif isinstance(output, str):
        if topo:
            with open(output, "w") as fo:
                fo.write(topo)
        else:
            pass  # we already wrote to the file!
        return output

    else:
        if topo:
            output.write(bytes(topo, encoding="utf-8"))
        else:
            pass  # We already wrote to the file!
        return None

createRaster

createRaster(
    bounds: tuple[numeric, numeric, numeric, numeric],
    output: None | str | Path = None,
    pixelWidth: numeric = 100,
    pixelHeight: numeric = 100,
    dtype: None | geokit_c_data_types_literal = None,
    srs: srs_input | None = None,
    compress: bool = True,
    noData: numeric | None = None,
    overwrite: bool = True,
    fill: numeric | None = None,
    data: ndarray | None = None,
    meta: dict | None = None,
    scale: numeric | None = 1,
    offset: numeric = 0,
    creationOptions: dict = dict(),
    raster_band_index: int = 1,
) -> Dataset | str

Create a raster file.

NOTE:

Raster datasets are always written in the 'yAtTop' orientation. Meaning that the first row of data values (either written to or read from the dataset) will refer to the TOP of the defined boundary, and will then move downward from there

If a data matrix is given, and a negative pixelWidth is defined, the data will be flipped automatically

Parameters:

  • bounds

    ((xMin, yMin, xMax, yMax) or Extent) –

    The geographic extents spanned by the raster

  • pixelWidth

    (numeric, default: 100 ) –

    The pixel width of the raster in units of the input srs * The keyword 'dx' can be used as well and will override anything given assigned to 'pixelWidth'

  • pixelHeight

    (numeric, default: 100 ) –

    The pixel height of the raster in units of the input srs * The keyword 'dy' can be used as well and will override anything given assigned to 'pixelHeight'

  • output

    ((str, Path, None), default: None ) –

    A path to an output file * If output is None, the raster will be created in memory and a dataset handle will be returned * If output is given, the raster will be written to disk and nothing will be returned

  • dtype

    ((geokit_c_data_types_literal, none), default: None ) –

    The datatype of the represented by the created raster's band * Options are: Byte, Int16, Int32, Int64, Float32, Float64 * If dtype is None and data is None, the assumed datatype is a 'Byte' * If dtype is None and data is not None, the datatype will be inferred from the given data

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the point to create * If not given, no srs will be assigned to the created raster. Please be aware that some operations may not work correctly if no SRS is given. * If 'bounds' is an Extent object, the bounds' internal srs will override this input

  • compress

    (bool, default: True ) –

    A flag instructing the output raster to use a compression algorithm * only useful if 'output' has been defined * "DEFLATE" used for Linux/Mac, "LZW" used for Windows

  • noData

    (numeric; optional, default: None ) –

    Specifies which value should be considered as 'no data' in the created raster * Must be the same datatype as the 'dtype' input (or that which is derived)

  • fill

    (numeric; optional, default: None ) –

    The initial value given to all pixels in the created raster band - numeric * Must be the same datatype as the 'dtype' input (or that which is derived)

  • overwrite

    (bool, default: True ) –

    A flag to overwrite a pre-existing output file * If set to False and an 'output' is specified which already exists, an error will be raised

  • data

    (matrix_like, default: None ) –

    A 2D matrix to write into the resulting raster * array dimensions must fit raster dimensions as calculated by the bounds and the pixel resolution

  • meta

    ((dictionary, None), default: None ) –

    of key values pairs that is stored in the raster as meta data. Is written to the raster using the

  • scale

    (numeric; optional, default: 1 ) –

    The scaling value given to apply to all values - numeric * Must be the same datatype as the 'dtype' input (or that which is derived)

  • offset

    (numeric; optional, default: 0 ) –

    The offset value given to apply to all values - numeric * Must be the same datatype as the 'dtype' input (or that which is derived)

  • raster_band_index

    (int, default: 1 ) –

    Determines which band is written to in the output raster dataset.

Returns:

  • * If 'output' is None: gdal.Dataset
  • * If 'output' is a string: The path to the output is returned (for easy opening).

    It has to be saved as geotiff with .tif suffix.

Source code in geokit/core/raster.py
def createRaster(
    bounds: tuple[numeric, numeric, numeric, numeric],
    output: None | str | pathlib.Path = None,
    pixelWidth: numeric = 100,
    pixelHeight: numeric = 100,
    dtype: None | geokit_c_data_types_literal = None,
    srs: srs_input | None = None,
    compress: bool = True,
    noData: numeric | None = None,
    overwrite: bool = True,
    fill: numeric | None = None,
    data: np.ndarray | None = None,
    meta: dict | None = None,
    scale: numeric | None = 1,
    offset: numeric = 0,
    creationOptions: dict = dict(),
    raster_band_index: int = 1,
) -> gdal.Dataset | str:
    """Create a raster file.

    NOTE:
    -----
    Raster datasets are always written in the 'yAtTop' orientation. Meaning that
    the first row of data values (either written to or read from the dataset) will
    refer to the TOP of the defined boundary, and will then move downward from
    there

    If a data matrix is given, and a negative pixelWidth is defined, the data
    will be flipped automatically

    Parameters
    ----------
    bounds : (xMin, yMin, xMax, yMax) or Extent
        The geographic extents spanned by the raster

    pixelWidth : numeric
        The pixel width of the raster in units of the input srs
        * The keyword 'dx' can be used as well and will override anything given
        assigned to 'pixelWidth'

    pixelHeight : numeric
        The pixel height of the raster in units of the input srs
        * The keyword 'dy' can be used as well and will override anything given
          assigned to 'pixelHeight'

    output : str, pathlib.Path, None
        A path to an output file
        * If output is None, the raster will be created in memory and a dataset
          handle will be returned
        * If output is given, the raster will be written to disk and nothing will
          be returned

    dtype : geokit_c_data_types_literal, none
        The datatype of the represented by the created raster's band
        * Options are: Byte, Int16, Int32, Int64, Float32, Float64
        * If dtype is None and data is None, the assumed datatype is a 'Byte'
        * If dtype is None and data is not None, the datatype will be inferred
          from the given data

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the point to create
          * If not given, no srs will be assigned to the created raster. Please be
            aware that some operations may not work correctly if no SRS is given.
        * If 'bounds' is an Extent object, the bounds' internal srs will override
          this input

    compress : bool
        A flag instructing the output raster to use a compression algorithm
        * only useful if 'output' has been defined
        * "DEFLATE" used for Linux/Mac, "LZW" used for Windows

    noData : numeric; optional
        Specifies which value should be considered as 'no data' in the created
        raster
        * Must be the same datatype as the 'dtype' input (or that which is derived)

    fill : numeric; optional
        The initial value given to all pixels in the created raster band
        - numeric
        * Must be the same datatype as the 'dtype' input (or that which is derived)

    overwrite : bool
        A flag to overwrite a pre-existing output file
        * If set to False and an 'output' is specified which already exists,
          an error will be raised

    data : matrix_like
        A 2D matrix to write into the resulting raster
        * array dimensions must fit raster dimensions as calculated by the bounds
          and the pixel resolution
    meta : dictionary, None
        of key values pairs that is stored in the raster as meta data. Is written
        to the raster using the


    scale : numeric; optional
        The scaling value given to apply to all values
        - numeric
        * Must be the same datatype as the 'dtype' input (or that which is derived)

    offset : numeric; optional
        The offset value given to apply to all values
        - numeric
        * Must be the same datatype as the 'dtype' input (or that which is derived)

    raster_band_index: int, defaults to 1
        Determines which band is written to in the output raster dataset.

    Returns
    -------
    * If 'output' is None: gdal.Dataset
    * If 'output' is a string: The path to the output is returned (for easy opening).
                                It has to be saved as geotiff with .tif suffix.
    """
    # Check for existing file

    if output is not None:
        output = str(output)
        output_path = pathlib.Path(output)
        if output_path.exists():
            if overwrite is True:
                output_path.unlink()
                output_path_with_aux_extension = output_path.with_suffix(".aux.xml")
                if output_path_with_aux_extension.exists():
                    output_path_with_aux_extension.unlink()
            else:
                raise GeoKitRasterError("Output file already exists: %s" % output)

        # check if the directory exists

        elif not output_path.parent.is_dir():
            raise FileNotFoundError(f"Output directory does not exist: {os.path.dirname(output)}")

        # check if writeable:
        if not os.access(os.path.dirname(output), os.W_OK):
            print(os.path.dirname(output))
            raise PermissionError(f"Writing permission error for path: {os.path.dirname(output)}")

    # Ensure bounds is okay
    # bounds = UTIL.fitBoundsTo(bounds, pixelWidth, pixelHeight)

    # Make a raster dataset and pull the band/maskBand objects
    x_min = bounds[0]
    y_min = bounds[1]
    x_max = bounds[2]
    y_max = bounds[3]  # Always use the "Y-at-Top" orientation

    cols = int(round((x_max - x_min) / pixelWidth))
    rows = int(round((y_max - y_min) / abs(pixelHeight)))

    list_of_numbers = []
    minimum_gdal_type_list = []
    if isinstance(dtype, str):
        minimum_gdal_type_list.append(dtype)
    if isinstance(noData, numeric):
        list_of_numbers.append(noData)

    if isinstance(fill, numeric):
        list_of_numbers.append(fill)
    if isinstance(data, np.ndarray):
        numpy_data_type = str(data.dtype)
        minimum_gdal_type_list.append(numpy_data_type)
        list_of_numbers.append(data.min())
        list_of_numbers.append(data.max())
    elif data is None:
        pass
    else:
        raise GeoKitRasterError("Data must be given as a numpy ndarray or None")

    data_type_constant = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_constant(
        list_of_numbers=list_of_numbers, minimum_gdal_type_list=minimum_gdal_type_list
    )
    # # Get DataType
    # if dtype is not None:  # a dtype was given, use it!
    #     dtype = gdalType(dtype)
    # elif data is not None:  # a data matrix was give, use it's dtype! (assume a numpy array or derivative)
    #     dtype = gdalType(data.dtype)
    # else:  # Otherwise, just assume we want a Byte
    #     dtype = "GDT_Byte"

    # Open the driver
    opts = OrderedDict()
    if compress and output is not None:
        opts["COMPRESS"] = COMPRESSION_OPTION_STR
    if creationOptions is not None:
        opts.update(creationOptions)
    opts = ["{}={}".format(k, v) for k, v in opts.items()]

    if output is None:
        driver: gdal.Driver = gdal.GetDriverByName("Mem")  # create a raster in memory
        raster: gdal.Dataset = driver.Create("", cols, rows, 1, data_type_constant, opts)
    else:
        driver: gdal.Driver = gdal.GetDriverByName("GTiff")  # Create a raster in storage
        raster: gdal.Dataset = driver.Create(output, cols, rows, 1, data_type_constant, opts)

    if raster is None:
        raise GeoKitRasterError("Failed to create raster")

    # Do the rest in a "try" statement so that a failure won't bind the source
    try:
        raster.SetGeoTransform((x_min, abs(pixelWidth), 0, y_max, 0, -1 * abs(pixelHeight)))

        # Set the SRS
        if srs is not None:
            rasterSRS = SRS.loadSRS(srs)
            raster.SetProjection(rasterSRS.ExportToWkt())
        else:
            warnings.warn(
                message="No srs given when creating raster. Please be aware that some operations may not work correctly.",
                category=UserWarning,
            )

        # Fill the raster will zeros, null values, or initial values (if given)
        band: gdal.Band = raster.GetRasterBand(raster_band_index)
        if scale is not None:
            band.SetScale(scale)
        if offset is not None:
            band.SetOffset(offset)

        if noData is not None:
            band.SetNoDataValue(noData)
            if fill is None and data is None:
                band.Fill(noData)

        if data is None:
            if fill is None:
                band.Fill(0)
            else:
                band.Fill(fill)
        else:
            # make sure dimension size is good
            if not (data.shape[0] == rows and data.shape[1] == cols):
                raise GeoKitRasterError(
                    "Raster dimensions and input data dimensions do not match.The data has rows="
                    + str(data.shape[0])
                    + " and columns="
                    + str(data.shape[1])
                    + ". The raster specification expects rows="
                    + str(rows)
                    + " and columns="
                    + str(cols)
                )

            # See if data needs flipping
            if pixelHeight < 0:
                data = data[::-1, :]

            # Write it!
            band.WriteArray(data)
            band.FlushCache()

            band.ComputeRasterMinMax(0)
            band.ComputeBandStats(0)

        # Write MetaData, maybe
        if meta is not None:
            for k, v in meta.items():
                raster.SetMetadataItem(k, v)

        # writes the raster to the hard drive
        raster.FlushCache()
        # Return raster if in memory
        if output is None:
            return raster

        # Done
        return output

    # Handle the fail case
    except Exception as e:
        raster = None
        raise e

createRasterLike

createRasterLike(
    source: load_raster_input | RasterInfo,
    copyMetadata: bool = True,
    metadata: dict | None = None,
    data: None | ndarray = None,
    dtype: None | geokit_c_data_types_literal = None,
    **kwargs,
)

Create a raster described by the given raster info (as returned from a call to rasterInfo() ).

  • This copies all characteristics of the given raster, including: bounds, pixelWidth, pixelHeight, dtype, srs, noData, and meta.
  • Any keyword argument which is given will override values found in the source
Source code in geokit/core/raster.py
def createRasterLike(
    source: load_raster_input | RasterInfo,
    copyMetadata: bool = True,
    metadata: dict | None = None,
    data: None | np.ndarray = None,
    dtype: None | geokit_c_data_types_literal = None,
    **kwargs,
):
    """Create a raster described by the given raster info (as returned from a
    call to rasterInfo() ).

    * This copies all characteristics of the given raster, including: bounds,
      pixelWidth, pixelHeight, dtype, srs, noData, and meta.
    * Any keyword argument which is given will override values found in the
      source
    """
    if UTIL.isRaster(source):
        raster_info = rasterInfo(source)
    elif isinstance(source, RasterInfo):
        raster_info = source
    else:
        raise GeoKitRasterError("Could not understand source")

    if copyMetadata and metadata is not None:
        raise GeoKitRasterError("If metadata is given, copyMetadata cannot be True!")

    bounds = kwargs.pop("bounds", raster_info.bounds)
    pixelWidth = kwargs.pop("pixelWidth", raster_info.pixelWidth)
    pixelHeight = kwargs.pop("pixelHeight", raster_info.pixelHeight)
    srs = kwargs.pop("srs", raster_info.srs)
    noData = kwargs.pop("noData", raster_info.noData)
    data_type_as_string = kwargs.pop("data_type_as_string", dtype)
    if copyMetadata:
        meta = kwargs.pop("meta", raster_info.meta)
    else:
        meta = metadata

    return createRaster(
        bounds=bounds,
        pixelWidth=pixelWidth,
        pixelHeight=pixelHeight,
        srs=srs,
        noData=noData,
        meta=meta,
        dtype=data_type_as_string,
        data=data,
        **kwargs,
    )

createVector

createVector(
    geoms,
    output: str | None = None,
    srs=None,
    driverName: str = "ESRI Shapefile",
    layerName: str = "default",
    fieldVals=None,
    fieldDef=None,
    checkAllGeoms=False,
    overwrite: bool = True,
)

Create a vector on disk from geometries or a DataFrame with 'geom' column.

Parameters:

  • geoms

    (Geometry or [Geometry] or DataFrane) –

    The geometries to write into the vector file * If a DataFrame is given, it must have a column called 'geom' * All geometries must share the same type (point, line, polygon, etc...) * All geometries must share the same SRS * If geometry SRS differs from the 'srs' input, then all geometries will be projected to the input srs

  • output

    (str; optional, default: None ) –

    A path on disk to create the output vector * If output is None, the vector dataset will be created in memory * Assumed to be of "ESRI Shapefile" format * Will create a number of files with different extensions

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the vector to create * If not given, the geometries' inherent srs is used * If srs does not match the inherent srs, all geometries will be transformed

  • driverName

    (str; optional, default: 'ESRI Shapefile' ) –

    The name of the driver to use when creating the vector. Currently supported options are: - ESRI Shapefile - GPKG

    For a list of all supported vector drivers by OGR, see: https://gdal.org/drivers/vector/index.html

  • layerName

    (str; optional, default: 'default' ) –

    The name of the layer to create within the vector. (Only applicable for GeoPackages) * If the layer already exists, it will be overwritten

  • fieldVals

    (dict of lists or pandas.DataFrame; optional, default: None ) –

    Explicit attribute values to assign to each geometry * If a dict is given, must be a dictionary of the attribute names and associated lists of the attribute values * If a DataFrame is given, the field names are taken from column names and attribute values from the corresponding column data * The order of each column/list will correspond to the order geometries are written into the dataset * The length of each column/list MUST match the number of geometries * All values in a single column/list must share the same type - Options are int, float, or str

  • fieldDef

    (dict; optional, default: None ) –

    A dictionary specifying the datatype of each attribute when written into the final dataset * Options are defined from ogr.OFT[...] - ex. Integer, Real, String * The ogrType() function can be used to map typical python and numpy types to appropriate ogr types

  • checkAllGeoms

    (bool, default: False ) –

    If True, all geoms will be asserted in object type and exact srs. Else, only the first geom in the geom column/iterable will be checked for performance reasons. By default False.

  • overwrite

    (bool; optional, default: True ) –

    Determines whether the preexisting files should be overwritten * Only used when output is not None

Returns:

  • * If 'output' is None: gdal.Dataset
  • * If 'output' is given: None
Source code in geokit/core/vector.py
def createVector(
    # geoms: ogr.Geometry | list[ogr.Geometry | gdal.Dataset | str] | pd.DataFrame | np.ndarray | str | gdal.Dataset,
    geoms,
    output: str | None = None,
    srs=None,
    driverName: str = "ESRI Shapefile",
    layerName: str = "default",
    fieldVals=None,
    fieldDef=None,
    checkAllGeoms=False,
    overwrite: bool = True,
):
    """
    Create a vector on disk from geometries or a DataFrame with 'geom' column.

    Parameters
    ----------
    geoms : ogr.Geometry or [ogr.Geometry, ] or pandas.DataFrane
        The geometries to write into the vector file
        * If a DataFrame is given, it must have a column called 'geom'
        * All geometries must share the same type (point, line, polygon, etc...)
        * All geometries must share the same SRS
        * If geometry SRS differs from the 'srs' input, then all geometries will
          be projected to the input srs

    output : str; optional
        A path on disk to create the output vector
        * If output is None, the vector dataset will be created in memory
        * Assumed to be of "ESRI Shapefile" format
        * Will create a number of files with different extensions

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the vector to create
          * If not given, the geometries' inherent srs is used
          * If srs does not match the inherent srs, all geometries will be
            transformed

    driverName : str; optional
        The name of the driver to use when creating the vector.
        Currently supported options are:
            - ESRI Shapefile
            - GPKG

        For a list of all supported vector drivers by OGR, see: https://gdal.org/drivers/vector/index.html

    layerName : str; optional
        The name of the layer to create within the vector. (Only applicable for GeoPackages)
        * If the layer already exists, it will be overwritten

    fieldVals : dict of lists or pandas.DataFrame; optional
        Explicit attribute values to assign to each geometry
        * If a dict is given, must be a dictionary of the attribute names and
          associated lists of the attribute values
        * If a DataFrame is given, the field names are taken from column names
          and attribute values from the corresponding column data
        * The order of each column/list will correspond to the order geometries
          are written into the dataset
        * The length of each column/list MUST match the number of geometries
        * All values in a single column/list must share the same type
            - Options are int, float, or str

    fieldDef : dict; optional
        A dictionary specifying the datatype of each attribute when written into
        the final dataset
        * Options are defined from ogr.OFT[...]
          - ex. Integer, Real, String
        * The ogrType() function can be used to map typical python and numpy types
          to appropriate ogr types

    checkAllGeoms : bool, optional
        If True, all geoms will be asserted in object type and exact srs. Else, only
        the first geom in the geom column/iterable will be checked for performance reasons.
        By default False.

    overwrite : bool; optional
        Determines whether the preexisting files should be overwritten
        * Only used when output is not None

    Returns
    -------
    * If 'output' is None: gdal.Dataset
    * If 'output' is given: None
    """
    if srs:
        srs = SRS.loadSRS(srs)

    # make geom or wkt list into a list of ogr.Geometry objects
    finalGeoms = []
    geomIndex = None
    if isinstance(geoms, str) or isinstance(geoms, ogr.Geometry):  # geoms is a single geometry
        geoms = [
            geoms,
        ]
    elif isinstance(geoms, pd.Series):
        geomIndex = geoms.index
        geoms = geoms.values
    elif isinstance(geoms, pd.DataFrame):
        if not fieldVals is None:
            raise GeoKitVectorError("fieldVals must be None when geoms input is a DataFrame")

        fieldVals = geoms.copy()
        geoms = geoms.geom.values
        fieldVals.drop("geom", inplace=True, axis=1)

    if len(geoms) == 0:
        raise GeoKitVectorError("Empty geometry list given")

    # Test if the first geometry is an ogr-Geometry type
    if isinstance(geoms[0], ogr.Geometry):
        #  (Assume all geometries in the array have the same type)
        geomSRS = geoms[0].GetSpatialReference()
        if checkAllGeoms:
            # check if all other geoms have the same SRS
            assert all([geomSRS.IsSame(g.GetSpatialReference()) for g in geoms]), (
                f"Not all geoms have the same SRS, srs of first geom: {geomSRS}"
            )
        # Set up some variables for the upcoming loop
        doTransform = False
        setSRS = False

        if srs and geomSRS and not srs.IsSame(geomSRS):  # Test if a transformation is needed
            trx = osr.CoordinateTransformation(geomSRS, srs)
            doTransform = True
        # Test if an srs was NOT given, but the incoming geometries have an SRS already
        elif srs is None and geomSRS:
            srs = geomSRS
        # Test if an srs WAS given, but the incoming geometries do NOT have an srs already
        elif srs and geomSRS is None:
            # In which case, assume the geometries are meant to have the given srs
            setSRS = True

        # Create geoms
        for i in range(len(geoms)):
            # clone just in case the geometry is tethered outside of function
            finalGeoms.append(geoms[i].Clone())
            if doTransform:
                finalGeoms[i].Transform(trx)  # Do transform if necessary
            if setSRS:
                finalGeoms[i].AssignSpatialReference(srs)  # Set srs if necessary

    # Otherwise, the geometry array should contain only WKT strings
    elif isinstance(geoms[0], str):
        if srs is None:
            raise ValueError("srs must be given when passing wkt strings")

        # Create geoms
        finalGeoms = [GEOM.convertWKT(wkt, srs) for wkt in geoms]

    else:
        raise ValueError("Geometry inputs must be ogr.Geometry objects or WKT strings")

    geoms = finalGeoms  # Overwrite geoms array with the conditioned finalGeoms

    # Determine geometry types
    types = set()
    for g in geoms:
        # Get a set of all geometry type-names (POINT, POLYGON, etc...)
        types.add(g.GetGeometryName())

    if types.issubset({"POINT", "MULTIPOINT"}):
        geomType = ogr.wkbPoint
    elif types.issubset({"LINESTRING", "MULTILINESTRING"}):
        geomType = ogr.wkbLineString
    elif types.issubset({"POLYGON", "MULTIPOLYGON"}):
        geomType = ogr.wkbPolygon
    else:
        # geomType = ogr.wkbGeometryCollection
        raise RuntimeError("Could not determine output shape's geometry type")

    # Create a driver and datasource
    # driver = gdal.GetDriverByName("ESRI Shapefile")
    # dataSource = driver.Create(output, 0, 0)

    if output is None:
        # Create datasource if no output path is provided
        driver = gdal.GetDriverByName("Memory")

        # Using 'Create' from a Memory driver leads to an error. But creating
        #  a temporary shapefile driver (it does not actually produce a file, I think)
        #  and then using 'CreateCopy' seems to work
        tmp_driver = gdal.GetDriverByName("ESRI Shapefile")
        t = TemporaryDirectory()
        tmp_dataSource = tmp_driver.Create(t.name + "tmp.shp", 0, 0)

        dataSource = driver.CreateCopy("MEMORY", tmp_dataSource)
        t.cleanup()
        del tmp_dataSource, tmp_driver, t
    elif output is not None:
        # Create datasource if output path is provided
        if overwrite is True:
            # Search for directory
            if os.path.dirname(output) == "":  # If no directory is given, assume current directory
                output = os.path.join(os.getcwd(), output)

            elif not os.path.isdir(os.path.dirname(output)):  # If directory does not exist, raise error
                raise FileNotFoundError(f"Directory {os.path.dirname(output)} does not exist")

            # Remove file if it exists
            if os.path.isfile(output):
                os.remove(output)

            driver = ogr.GetDriverByName(driverName)
            dataSource = driver.CreateDataSource(output)

        elif overwrite is False:
            dataSource = ogr.Open(output, 1)
            assert dataSource is not None, f"Could not open {output}"
    # Wrap the whole writing function in a 'try' statement in case it fails
    try:
        # Create the layer
        if output is not None and overwrite is False:
            layerName = layerName
            assert layerName not in listLayers(output), (
                f"Layer name '{layerName}' already exists in {output}. Please Specify a new layer name or set overwrite=True."
            )

        else:
            layerName = layerName

        layer = dataSource.CreateLayer(layerName, srs, geomType)
        assert layer is not None, "Could not create layer!"

        # Setup fieldVals and fieldDef dicts
        if not fieldVals is None:
            # Ensure fieldVals is a dataframe
            if not isinstance(fieldVals, pd.DataFrame):
                # If not, try converting it
                fieldVals = pd.DataFrame(fieldVals)
            if not geomIndex is None:
                fieldVals = fieldVals.loc[geomIndex]

            # check if length is good
            if fieldVals.shape[0] != len(geoms):
                raise GeoKitVectorError("Values table length does not equal geometry count")

            # Ensure fieldDefs exists and is a dict
            if fieldDef is None:  # Try to determine data types
                fieldDef = {}
                for k, v in fieldVals.items():
                    fieldDef[k] = ogrType(v.dtype)

            elif isinstance(fieldDef, dict):  # Assume fieldDef is a dict mapping fields to inappropriate
                #   datatypes. Fix these to ogr indicators!
                for k in fieldVals.columns:
                    try:
                        fieldDef[k] = ogrType(fieldDef[k])
                    except KeyError as e:
                        print('"%s" not in attribute definition table' % k)
                        raise e

            else:  # Assume a single data type was given. Apply to all fields
                _type = ogrType(fieldDef)
                fieldDef = {}
                for k in fieldVals.keys():
                    fieldDef[k] = _type

            # Write field definitions to layer
            for fieldName, dtype in fieldDef.items():
                layer.CreateField(ogr.FieldDefn(str(fieldName), getattr(ogr, dtype)))

            # Ensure list lengths match geom length
            for k, v in fieldVals.items():
                if len(v) != len(geoms):
                    raise RuntimeError("'{}' length does not match geom list".format(k))

        # Create features
        for gi in range(len(geoms)):
            # Create a blank feature
            feature = ogr.Feature(layer.GetLayerDefn())

            # Fill the attributes, if required
            if not fieldVals is None:
                for fieldName, value in fieldVals.items():
                    _type = fieldDef[fieldName]

                    # cast to basic type
                    if _type == "OFTString":
                        val = str(value.iloc[gi])
                    elif _type == "OFTInteger" or _type == "OFTInteger64":
                        val = int(value.iloc[gi])
                    else:
                        val = float(value.iloc[gi])

                    # Write to feature
                    feature.SetField(str(fieldName), val)

            # Set the Geometry
            feature.SetGeometry(geoms[gi])

            # Create the feature
            layer.CreateFeature(feature)
            feature.Destroy()  # Free resources (probably not necessary here)

        # Finish
        if output:
            return output
        else:
            return dataSource

    # Delete the datasource in case it failed
    except Exception as e:
        dataSource = None
        raise e

divideMultipolygonIntoEasternAndWesternPart

divideMultipolygonIntoEasternAndWesternPart(
    geom, side="both"
)

Multipolygons spanning the antimeridian (this includes polygons that are split at the antimeridian, with the Western half shifted Eastwards by 360° longitude) are separated into a part East and West of the antimeridian by identifying the largest longitudinal gap between any of the sub polygons and dividing the sub polys into one Eastern and one Western polygon list which is returned as multipolygons. NOTE: This function only works for already shifted subpolygons with an overall envelope between -180° and +180° longitude.

Parameters:

  • geom

    (Geometry) –

    The geometry to split. Must be a MultiPolygon.

  • side

    (str, default: 'both' ) –

    'left' or 'right' to return the left or right side of the antimeridian 'main' to return the side with the largest area 'both' to return both sides as a tuple (left, right)

Source code in geokit/core/geom.py
def divideMultipolygonIntoEasternAndWesternPart(geom, side="both"):
    """
    Multipolygons spanning the antimeridian (this includes polygons that are
    split at the antimeridian, with the Western half shifted Eastwards by 360°
    longitude) are separated into a part East and West of the antimeridian by
    identifying the largest longitudinal gap between any of the sub polygons and
    dividing the sub polys into one Eastern and one Western polygon list which
    is returned as multipolygons.
    NOTE: This function only works for already shifted subpolygons with an
    overall envelope between -180° and +180° longitude.

    Parameters
    ----------
    geom : ogr.Geometry
        The geometry to split. Must be a MultiPolygon.
    side : str, optional
        'left' or 'right' to return the left or right side of the antimeridian
        'main' to return the side with the largest area
        'both' to return both sides as a tuple (left, right)
    """
    # check inputs
    assert side in (
        "both",
        "left",
        "right",
        "main",
    ), "side must be 'left', 'right', 'main' or 'both'"
    assert isinstance(geom, ogr.Geometry), "geom must be of type osgeo.ogr.Geometry"
    assert geom.GetGeometryName() == "MULTIPOLYGON", "Only MultiPolygon supported"
    assert geom.GetSpatialReference().IsSame(SRS.loadSRS(4326)), "geometry must be in EPSG:4326"
    assert geom.GetEnvelope()[0] >= -180 and geom.GetEnvelope()[1] <= 180, (
        "Envelope must be between -180° and +180° longitude"
    )
    assert geom.GetSpatialReference().IsSame(SRS.loadSRS(4326)), "Only EPSG:4326 lat/lon supported"

    # first extract sub polygons
    sub_polys = [geom.GetGeometryRef(i) for i in range(geom.GetGeometryCount())]

    # get all the bounding boxes
    bounds = []
    for poly in sub_polys:
        env = poly.GetEnvelope()  # (minX, maxX, minY, maxY)
        bounds.append((env[0], env[1], poly))  # store (minX, maxX, polygon)

    # sort them from left to right based on minX
    bounds.sort(key=lambda x: x[0])

    # find the largest gap iteratively
    max_gap = 0
    split_index = 0
    curr_maxs = list()
    for i in range(len(bounds) - 1):
        curr_maxs.append(bounds[i][1])
        curr_max = max(curr_maxs)
        next_min = bounds[i + 1][0]
        gap = next_min - curr_max
        if gap > max_gap:
            # overwrite the max gap so far and save the index when it occurs
            max_gap = gap
            split_index = i

    # split into two sets of geoms - left and right of the gap (do only if necessary for time)
    if side in ["both", "main", "right"]:
        # filter only sub polys below (or equal) split index
        right_polys = [b[2] for i, b in enumerate(bounds) if i <= split_index]
        # merge all right polygons into a single multipolygon and assign the same spatial reference
        right_multi = ogr.Geometry(ogr.wkbMultiPolygon)
        for poly in right_polys:
            right_multi.AddGeometry(poly.Clone())
        right_multi.AssignSpatialReference(geom.GetSpatialReference())
    # same for the left side
    if side in ["both", "main", "left"]:
        # only polys above split index
        left_polys = [b[2] for i, b in enumerate(bounds) if i > split_index]
        left_multi = ogr.Geometry(ogr.wkbMultiPolygon)
        for poly in left_polys:
            left_multi.AddGeometry(poly.Clone())
        left_multi.AssignSpatialReference(geom.GetSpatialReference())

    if side == "left":
        return left_multi
    elif side == "right":
        return right_multi
    elif side == "both":
        # return both left and right
        return left_multi, right_multi
    elif side == "main":
        # return the side with the largest area
        left_area = left_multi.GetArea()
        right_area = right_multi.GetArea()
        if left_area > right_area:
            return left_multi
        else:
            return right_multi
    else:
        raise ValueError("side must be 'left', 'right', 'main' or None")

drawGeoms

drawGeoms(
    geoms: Geometry | list[Geometry] | DataFrame | ndarray,
    srs: srs_input | None = None,
    ax: None | Axes | AxHands = None,
    simplificationFactor: numeric | None = 5000,
    colorBy: str | None = None,
    figsize: tuple[numeric, numeric] = (12, 12),
    xlim: tuple[numeric, numeric] | None = None,
    ylim: tuple[numeric, numeric] | None = None,
    fontsize: int = 16,
    hideAxis: bool = False,
    cbarTitle=None,
    vmin=None,
    vmax=None,
    cmap="viridis",
    cbargs: dict | None = None,
    **mplArgs,
) -> AxHands

Draw geometries onto a matplotlib figure.

  • Each geometry type is displayed as an appropriate plotting type -> Points/ Multipoints are displayed as points using plt.plot(...) -> Lines/ MultiLines are displayed as lines using plt.plot(...) -> Polygons/ MultiPolygons are displayed as patches using the descartes library
  • Each geometry can be given its own set of matplotlib plotting parameters
Notes

This function does not call plt.show() for the final display of the figure. This must be done manually after calling this function. Otherwise plt.savefig(...) can be called to save the output somewhere.

Sometimes geometries will disappear because of the simplification procedure. If this happens, the procedure can be avoided by setting simplificationFactor to None. This will take much more memory and will take longer to plot, however

Parameters:

  • geoms

    (Geometry or [Geometry] or DataFrame) –

    The geometries to be drawn * If a DataFrame is given, the function looks for geometries under a columns named 'geom' * plotting arguments can be given by adding a column named 'MPL:*' where '*' stands in for the argument to be added - For geometries that should ignore this argument, set it as None

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs in which to draw each geometry * If not given, longitude/latitude is assumed * Although geometries can be given in any SRS, it is very helpful if they are already provided in the correct SRS

  • ax

    (matplotlib axis; optional, default: None ) –

    The axis to draw the geometries on * If not given, a new axis is generated and returned

  • simplificationFactor

    (float; optional, default: 5000 ) –

    The level to which geometries should be simplified. It can be thought of as the number of vertices allowed in either the X or Y dimension across the figure * A higher value means a more detailed plot, but may take longer to draw

  • colorBy

    (str; optional, default: None ) –

    The column in the geoms DataFrame to color by * Only useful when geoms is given as a DataFrame

  • figsize

    ((int, int); optional, default: (12, 12) ) –

    The figure size to create when generating a new axis * If resultign figure looks weird, altering the figure size is your best bet to make it look nicer

  • xlim

    ((float, float); optional, default: None ) –

    The x-axis limits

  • ylim

    ((float, float); optional, default: None ) –

    The y-axis limits

  • fontsize

    (int; optional, default: 16 ) –

    A base font size to apply to tick marks which appear * Titles and labels are given a size of 'fontsize' + 2

  • hideAxis

    (bool; optional, default: False ) –

    Instructs the created axis to hide its boundary * Only useful when generating a new axis

  • cbarTitle

    (str; optional, default: None ) –

    The title to give to the generated colorbar * If not given, but 'colorBy' is given, the same string for 'colorBy' is used * Only useful when 'colorBy' is given

  • vmin

    (float; optional, default: None ) –

    The minimum value to color * Only useful when 'colorBy' is given

  • vmax

    (float; optional, default: None ) –

    The maximum value to color * Only useful when 'colorBy' is given

  • cmap

    (str or matplotlib ColorMap; optional, default: 'viridis' ) –

    The colormap to use when coloring * Only useful when 'colorBy' is given

  • cbax

    (matplotlib axis; optional) –

    An explicitly given axis to use for drawing the colorbar * If not given, but 'colorBy' is given, an axis for the colorbar is automatically generated

  • cbargs

    (dict; optional, default: None ) –

    keyword arguments to pass on when creating the colorbar

  • **mplArgs

    All other keyword arguments are passed on to the plotting functions called for each geometry * Will be applied to ALL geometries. Be careful since this can cause errors when plotting geometries of different types

Returns:

  • A namedtuple containing:

    'ax' -> The map axis 'handles' -> All geometry handles which were created in the order they were drawn 'cbar' -> The colorbar handle if it was drawn

Source code in geokit/core/geom.py
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
def drawGeoms(
    geoms: ogr.Geometry | list[ogr.Geometry] | pd.DataFrame | np.ndarray,
    # srs: srs_input = 4326,
    srs: srs_input | None = None,
    ax: None | matplotlib.axes._axes.Axes | AxHands = None,
    simplificationFactor: numeric | None = 5000,
    colorBy: str | None = None,
    figsize: tuple[numeric, numeric] = (12, 12),
    xlim: tuple[numeric, numeric] | None = None,
    ylim: tuple[numeric, numeric] | None = None,
    fontsize: int = 16,
    hideAxis: bool = False,
    cbarTitle=None,
    vmin=None,
    vmax=None,
    cmap="viridis",
    cbargs: dict | None = None,
    **mplArgs,
) -> AxHands:
    """Draw geometries onto a matplotlib figure.

    * Each geometry type is displayed as an appropriate plotting type
        -> Points/ Multipoints are displayed as points using plt.plot(...)
        -> Lines/ MultiLines are displayed as lines using plt.plot(...)
        -> Polygons/ MultiPolygons are displayed as patches using the descartes
           library
    * Each geometry can be given its own set of matplotlib plotting parameters

    Notes
    -----
    This function does not call plt.show() for the final display of the figure.
    This must be done manually after calling this function. Otherwise
    plt.savefig(...) can be called to save the output somewhere.

    Sometimes geometries will disappear because of the simplification procedure.
    If this happens, the procedure can be avoided by setting simplificationFactor
    to None. This will take much more memory and will take longer to plot, however

    Parameters
    ----------
    geoms : ogr.Geometry or [ogr.Geometry, ] or pd.DataFrame
        The geometries to be drawn
          * If a DataFrame is given, the function looks for geometries under a
            columns named 'geom'
          * plotting arguments can be given by adding a column named 'MPL:****'
            where '****' stands in for the argument to be added
              - For geometries that should ignore this argument, set it as None

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs in which to draw each geometry
          * If not given, longitude/latitude is assumed
          * Although geometries can be given in any SRS, it is very helpful if
            they are already provided in the correct SRS

    ax : matplotlib axis; optional
        The axis to draw the geometries on
          * If not given, a new axis is generated and returned

    simplificationFactor : float; optional
        The level to which geometries should be simplified. It can be thought of
        as the number of vertices allowed in either the X or Y dimension across
        the figure
          * A higher value means a more detailed plot, but may take longer to draw

    colorBy : str; optional
        The column in the geoms DataFrame to color by
          * Only useful when geoms is given as a DataFrame

    figsize : (int, int); optional
        The figure size to create when generating a new axis
          * If resultign figure looks weird, altering the figure size is your best
            bet to make it look nicer

    xlim : (float, float); optional
        The x-axis limits

    ylim : (float, float); optional
        The y-axis limits

    fontsize : int; optional
        A base font size to apply to tick marks which appear
          * Titles and labels are given a size of 'fontsize' + 2

    hideAxis : bool; optional
        Instructs the created axis to hide its boundary
          * Only useful when generating a new axis

    cbarTitle : str; optional
        The title to give to the generated colorbar
          * If not given, but 'colorBy' is given, the same string for 'colorBy'
            is used
            * Only useful when 'colorBy' is given

    vmin : float; optional
        The minimum value to color
          * Only useful when 'colorBy' is given

    vmax : float; optional
        The maximum value to color
          * Only useful when 'colorBy' is given

    cmap : str or matplotlib ColorMap; optional
        The colormap to use when coloring
          * Only useful when 'colorBy' is given

    cbax : matplotlib axis; optional
        An explicitly given axis to use for drawing the colorbar
          * If not given, but 'colorBy' is given, an axis for the colorbar is
            automatically generated

    cbargs : dict; optional
        keyword arguments to pass on when creating the colorbar


    **mplArgs
        All other keyword arguments are passed on to the plotting functions called
        for each geometry
          * Will be applied to ALL geometries. Be careful since this can cause
            errors when plotting geometries of different types

    Returns
    -------
    A namedtuple containing:
       'ax' -> The map axis
       'handles' -> All geometry handles which were created in the order they were
                    drawn
       'cbar' -> The colorbar handle if it was drawn
    """
    if isinstance(ax, AxHands):
        ax = ax.ax
    if ax is None:
        pass
        fig, ax = plt.subplots(figsize=figsize)
    elif isinstance(ax, matplotlib.axes._axes.Axes):
        pass
    else:
        raise Exception(
            "Expected None or matplotlib.axes._axes.Axes object for the 'ax' argument. However, an object of type: "
            + str(type(ax))
            + " has been provided."
        )
    if hideAxis:
        ax.axis("off")

    if colorBy is None:
        cbax = None
    else:
        divider = make_axes_locatable(ax)
        cbax = divider.append_axes("right", size="2.5%", pad=0.05)

    ax.tick_params(labelsize=fontsize)

    # # Be sure we have a list
    pargs = None
    isFrame = False
    if isinstance(geoms, ogr.Geometry):
        geoms = [
            geoms,
        ]
    elif isinstance(geoms, pd.DataFrame):  # We have a DataFrame with plotting arguments
        isFrame = True
        data = geoms.drop("geom", axis=1)
        geoms = geoms["geom"].values

        pargs = pd.DataFrame(index=data.index)
        for c in data.columns:
            if not c[:4] == "MPL:":
                continue
            pargs[c[4:]] = data[c]

        if pargs.size == 0:
            pargs = None
    else:  # Assume its an iterable
        geoms = list(geoms)

    # Check Geometry SRS
    if not srs is None:
        srs = SRS.loadSRS(srs)
        transformed_geoms = []
        for gi, geometry in enumerate(geoms):
            gsrs = geometry.GetSpatialReference()
            if gsrs is None:
                continue  # Skip it if we don't know it...
            if not gsrs.IsSame(srs):
                transformed_geoms.append(transform(geoms[gi], srs))
            else:
                transformed_geoms.append(geoms[gi])
        geoms = np.asarray(transformed_geoms)

    # Apply simplifications if required
    if not simplificationFactor is None:
        if xlim is None or ylim is None:
            xMin, yMin, xMax, yMax = 1e100, 1e100, -1e100, -1e100
            for geometry in geoms:
                _xMin, _xMax, _yMin, _yMax = geometry.GetEnvelope()

                xMin = min(_xMin, xMin)
                xMax = max(_xMax, xMax)
                yMin = min(_yMin, yMin)
                yMax = max(_yMax, yMax)

        if not xlim is None:
            xMin, xMax = xlim
        if not ylim is None:
            yMin, yMax = ylim

        simplificationValue = max(xMax - xMin, yMax - yMin) / simplificationFactor

        oGeoms = geoms
        geoms = []

        def doSimplify(g):
            ng = g.Simplify(simplificationValue)
            return ng

        for geometry in oGeoms:
            # carefulSimplification=False
            # if carefulSimplification and "MULTI" in g.GetGeometryName():
            if "MULTI" in geometry.GetGeometryName():
                subgeoms = []
                for gi in range(geometry.GetGeometryCount()):
                    ng = doSimplify(geometry.GetGeometryRef(gi))
                    subgeoms.append(ng)

                geoms.append(flatten(subgeoms))
            else:
                geoms.append(doSimplify(geometry))

    # Handle color value
    if not colorBy is None:
        color_values = data[colorBy].values

        if isinstance(cmap, str):
            from matplotlib import cm

            cmap = getattr(cm, cmap)

        if is_numeric_dtype(arr_or_dtype=color_values.dtype):
            color_values_without_nan = color_values[~np.isnan(color_values)]
            if color_values_without_nan.size == 0:
                # All values are NaN: avoid max/min on empty array and division by zero.
                # Use a default normalized value (e.g. 0.0) for all entries.
                norm_vals = np.zeros_like(color_values, dtype=float)
            else:
                cValMax = color_values_without_nan.max() if vmax is None else vmax
                cValMin = color_values_without_nan.min() if vmin is None else vmin
                denom = cValMax - cValMin
                if denom == 0:
                    # All (non-NaN) values are identical: avoid division by zero.
                    # Map everything to the middle of the colormap.
                    norm_vals = np.full_like(color_values, 0.5, dtype=float)
                else:
                    norm_vals = (color_values - cValMin) / denom
            _colorVals = [cmap(v) for v in norm_vals]
        else:
            # categorical data

            unique_values = pd.unique(color_values)
            n_values = unique_values.size
            color_map = {val: cmap(i / n_values) for i, val in enumerate(unique_values)}
            _colorVals = [color_map[v] if v in color_map else (0, 0, 0, 0) for v in color_values]

    # Do Plotting
    # make patches
    handles = []
    if not pargs is None:
        s = [not v is None for v in pargs.iloc[gi]]
        plotargs = pargs.iloc[gi, s].to_dict()
    else:
        plotargs = dict()
    plotargs.update(mplArgs)

    for gi, geometry in enumerate(geoms):
        if not colorBy is None:
            colorVal = _colorVals[gi]
        else:
            colorVal = None
        # Determine type
        if geometry.GetGeometryName() == "POINT":
            handles.append(drawPoint(g=geometry, plotargs=plotargs, ax=ax, colorVal=colorVal))
        elif geometry.GetGeometryName() == "MULTIPOINT":
            handles.append(drawMultiPoint(geometry, plotargs, ax, colorVal))
        elif geometry.GetGeometryName() == "LINESTRING":
            handles.append(drawLine(geometry, plotargs, ax, colorVal))
        elif geometry.GetGeometryName() == "MULTILINESTRING":
            handles.append(drawMultiLine(geometry, plotargs, ax, colorVal))
        elif geometry.GetGeometryName() == "LINEARRING":
            handles.append(drawLinearRing(geometry, plotargs, ax, colorVal))
        elif geometry.GetGeometryName() == "POLYGON":
            handles.append(drawPolygon(g=geometry, plotargs=plotargs, ax=ax, colorVal=colorVal))
        elif geometry.GetGeometryName() == "MULTIPOLYGON":
            handles.append(drawMultiPolygon(g=geometry, plotargs=plotargs, ax=ax, colorVal=colorVal))
        else:
            msg = (
                "Could not draw geometry of type:",
                pargs.index[gi],
                "->",
                geometry.GetGeometryName(),
            )
            warnings.warn(msg, UserWarning)

    # Add the colorbar, maybe
    if colorBy is not None:
        norm = Normalize(vmin=cValMin, vmax=cValMax)
        tmp = dict(cmap=cmap, norm=norm, orientation="vertical")
        if not cbargs is None:
            tmp.update(cbargs)
        color_bar = ColorbarBase(cbax, **tmp)
        color_bar.ax.tick_params(labelsize=fontsize)
        color_bar.set_label(colorBy if cbarTitle is None else cbarTitle, fontsize=fontsize + 2)
    else:
        color_bar = None

    # Do some formatting

    ax.set_aspect("equal")
    ax.autoscale(enable=True)

    if not xlim is None:
        ax.set_xlim(*xlim)
    if not ylim is None:
        ax.set_ylim(*ylim)

    # Organize return
    if isFrame:
        return AxHands(ax, pd.Series(handles, index=data.index), color_bar)
    else:
        return AxHands(ax, handles, color_bar)

drawImage

drawImage(
    matrix: ndarray,
    ax: Axis | None = None,
    xlim: tuple[float | float] | None = None,
    ylim: tuple[float | float] | None = None,
    yAtTop: bool = True,
    scaling: float = 1,
    fontsize: int = 16,
    hideAxis=False,
    figsize: tuple[float, float] = (12, 12),
    cbar: bool = True,
    cbarPadding=0.01,
    cbarTitle=None,
    vmin=None,
    vmax=None,
    cmap="viridis",
    cbax=None,
    cbargs=None,
    leftMargin=0,
    rightMargin=0,
    topMargin=0,
    bottomMargin=0,
    **kwargs,
) -> AxHands

Draw a matrix as an image on a matplotlib canvas.

Parameters:

  • matrix

    (ndarray) –

    The matrix data to draw

  • ax

    (matplotlib.axis.Axis; optional, default: None ) –

    The axis to draw the geometries on * If not given, a new axis is generated and returned

  • xlim

    (tuple[float, float]; optional, default: None ) –

    The x-axis limits to draw the matrix on

  • ylim

    (tuple[float, float]; optional, default: None ) –

    The y-axis limits to draw the matrix on

  • yAtTop

    (bool; optional, default: True ) –

    If True, the first row of data should be plotted at the top of the image

  • scaling

    (numeric; optional, default: 1 ) –

    An integer factor by which to scale the matrix before plotting

  • figsize

    (tuple[float, float]; optional, default: (12, 12) ) –

    The figure size to create when generating a new axis * If resulting figure looks weird, altering the figure size is your best bet to make it look nicer

  • fontsize

    (int; optional, default: 16 ) –

    A base font size to apply to tick marks which appear * Titles and labels are given a size of 'fontsize' + 2

  • cbar

    (bool; optional, default: True ) –

    If True, a color bar will be drawn

  • cbarPadding

    (float; optional, default: 0.01 ) –

    The spacing padding to add between the generated axis and the generated colorbar axis * Only useful when generating a new axis * Only useful when 'colorBy' is given

  • cbarTitle

    (str; optional, default: None ) –

    The title to give to the generated colorbar * If not given, but 'colorBy' is given, the same string for 'colorBy' is used * Only useful when 'colorBy' is given

  • vmin

    (float; optional, default: None ) –

    The minimum value to color

  • vmax

    (float; optional, default: None ) –

    The maximum value to color

  • cmap

    (str or matplotlib ColorMap; optional, default: 'viridis' ) –

    The colormap to use when coloring

  • cbax

    (matplotlib axis; optional, default: None ) –

    An explicitly given axis to use for drawing the colorbar

  • cbargs

    (dict; optional, default: None ) –
  • leftMargin

    (float; optional, default: 0 ) –

    Additional margin to add to the left of the figure * Before using this, try adjusting the 'figsize'

  • rightMargin

    (float; optional, default: 0 ) –

    Additional margin to add to the left of the figure * Before using this, try adjusting the 'figsize'

  • topMargin

    (float; optional, default: 0 ) –

    Additional margin to add to the left of the figure * Before using this, try adjusting the 'figsize'

  • bottomMargin

    (float; optional, default: 0 ) –

    Additional margin to add to the left of the figure * Before using this, try adjusting the 'figsize'

Returns:

  • A namedtuple containing:

    'ax' -> The map axis 'handles' -> All geometry handles which were created in the order they were drawn 'cbar' -> The colorbar handle if it was drawn

Source code in geokit/core/util.py
def drawImage(
    matrix: np.ndarray,
    ax: matplotlib.axis.Axis | None = None,
    xlim: tuple[float | float] | None = None,
    ylim: tuple[float | float] | None = None,
    yAtTop: bool = True,
    scaling: float = 1,
    fontsize: int = 16,
    hideAxis=False,
    figsize: tuple[float, float] = (12, 12),
    cbar: bool = True,
    cbarPadding=0.01,
    cbarTitle=None,
    vmin=None,
    vmax=None,
    cmap="viridis",
    cbax=None,
    cbargs=None,
    leftMargin=0,
    rightMargin=0,
    topMargin=0,
    bottomMargin=0,
    **kwargs,
) -> AxHands:
    """Draw a matrix as an image on a matplotlib canvas.

    Parameters
    ----------
    matrix : numpy.ndarray
        The matrix data to draw

    ax : matplotlib.axis.Axis; optional
        The axis to draw the geometries on
          * If not given, a new axis is generated and returned

    xlim : tuple[float, float]; optional
        The x-axis limits to draw the matrix on

    ylim : tuple[float, float]; optional
        The y-axis limits to draw the matrix on

    yAtTop : bool; optional
        If True, the first row of data should be plotted at the top of the image

    scaling : numeric; optional
        An integer factor by which to scale the matrix before plotting

    figsize : tuple[float, float]; optional
        The figure size to create when generating a new axis
          * If resulting figure looks weird, altering the figure size is your best
            bet to make it look nicer

    fontsize : int; optional
        A base font size to apply to tick marks which appear
          * Titles and labels are given a size of 'fontsize' + 2

    cbar : bool; optional
        If True, a color bar will be drawn

    cbarPadding : float; optional
        The spacing padding to add between the generated axis and the generated
        colorbar axis
          * Only useful when generating a new axis
          * Only useful when 'colorBy' is given

    cbarTitle : str; optional
        The title to give to the generated colorbar
          * If not given, but 'colorBy' is given, the same string for 'colorBy'
            is used
            * Only useful when 'colorBy' is given

    vmin : float; optional
        The minimum value to color

    vmax : float; optional
        The maximum value to color

    cmap : str or matplotlib ColorMap; optional
        The colormap to use when coloring

    cbax : matplotlib axis; optional
        An explicitly given axis to use for drawing the colorbar

    cbargs : dict; optional

    leftMargin : float; optional
        Additional margin to add to the left of the figure
          * Before using this, try adjusting the 'figsize'

    rightMargin : float; optional
        Additional margin to add to the left of the figure
          * Before using this, try adjusting the 'figsize'

    topMargin : float; optional
        Additional margin to add to the left of the figure
          * Before using this, try adjusting the 'figsize'

    bottomMargin : float; optional
        Additional margin to add to the left of the figure
          * Before using this, try adjusting the 'figsize'

    Returns
    -------
    A namedtuple containing:
       'ax' -> The map axis
       'handles' -> All geometry handles which were created in the order they were
                    drawn
       'cbar' -> The colorbar handle if it was drawn
    """
    # Create an axis, if needed
    if isinstance(ax, AxHands):
        ax = ax.ax
    if ax is None:
        newAxis = True

        plt.figure(figsize=figsize)

        if not cbar:  # We don't need a colorbar
            if not hideAxis:
                leftMargin += 0.07

            ax = plt.axes(
                [
                    leftMargin,
                    bottomMargin,
                    1 - (rightMargin + leftMargin),
                    1 - (topMargin + bottomMargin),
                ]
            )
            cbax = None

        else:  # We need a colorbar
            rightMargin += 0.08  # Add area on the right for colorbar text
            if not hideAxis:
                leftMargin += 0.07

            cbarExtraPad = 0.05
            cbarWidth = 0.04

            ax = plt.axes(
                [
                    leftMargin,
                    bottomMargin,
                    1 - (rightMargin + leftMargin + cbarWidth + cbarPadding),
                    1 - (topMargin + bottomMargin),
                ]
            )

            cbax = plt.axes(
                [
                    1 - (rightMargin + cbarWidth),
                    bottomMargin + cbarExtraPad,
                    cbarWidth,
                    1 - (topMargin + bottomMargin + 2 * cbarExtraPad),
                ]
            )

        if hideAxis:
            ax.axis("off")
        else:
            ax.tick_params(labelsize=fontsize)
    else:
        newAxis = False

    # handle flipped matrix
    if not yAtTop:
        matrix = matrix[::-1, :]

    # Draw image
    if scaling:
        matrix = scaleMatrix(matrix, scaling, strict=False)

    if not (xlim is None and ylim is None):
        extent = xlim[0], xlim[1], ylim[0], ylim[1]
    else:
        extent = None

    h = ax.imshow(
        matrix,
        extent=extent,
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
        interpolation="none",
        **kwargs,
    )

    # Draw Colorbar
    if cbar:
        tmp = dict(cmap=cmap, orientation="vertical")
        if not cbargs is None:
            tmp.update(cbargs)

        if cbax is None:
            cbar = plt.colorbar(h, ax=ax, **tmp)
        else:
            cbar = plt.colorbar(h, cax=cbax)

        cbar.ax.tick_params(labelsize=fontsize)
        if not cbarTitle is None:
            cbar.set_label(cbarTitle, fontsize=fontsize + 2)

    # Do some formatting
    if newAxis:
        ax.set_aspect("equal")
        ax.autoscale(enable=True)

    # Done!
    return AxHands(ax, h, cbar)

drawRaster

drawRaster(
    source: load_raster_input | DataFrame,
    srs: srs_input | None = None,
    ax: Axes | None | AxHands = None,
    resolution: numeric | None = None,
    cutline=None,
    figsize: tuple[numeric, numeric] = (12, 12),
    xlim: tuple[numeric, numeric] | None = None,
    ylim: tuple[numeric, numeric] | None = None,
    fontsize: int = 16,
    hideAxis: bool = False,
    cbar: bool = True,
    cbarTitle: str | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap="viridis",
    cbargs=None,
    noData: numeric | None = None,
    zorder=0,
    resampleAlg: Literal[
        "near",
        "bilinear",
        "cubic",
        "cubicspline",
        "lanczos",
        "average",
        "rms",
        "mode",
        "max",
        "min",
        "med",
        "Q1",
        "Q3",
        "sum",
    ] = "med",
    **warp_kwargs,
) -> AxHands

Draw a raster as an image on a matplotlib canvas.

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource to draw

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the drawn raster data * If not given, the raster's internal srs is assumed * If the drawing resolution does not match the source's inherent resolution, the source will be warped to the correct format

  • ax

    (matplotlib axis; optional, default: None ) –

    The axis to draw the geometries on * If not given, a new axis is generated and returned

  • resolution

    (numeric or tuple; optional, default: None ) –

    The resolution of the plotted raster data * Lower resolution means more pixels to draw and can be a burden on memory * If a tuple is given, resolutions in the X and Y direction are expected * Changing the resolution from the inherent resolution requires a warp

  • cutline

    (str or ogr.Geometry; optional, default: None ) –

    The cutline to limit the drawn data too * If a string is given, it must be a path to a vector file * Values outside of the cutline are given the noData value of the raster * Requires a warp

  • figsize

    ((int, int); optional, default: (12, 12) ) –

    The figure size to create when generating a new axis * If resultign figure looks weird, altering the figure size is your best bet to make it look nicer

  • xlim

    ((float, float); optional, default: None ) –

    The x-axis limits

  • ylim

    ((float, float); optional, default: None ) –

    The y-axis limits

  • fontsize

    (int; optional, default: 16 ) –

    A base font size to apply to tick marks which appear * Titles and labels are given a size of 'fontsize' + 2

  • cbarTitle

    (str; optional, default: None ) –

    The title to give to the generated colorbar * If not given, but 'colorBy' is given, the same string for 'colorBy' is used * Only useful when 'colorBy' is given

  • vmin

    (float; optional, default: None ) –

    The minimum value to color * Only useful when 'colorBy' is given

  • vmax

    (float; optional, default: None ) –

    The maximum value to color * Only useful when 'colorBy' is given

  • cmap

    (str or matplotlib ColorMap; optional, default: 'viridis' ) –

    The colormap to use when coloring * Only useful when 'colorBy' is given

  • cbargs

    (dict; optional, default: None ) –
  • noData

    (numeric; optional, default: None ) –

    Replaces all previous noData values with this value in the output raster.

  • resampleAlg

    (str, default: 'med' ) –

    The resampleAlg passed on to a call of warp() if needed, by default "med"

  • **kwargs

    (Passed on to a call to warp()) –
    • Determines how the warping is carried out
    • Consider using 'resampleAlg' or 'workingType' for finer control

Returns:

  • A namedtuple containing:

    'ax' -> The map axis 'handles' -> All geometry handles which were created in the order they were drawn 'cbar' -> The colorbar handle if it was drawn

Source code in geokit/core/raster.py
def drawRaster(
    source: load_raster_input | pd.DataFrame,
    srs: srs_input | None = None,
    ax: matplotlib.axes._axes.Axes | None | AxHands = None,
    resolution: numeric | None = None,
    cutline=None,
    figsize: tuple[numeric, numeric] = (12, 12),
    xlim: tuple[numeric, numeric] | None = None,
    ylim: tuple[numeric, numeric] | None = None,
    fontsize: int = 16,
    hideAxis: bool = False,
    cbar: bool = True,
    cbarTitle: str | None = None,
    vmin: float | None = None,
    vmax: float | None = None,
    cmap="viridis",
    cbargs=None,
    noData: numeric | None = None,
    zorder=0,
    resampleAlg: Literal[
        "near",
        "bilinear",
        "cubic",
        "cubicspline",
        "lanczos",
        "average",
        "rms",
        "mode",
        "max",
        "min",
        "med",
        "Q1",
        "Q3",
        "sum",
    ] = "med",
    **warp_kwargs,
) -> AxHands:
    """Draw a raster as an image on a matplotlib canvas.

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource to draw

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the drawn raster data
          * If not given, the raster's internal srs is assumed
          * If the drawing resolution does not match the source's inherent
            resolution, the source will be warped to the correct format

    ax : matplotlib axis; optional
        The axis to draw the geometries on
          * If not given, a new axis is generated and returned

    resolution : numeric or tuple; optional
        The resolution of the plotted raster data
        * Lower resolution means more pixels to draw and can be a burden on
          memory
        * If a tuple is given, resolutions in the X and Y direction are expected
        * Changing the resolution from the inherent resolution requires a warp

    cutline : str or ogr.Geometry; optional
        The cutline to limit the drawn data too
        * If a string is given, it must be a path to a vector file
        * Values outside of the cutline are given the noData value of the raster
        * Requires a warp

    figsize : (int, int); optional
        The figure size to create when generating a new axis
          * If resultign figure looks weird, altering the figure size is your best
            bet to make it look nicer

    xlim : (float, float); optional
        The x-axis limits

    ylim : (float, float); optional
        The y-axis limits

    fontsize : int; optional
        A base font size to apply to tick marks which appear
          * Titles and labels are given a size of 'fontsize' + 2

    cbarTitle : str; optional
        The title to give to the generated colorbar
          * If not given, but 'colorBy' is given, the same string for 'colorBy'
            is used
            * Only useful when 'colorBy' is given

    vmin : float; optional
        The minimum value to color
          * Only useful when 'colorBy' is given

    vmax : float; optional
        The maximum value to color
          * Only useful when 'colorBy' is given

    cmap : str or matplotlib ColorMap; optional
        The colormap to use when coloring
          * Only useful when 'colorBy' is given

    cbargs : dict; optional

    noData : numeric; optional
        Replaces all previous noData values with this value in the output raster.

    resampleAlg : str, optional
        The resampleAlg passed on to a call of warp() if needed, by default "med"

    **kwargs : Passed on to a call to warp()
        * Determines how the warping is carried out
        * Consider using 'resampleAlg' or 'workingType' for finer control

    Returns
    -------
    A namedtuple containing:
       'ax' -> The map axis
       'handles' -> All geometry handles which were created in the order they were
                    drawn
       'cbar' -> The colorbar handle if it was drawn
    """
    if isinstance(ax, AxHands):
        ax = ax.ax
    if ax is None:
        new_main_axis = True
        fig, ax = plt.subplots(figsize=figsize)
    elif isinstance(ax, matplotlib.axes._axes.Axes):
        new_main_axis = False
    else:
        raise Exception(
            "Expected None or matplotlib.axes._axes.Axes object for the 'ax' argument. However, an object of type: "
            + str(type(ax))
            + " has been provided."
        )
    if hideAxis:
        ax.axis("off")

    if cbar is False:
        cbax = None
    else:
        divider = make_axes_locatable(ax)
        cbax = divider.append_axes("right", size="2.5%", pad=0.05)

    ax.tick_params(labelsize=fontsize)

    # Load the raster datasource and check for transformation
    source = loadRaster(source)
    info = rasterInfo(source)

    if not (srs is None and resolution is None and cutline is None and xlim is None and ylim is None):
        if not (xlim is None and ylim is None):
            bounds = (
                xlim[0],
                ylim[0],
                xlim[1],
                ylim[1],
            )
        else:
            bounds = None

        if resolution is None:
            xres, yres = None, None
        else:
            try:
                xres, yres = resolution
            except:
                xres, yres = resolution, resolution

        source = warp(
            source,
            cutline=cutline,
            pixelHeight=yres,
            pixelWidth=xres,
            srs=srs,
            bounds=bounds,
            noData=noData,
            resampleAlg=resampleAlg,
            **warp_kwargs,
        )

    info = rasterInfo(source)

    # Read the Data
    data = extractMatrix(source).astype(float)

    data[data == info.noData] = np.nan

    # Draw image
    ext = (
        info.xMin,
        info.xMax,
        info.yMin,
        info.yMax,
    )
    h = ax.imshow(
        data,
        extent=ext,
        vmin=vmin,
        vmax=vmax,
        cmap=cmap,
        zorder=zorder,
        interpolation="none",
    )

    # Draw Colorbar
    if cbar is True:
        tmp = dict(cmap=cmap, orientation="vertical")
        if cbargs is not None:
            tmp.update(cbargs)

        cbar_output = plt.colorbar(h, cax=cbax, **tmp)
        cbar_output.ax.tick_params(labelsize=fontsize)
        if cbarTitle is not None:
            cbar_output.set_label(cbarTitle, fontsize=fontsize + 2)
    else:
        cbar_output = None

    # Do some formatting
    ax.set_aspect("equal")
    ax.autoscale(enable=True)

    if xlim is not None:
        ax.set_xlim(*xlim)
    if ylim is not None:
        ax.set_ylim(*ylim)

    # Done!
    return UTIL.AxHands(ax, h, cbar_output)

drawSmopyMap

drawSmopyMap(
    bounds,
    zoom,
    tileserver="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
    tilesize: int = 256,
    maxtiles: int = 100,
    ax=None,
    attribution="© OpenStreetMap contributors",
    attribution_size=12,
    **kwargs,
)

Draws a basemap using the "smopy" python package.

NOTE: * The basemap is drawn using the Smopy python package. See here: https://github.com/rossant/smopy * Be careful to adhere to the usage guidelines of the chosen tile source - By default, this source is OSM. See here: https://wiki.openstreetmap.org/wiki/Tile_servers

!IMPORTANT! If you will publish any images drawn with this method, it's likely that the tile source will require an attribution to be written on the image. For example, if using OSM tile (the default), you have to write "© OpenStreetMap contributors" clearly on the map. But this is different for each tile source!

Returns:

  • namedtuple
    • .ax -> The axes draw on
    • .srs -> The SRS used when drawing (will always be EPSG 3857)
    • .bounds -> The boundaries of the drawn map
Source code in geokit/core/raster.py
def drawSmopyMap(
    bounds,
    zoom,
    tileserver="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png",
    tilesize: int = 256,
    maxtiles: int = 100,
    ax=None,
    attribution="© OpenStreetMap contributors",
    attribution_size=12,
    **kwargs,
):
    """
    Draws a basemap using the "smopy" python package.

    NOTE:
    * The basemap is drawn using the Smopy python package. See here: https://github.com/rossant/smopy
    * Be careful to adhere to the usage guidelines of the chosen tile source
        - By default, this source is OSM. See here: https://wiki.openstreetmap.org/wiki/Tile_servers

    !IMPORTANT! If you will publish any images drawn with this method, it's likely that the tile source
    will require an attribution to be written on the image. For example, if using OSM tile (the default),
    you have to write "© OpenStreetMap contributors" clearly on the map. But this is different for each
    tile source!

    Parameters
    ----------
        bounds : (xMin, yMix, xMax, yMax) or Extent
            The geographic extent to be drawn

        zoom : int
            The zoom level to draw (between 1-20)
            * I suggest starting low (e.g. 4), and zooming in until you find a level that suits your needs

        tileserver : string
            The tile server to use

        tilesize : int
            The pixel size of the tiles from 'tileserver'

        maxtiles : int
            The maximum tiles to use when drawing an image
            * Be careful to adhere to the usage conditions stated by your selected tileserver!

        ax : matplotlib.axes
            The matplotlib axes to draw on
            * If 'None', then one will be generated automatically

        kwargs
            All extra keyword arguments are passed on to matplotlib.ax.imshow

    Returns
    -------
        namedtuple
            * .ax     -> The axes draw on
            * .srs    -> The SRS used when drawing (will always be EPSG 3857)
            * .bounds -> The boundaries of the drawn map
    """
    import smopy

    if ax is None:
        import matplotlib.pyplot as plt

        ax = plt.gca()

    try:
        lon_min, lat_min, lon_max, lat_max = bounds.xyXY
    except:
        lon_min, lat_min, lon_max, lat_max = bounds

    tile_box = smopy.get_tile_box((lat_max, lon_min, lat_min, lon_max), zoom)

    img = smopy.fetch_map(
        box=tile_box,
        z=zoom,
        tileserver=tileserver,
        tilesize=tilesize,
        maxtiles=maxtiles,
    )

    tl_lat, tl_lon = smopy.num2deg(tile_box[0], tile_box[1], zoom=zoom)
    br_lat, br_lon = smopy.num2deg(tile_box[2] + 1, tile_box[3] + 1, zoom=zoom)

    xy = SRS.xyTransform(
        [(tl_lon, tl_lat), (br_lon, br_lat)],
        fromSRS=SRS.EPSG4326,
        toSRS=SRS.EPSG3857,
        outputFormat="xy",
    )

    ax.imshow(img, extent=(xy.x.min(), xy.x.max(), xy.y.min(), xy.y.max()), **kwargs)

    if attribution is not None:
        ax.text(
            1,
            0,
            attribution,
            transform=ax.transAxes,
            ha="right",
            va="bottom",
            zorder=10,
            fontsize=attribution_size,
        )

    SmopyMap = namedtuple("SmopyMap", "ax srs bounds")

    return SmopyMap(ax, SRS.EPSG3857, (tl_lon, br_lat, br_lon, tl_lat))

empty

empty(gtype, srs=None)

Make a generic OGR geometry of a desired type.

Not for the feint of heart

Parameters:

  • gtpe

    (str) –

    The geometry type to make * Point, MultiPoint, Line, MultiLine, Polygon, MultiPolygon, etc...

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the geometry to create

Returns:

  • Geometry
Source code in geokit/core/geom.py
def empty(gtype, srs=None):
    """
    Make a generic OGR geometry of a desired type.

    *Not for the feint of heart*

    Parameters
    ----------
    gtpe : str
        The geometry type to make
          * Point, MultiPoint, Line, MultiLine, Polygon, MultiPolygon, etc...

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometry to create

    Returns
    -------
    ogr.Geometry
    """
    if not hasattr(ogr, "wkb" + gtype):
        raise GeoKitGeomError("Could not find geometry type: " + gtype)
    geom = ogr.Geometry(getattr(ogr, "wkb" + gtype))

    if not srs is None:
        geom.AssignSpatialReference(SRS.loadSRS(srs))

    return geom

extractAndClipFeatures

extractAndClipFeatures(
    source: load_vector_input | DataFrame,
    geom,
    where=None,
    srs=None,
    onlyGeom=False,
    indexCol=None,
    skipMissingGeoms=True,
    layerName=None,
    scaleAttrs=None,
    minShare=0.001,
    **kwargs,
) -> DataFrame | Series | Generator

Extracts features from a source and clips them to the boundaries of a given geom. Optionally scales numeric attribute values linearly to the overlapping area share.

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector data source to read from

  • geom

    (Geometry) –

    The geometry to search with * All features touching this geometry are extracted and clipped to the geometry boundaries.

  • where

    (str; optional, default: None ) –

    An SQL-like where statement to apply to the source * Feature attribute name do not need quotes * String values should be wrapped in 'single quotes' Example: If the source vector has a string attribute called "ISO" and a integer attribute called "POP", you could use....

    where = "ISO='DEU' AND POP>1000"
    
  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the geometries to extract * If not given, the geom's inherent srs is used * If srs does not match the inherent srs, all geometries will be transformed

  • onlyGeom

    (bool; optional, default: False ) –

    If True, only feature geometries will be returned

  • indexCol

    (str; optional, default: None ) –

    The feature identifier to use as the DataFrams's index * Only useful when as DataFrame is True

  • skipMissingGeoms

    (bool; optional, default: True ) –

    If True, then the parser will not read a feature which are missing a geometry

  • layerName

    (str; optional, default: None ) –

    The name of the layer to extract from the source vector dataset (only applicable in case of a geopackage).

  • scaleAttrs

    (str or list, default: None ) –

    attribute names of the source with numeric values. The values will be scaled linearly with the area share of the feature overlapping the geom.

  • minShare

    (float, default: 0.001 ) –

    The min. area share of a polygon that falls either inside or outside the clipping geom. Allows to deal with imperfect boundary alignments. 0 means that all clipped geoms, however small they may be, are considered. Example: If minShare=0.001 (0.1%), a polygon that overlaps with the clipping geom by 99.92% of its area will NOT be clipped. If another polygon also at minShare=0.001 overlaps by only 0.06% of its area, it will be disregarded. By default 0.001.

Returns:

  • * pandas.DataFrame or pandas.Series
Source code in geokit/core/vector.py
def extractAndClipFeatures(
    source: load_vector_input | pd.DataFrame,
    geom,
    where=None,
    srs=None,
    onlyGeom=False,
    indexCol=None,
    skipMissingGeoms=True,
    layerName=None,
    scaleAttrs=None,
    minShare=0.001,
    **kwargs,
) -> pd.DataFrame | pd.Series | Generator:
    """
    Extracts features from a source and clips them to the boundaries of a given geom.
    Optionally scales numeric attribute values linearly to the overlapping area share.

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector data source to read from

    geom : ogr.Geometry
        The geometry to search with
        * All features touching this geometry are extracted and clipped to the geometry boundaries.

    where : str; optional
        An SQL-like where statement to apply to the source
        * Feature attribute name do not need quotes
        * String values should be wrapped in 'single quotes'
        Example: If the source vector has a string attribute called "ISO" and
                 a integer attribute called "POP", you could use....

            where = "ISO='DEU' AND POP>1000"

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometries to extract
          * If not given, the geom's inherent srs is used
          * If srs does not match the inherent srs, all geometries will be
            transformed

    onlyGeom : bool; optional
        If True, only feature geometries will be returned

    indexCol : str; optional
        The feature identifier to use as the DataFrams's index
        * Only useful when as DataFrame is True

    skipMissingGeoms : bool; optional
        If True, then the parser will not read a feature which are missing a geometry

    layerName : str; optional
        The name of the layer to extract from the source vector dataset (only applicable in case of a geopackage).

    scaleAttrs : str or list, optional
        attribute names of the source with numeric values. The values will be scaled linearly with the
        area share of the feature overlapping the geom.

    minShare : float
        The min. area share of a polygon that falls either inside or
        outside the clipping geom. Allows to deal with imperfect boundary
        alignments. 0 means that all clipped geoms, however small they
        may be, are considered. Example: If minShare=0.001 (0.1%), a
        polygon that overlaps with the clipping geom by 99.92% of its
        area will NOT be clipped. If another polygon also at
        minShare=0.001 overlaps by only 0.06% of its area, it will be
        disregarded. By default 0.001.

    Returns
    -------
    * pandas.DataFrame or pandas.Series
    """
    assert 0 <= minShare <= 1, f"minShare must be greater or equal to 0 and less or equal to 1.0"
    # assert and preprocess input source
    if isinstance(source, pd.DataFrame):
        # check validity of input dataframe
        if not "geom" in source.columns:
            raise AttributeError(f"source is given as a pd.DataFrame but has not 'geom' column.")
        if not isinstance(source.geom.iloc[0], ogr.Geometry):
            raise TypeError(
                f"source is given as a pd.DataFrame but value in 'geom' column is not of type osgeo.ogr.Geometry."
            )
        # return empty dataframe with empty expected "areaShare" column if no geometries contained since vector cannot be created without geometries
        if len(source) == 0:
            source["areaShare"] = None
            return source
        # generate a vector from source dataframe
        source = createVector(source)
    elif isinstance(source, str) or isinstance(source, pathlib.Path):
        if not os.path.isfile(source):
            raise FileNotFoundError(f"source is given as a string but is not an existing filepath: {source}")
        # load as vector file
        source = loadVector(source)
    elif not isinstance(source, gdal.Dataset):
        raise TypeError(
            f"source must either be a pd.DataFrame, a gdal.Dataset vector instance or a str formatted shapefile path."
        )

    # extract only the overlapping geoms, first define srs
    if srs is None:
        srs = geom.GetSpatialReference()
    else:
        geom = GEOM.transform(geom, toSRS=srs)
    df = extractFeatures(
        source=source,
        geom=geom,
        where=where,
        srs=srs,
        onlyGeom=onlyGeom,
        indexCol=indexCol,
        skipMissingGeoms=skipMissingGeoms,
        layerName=layerName,
        **kwargs,
    )
    if scaleAttrs is None:
        scaleAttrs = []
    elif isinstance(scaleAttrs, str):
        scaleAttrs = [scaleAttrs]
    else:
        assert isinstance(scaleAttrs, list), f"scaleAttrs must be a str or a list thereof if not None."
    if len(df) > 0:
        for _attr in scaleAttrs:
            if not _attr in list(df.columns):
                raise AttributeError(
                    f"'{_attr}' was given as scaleAttrs but is not an attribute of the source dataframe."
                )
            if not all([isinstance(_val, numbers.Number) for _val in df[_attr]]):
                raise TypeError(f"All values in column '{_attr}' in scaleAttrs must be numeric.")

    # check if we have any features to clip at all
    if len(df) == 0:
        # if not, add the mandatory areaShare column in case that it is not there already and return empty dataframe
        df["areaShare"] = None
        return df
    # else add the expected areaShare column
    assert not "areaShare" in list(df.columns), f"source data must not contain a 'areaShare' attribute."
    df["areaShare"] = 1.0
    # check if we need to clip the geometries at all
    if df.geom.iloc[0].GetGeometryName()[:5] == "POINT":
        # we have only points and no further clipping is needed
        return df

    # else we need to add an ID column and generate a new vector
    assert not "clippingID" in list(df.columns), f"source data must not contain a 'clippingID' attribute."
    df["clippingID"] = range(len(df))
    _vec = createVector(df)
    # extract only these features intersected by the outer geom boundary
    outer_df = extractFeatures(
        source=_vec,
        geom=geom.Boundary(),
        where=where,
        srs=srs,
        indexCol=indexCol,
        skipMissingGeoms=skipMissingGeoms,
        layerName=layerName,
        **kwargs,
    )
    del _vec
    if len(outer_df) == 0:
        # we have no features intersecting with the geom boundary, return all included features

        return df.drop(columns="clippingID")

    # else clip these features that are intersected by the geom
    _clippedIDs = list()
    _clippedGeoms = list()
    _areaShares = list()
    for i, feat in zip(outer_df.clippingID, outer_df.geom):
        _clipped = feat.Intersection(geom)
        _areaShare = _clipped.Area() / feat.Area()
        if _areaShare >= 1.0 - minShare:
            # the feature is only touched by the boundary (or protrudes minimally outside the geom edge) -> will not be reduced
            continue
        elif _areaShare <= 0.0 + minShare:
            # the feature is fully outside the geom and only touches the geom boundary (or overlaps only minimally with geom)
            # -> set clipped feature geometry to np.nan to filter out later
            _clipped = np.nan
        _clippedGeoms.append(_clipped)
        _areaShares.append(_areaShare)
        _clippedIDs.append(i)

    if len(_clippedIDs) == 0:
        # we have not clipped any feature at all, return df
        return df.drop(columns="clippingID")

    # else replace the original feature geometries with the clipped ones where needed and add area shares
    df.loc[df.clippingID.isin(_clippedIDs), "geom"] = _clippedGeoms
    df.loc[df.clippingID.isin(_clippedIDs), "areaShare"] = _areaShares
    for _attr in scaleAttrs:
        df[_attr] = df.apply(lambda x: x[_attr] * x.areaShare, axis=1)

    # drop nan geometries that do not (sufficiently) overlap filter geom
    df = df[~df.geom.isna()].reset_index(drop=True)

    # return the adapted dataframe
    return df.drop(columns="clippingID")

extractAsDataFrame

extractAsDataFrame(
    source: load_vector_input,
    indexCol: str | None = None,
    geom: None | Geometry = None,
    where=None,
    srs: srs_input | None = None,
    **kwargs,
)

Convenience function calling extractFeatures and structuring the output as a pandas DataFrame.

  • Geometries are written to a column called 'geom'
  • Attributes are written to a column of the same name

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector datasource to read from

  • indexCol

    (str; optional, default: None ) –

    The feature identifier to use as the DataFrams's index

  • geom

    (ogr.Geometry; optional, default: None ) –

    The geometry to search within * All features are extracted which touch this geometry

  • where

    (str; optional, default: None ) –

    An SQL-like where statement to apply to the source * Feature attribute name do not need quotes * String values should be wrapped in 'single quotes' Example: If the source vector has a string attribute called "ISO" and a integer attribute called "POP", you could use....

    where = "ISO='DEU' AND POP>1000"
    
  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the geometries to extract * If not given, the source's inherent srs is used * If srs does not match the inherent srs, all geometries will be transformed

Returns:

  • DataFrame
Source code in geokit/core/vector.py
def extractAsDataFrame(
    source: load_vector_input,
    indexCol: str | None = None,
    geom: None | ogr.Geometry = None,
    where=None,
    srs: srs_input | None = None,
    **kwargs,
):
    """Convenience function calling extractFeatures and structuring the output as
    a pandas DataFrame.

    * Geometries are written to a column called 'geom'
    * Attributes are written to a column of the same name

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector datasource to read from

    indexCol : str; optional
        The feature identifier to use as the DataFrams's index

    geom : ogr.Geometry; optional
        The geometry to search within
        * All features are extracted which touch this geometry

    where : str; optional
        An SQL-like where statement to apply to the source
        * Feature attribute name do not need quotes
        * String values should be wrapped in 'single quotes'
        Example: If the source vector has a string attribute called "ISO" and
                 a integer attribute called "POP", you could use....

            where = "ISO='DEU' AND POP>1000"

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometries to extract
          * If not given, the source's inherent srs is used
          * If srs does not match the inherent srs, all geometries will be
            transformed

    Returns
    -------
    pandas.DataFrame
    """
    warnings.warn(
        "This function will be removed in favor of geokit.vector.extractFeatures",
        DeprecationWarning,
    )
    return extractFeatures(source=source, indexCol=indexCol, geom=geom, where=where, srs=srs, **kwargs)

extractFeature

extractFeature(
    source: load_vector_input,
    where: None | str | int = None,
    geom=None,
    srs=None,
    onlyGeom=False,
    onlyAttr=False,
) -> Feature | Geometry | dict

Convenience function calling extractFeatures which assumes there is only one feature to extract.

  • Will raise an error if multiple features are found

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector datasource to read from

  • where

    (str, int; optional, default: None ) –

    If string -> An SQL-like where statement to apply to the source If int -> The feature's ID within the dataset * Feature attribute name do not need quotes * String values should be wrapped in 'single quotes' Example: If the source vector has a string attribute called "ISO" and a integer attribute called "POP", you could use....

    where = "ISO='DEU' AND POP>1000"
    
  • geom

    (ogr.Geometry; optional, default: None ) –

    The geometry to search with * All features are extracted which touch this geometry

  • outputSRS

    (Anything acceptable to geokit.srs.loadSRS(); optional) –

    The srs of the geometries to extract * If not given, the source's inherent srs is used * If srs does not match the inherent srs, all geometries will be transformed

  • onlyGeom

    (bool; optional, default: False ) –

    If True, only feature geometries will be returned

  • onlyAttr

    (bool; optional, default: False ) –

    If True, only feature attributes will be returned

Returns:

  • * If onlyGeom and onlyAttr are False: namedtuple -> (geom, attr)
  • * If onlyGeom is True: ogr.Geometry
  • * If onlyAttr is True: dict
Source code in geokit/core/vector.py
def extractFeature(
    source: load_vector_input,
    where: None | str | int = None,
    geom=None,
    srs=None,
    onlyGeom=False,
    onlyAttr=False,
) -> UTIL.Feature | ogr.Geometry | dict:
    """Convenience function calling extractFeatures which assumes there is only
    one feature to extract.

    * Will raise an error if multiple features are found

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector datasource to read from

    where : str, int; optional
        If string -> An SQL-like where statement to apply to the source
        If int -> The feature's ID within the dataset
        * Feature attribute name do not need quotes
        * String values should be wrapped in 'single quotes'
        Example: If the source vector has a string attribute called "ISO" and
                 a integer attribute called "POP", you could use....

            where = "ISO='DEU' AND POP>1000"

    geom : ogr.Geometry; optional
        The geometry to search with
        * All features are extracted which touch this geometry

    outputSRS : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometries to extract
          * If not given, the source's inherent srs is used
          * If srs does not match the inherent srs, all geometries will be
            transformed

    onlyGeom : bool; optional
        If True, only feature geometries will be returned

    onlyAttr : bool; optional
        If True, only feature attributes will be returned

    Returns
    -------
    * If onlyGeom and onlyAttr are False: namedtuple -> (geom, attr)
    * If onlyGeom is True: ogr.Geometry
    * If onlyAttr is True: dict
    """
    warnings.warn(message="extractFeature is deprecated use extractFeatures instead.", category=DeprecationWarning)
    if isinstance(where, int):
        ds = loadVector(source)
        lyr = ds.GetLayer()
        ftr = lyr.GetFeature(where)

        fGeom = ftr.GetGeometryRef().Clone()
        fItems = ftr.items().copy()

    else:
        getter = _extractFeatures(
            source=source,
            geom=geom,
            where=where,
            srs=srs,
            onlyGeom=onlyGeom,
            onlyAttr=onlyAttr,
            skipMissingGeoms=False,
        )

        # Get first result
        res = next(getter)

        if onlyGeom:
            fGeom = res
        elif onlyAttr:
            fItems = res
        else:
            (fGeom, fItems) = res

        # try to get a second result
        try:
            next(getter)
        except StopIteration:
            pass
        else:
            raise GeoKitVectorError("More than one feature found")

    if not srs is None and not onlyAttr:
        srs = SRS.loadSRS(srs)
        if not fGeom.GetSpatialReference().IsSame(srs):
            fGeom.TransformTo(srs)

    # Done!
    if onlyGeom:
        return fGeom
    elif onlyAttr:
        return fItems
    else:
        return UTIL.Feature(fGeom, fItems)

extractFeatures

extractFeatures(
    source,
    where=None,
    geom=None,
    srs=None,
    onlyGeom=False,
    onlyAttr=False,
    asPandas=True,
    indexCol=None,
    skipMissingGeoms=True,
    layerName=None,
    spatialPredicate: Literal[
        "Touches", "Overlaps", "CentroidWithin"
    ] = "Touches",
) -> DataFrame | Series | Generator

Creates a generator which extract the features contained within the source.

  • Iteratively returns (feature-geometry, feature-fields)
Note:

Be careful when filtering by a geometry as the extracted features may not necessarily be IN the given shape * Sometimes they may only overlap * Sometimes they are only in the geometry's envelope * To be sure an extracted geometry fits the selection criteria, you may still need to do further processing or use extractAndClipFeatures()

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector data source to read from

  • geom

    (ogr.Geometry; optional, default: None ) –

    The geometry to search with * All features are extracted which touch this geometry

  • where

    (str; optional, default: None ) –

    An SQL-like where statement to apply to the source * Feature attribute name do not need quotes * String values should be wrapped in 'single quotes' Example: If the source vector has a string attribute called "ISO" and a integer attribute called "POP", you could use....

    where = "ISO='DEU' AND POP>1000"
    
  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the geometries to extract * If not given, the source's inherent srs is used * If srs does not match the inherent srs, all geometries will be transformed

  • onlyGeom

    (bool; optional, default: False ) –

    If True, only feature geometries will be returned

  • onlyAttr

    (bool; optional, default: False ) –

    If True, only feature attributes will be returned

  • asPandas

    (bool; optional, default: True ) –

    Whether or not the result should be returned as a pandas.DataFrame (when onlyGeom is False) or pandas.Series (when onlyGeom is True)

  • indexCol

    (str; optional, default: None ) –

    The feature identifier to use as the DataFrams's index * Only useful when as DataFrame is True

  • skipMissingGeoms

    (bool; optional, default: True ) –

    If True, then the parser will not read a feature which are missing a geometry

  • layerName

    (str; optional, default: None ) –

    The name of the layer to extract from the source vector dataset (only applicable in case of a geopackage).

  • spatialPredicate

    (str, default: 'Touches' ) –

    Applies only in combination with given 'geom' filter. If "Touches", all geometries will be extracted that simply touch the filter geom. If "Overlaps", geometries to be extracted must overlap (for lines, this represents an "Intersect") either partially or completely (i.e. it includes "Within"), and if "CentroidWithin" the centroid of the extracted geom must be within or on the filter geom. By default "Touches". NOTE: When filter geom is a polygon, centroids exactly on the filter geom boundary will NOT be extracted.

Returns:

  • * If asPandas is True: pandas.DataFrame or pandas.Series
  • * If asPandas is False: generator
Source code in geokit/core/vector.py
def extractFeatures(
    source,
    where=None,
    geom=None,
    srs=None,
    onlyGeom=False,
    onlyAttr=False,
    asPandas=True,
    indexCol=None,
    skipMissingGeoms=True,
    layerName=None,
    spatialPredicate: Literal["Touches", "Overlaps", "CentroidWithin"] = "Touches",
) -> pd.DataFrame | pd.Series | Generator:
    """Creates a generator which extract the features contained within the source.

    * Iteratively returns (feature-geometry, feature-fields)

    Note:
    -----
    Be careful when filtering by a geometry as the extracted features may not
    necessarily be IN the given shape
    * Sometimes they may only overlap
    * Sometimes they are only in the geometry's envelope
    * To be sure an extracted geometry fits the selection criteria, you may
      still need to do further processing or use extractAndClipFeatures()

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector data source to read from

    geom : ogr.Geometry; optional
        The geometry to search with
        * All features are extracted which touch this geometry

    where : str; optional
        An SQL-like where statement to apply to the source
        * Feature attribute name do not need quotes
        * String values should be wrapped in 'single quotes'
        Example: If the source vector has a string attribute called "ISO" and
                 a integer attribute called "POP", you could use....

            where = "ISO='DEU' AND POP>1000"

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometries to extract
          * If not given, the source's inherent srs is used
          * If srs does not match the inherent srs, all geometries will be
            transformed

    onlyGeom : bool; optional
        If True, only feature geometries will be returned

    onlyAttr : bool; optional
        If True, only feature attributes will be returned

    asPandas : bool; optional
        Whether or not the result should be returned as a pandas.DataFrame (when
        onlyGeom is False) or pandas.Series (when onlyGeom is True)

    indexCol : str; optional
        The feature identifier to use as the DataFrams's index
        * Only useful when as DataFrame is True

    skipMissingGeoms : bool; optional
        If True, then the parser will not read a feature which are missing a geometry

    layerName : str; optional
        The name of the layer to extract from the source vector dataset (only applicable in case of a geopackage).

    spatialPredicate : str, optional
        Applies only in combination with given 'geom' filter. If "Touches",
        all geometries will be extracted that simply touch the filter
        geom. If "Overlaps", geometries to be extracted must overlap (for
        lines, this represents an "Intersect") either partially or
        completely (i.e. it includes "Within"), and if "CentroidWithin"
        the centroid of the extracted geom must be within or on the
        filter geom. By default "Touches".
        NOTE: When filter geom is a polygon, centroids exactly on the
        filter geom boundary will NOT be extracted.

    Returns
    -------
    * If asPandas is True: pandas.DataFrame or pandas.Series
    * If asPandas is False: generator
    """
    # arrange output
    if not asPandas:
        return _extractFeatures(
            source=source,
            geom=geom,
            where=where,
            srs=srs,
            onlyGeom=onlyGeom,
            onlyAttr=onlyAttr,
            skipMissingGeoms=skipMissingGeoms,
            layerName=layerName,
            spatialPredicate=spatialPredicate,
        )
    else:
        fields = defaultdict(list)
        fields["geom"] = []

        for g, a in _extractFeatures(
            source=source,
            geom=geom,
            where=where,
            srs=srs,
            onlyGeom=False,
            onlyAttr=False,
            skipMissingGeoms=skipMissingGeoms,
            layerName=layerName,
            spatialPredicate=spatialPredicate,
        ):
            fields["geom"].append(g.Clone())
            for k, v in a.items():
                fields[k].append(v)

        df = pd.DataFrame(fields)
        if not indexCol is None:
            df.set_index(indexCol, inplace=True, drop=False)

        if onlyGeom:
            assert not onlyAttr, f"onlyGeom cannot be combined with onlyAttr."
            return df["geom"]
        elif onlyAttr:
            return df.drop("geom", axis=1)
        else:
            return df

extractMatrix

extractMatrix(
    source: load_raster_input,
    bounds=None,
    boundsSRS: srs_input = "latlon",
    maskBand: bool = False,
    autocorrect: bool = False,
    returnBounds: bool = False,
) -> (
    ndarray
    | tuple[
        ndarray, tuple[float, float, float, float] | None
    ]
)

Extract all or part of a raster's band as a numpy matrix.

Note:

Unless one is trying to get the entire matrix from the raster dataset, usage of this function requires intimate knowledge of the raster's characteristics. In such a case it is probably easier to use Extent.extractMatrix

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource

  • bounds

    The boundary to clip the raster to before mutating * If given as an Extent, the extent is always cast to the source's - native srs before mutating * If given as a tuple, (xMin, yMin, xMax, yMax) is expected - Units must be in the srs specified by 'boundsSRS' * This boundary must fit within the boundary of the rasters source * The boundary is always fitted to the source's grid, so the returned values do not necessarily match to the boundary which is provided

  • boundsSRS

    (srs_input, default: 'latlon' ) –

    The srs of the 'bounds' argument * This is ignored if the 'bounds' argument is an Extent object or is None

  • autocorrect

    (bool; optional, default: False ) –

    If True, the matrix will search for no data values and change them to numpy.nan * Data type will always result in a float, so be careful with large matrices

  • returnBounds

    (bool; optional, default: False ) –

    If True, return the computed bounds along with the matrix data

Returns:

  • * If returnBounds is False: numpy.ndarray -> Two dimensional matrix
  • * If returnBounds is True: (numpy.ndarray, tuple)
    • ndarray is matrix data
    • tuple is the (xMin, yMin, xMax, yMax) of the computed bounds
Source code in geokit/core/raster.py
def extractMatrix(
    source: load_raster_input,
    bounds=None,
    boundsSRS: srs_input = "latlon",
    maskBand: bool = False,
    autocorrect: bool = False,
    returnBounds: bool = False,
) -> np.ndarray | tuple[np.ndarray, tuple[float, float, float, float] | None]:
    """Extract all or part of a raster's band as a numpy matrix.

    Note:
    -----
    Unless one is trying to get the entire matrix from the raster dataset, usage
    of this function requires intimate knowledge of the raster's characteristics.
    In such a case it is probably easier to use Extent.extractMatrix

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource

    bounds: tuple or Extent
        The boundary to clip the raster to before mutating
        * If given as an Extent, the extent is always cast to the source's
            - native srs before mutating
        * If given as a tuple, (xMin, yMin, xMax, yMax) is expected
            - Units must be in the srs specified by 'boundsSRS'
        * This boundary must fit within the boundary of the rasters source
        * The boundary is always fitted to the source's grid, so the returned
          values do not necessarily match to the boundary which is provided

    boundsSRS: Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the 'bounds' argument
        * This is ignored if the 'bounds' argument is an Extent object or is None

    autocorrect : bool; optional
        If True, the matrix will search for no data values and change them to
        numpy.nan
        * Data type will always result in a float, so be careful with large
          matrices

    returnBounds : bool; optional
        If True, return the computed bounds along with the matrix data

    Returns
    -------
    * If returnBounds is False: numpy.ndarray -> Two dimensional matrix
    * If returnBounds is True: (numpy.ndarray, tuple)
        - ndarray is matrix data
        - tuple is the (xMin, yMin, xMax, yMax) of the computed bounds
    """
    sourceDS = loadRaster(source)  # BE sure we have a raster
    dsInfo = rasterInfo(sourceDS)

    if maskBand:
        mb = sourceDS.GetMaskBand()
    else:
        sourceBand = sourceDS.GetRasterBand(1)  # get band

    # Handle the boundaries
    if bounds is not None:
        # check for extent
        try:
            isExtent = bounds._whatami == "Extent"
        except:
            isExtent = False

        # Ensure srs is okay
        if isExtent:
            bounds = bounds.castTo(dsInfo.srs).fit((dsInfo.dx, dsInfo.dy)).xyXY
        else:
            boundsSRS = SRS.loadSRS(boundsSRS)
            if not dsInfo.srs.IsSame(boundsSRS):
                bounds = GEOM.boundsToBounds(bounds, boundsSRS, dsInfo.srs)
            bounds = UTIL.fitBoundsTo(bounds, dsInfo.dx, dsInfo.dy, expand=True, startAtZero=True)

        # Find offsets
        xoff = int(np.round((bounds[0] - dsInfo.xMin) / dsInfo.dx))
        if xoff < 0:
            raise GeoKitRasterError("The given boundary exceeds the raster's xMin value")

        xwin = int(np.round((bounds[2] - dsInfo.xMin) / dsInfo.dx)) - xoff
        if xwin > dsInfo.xWinSize:
            raise GeoKitRasterError("The given boundary exceeds the raster's xMax value")

        if dsInfo.yAtTop:
            yoff = int(np.round((dsInfo.yMax - bounds[3]) / dsInfo.dy))
            if yoff < 0:
                raise GeoKitRasterError("The given boundary exceeds the raster's yMax value")

            ywin = int(np.round((dsInfo.yMax - bounds[1]) / dsInfo.dy)) - yoff

            if ywin > dsInfo.yWinSize:
                raise GeoKitRasterError("The given boundary exceeds the raster's yMin value")
        else:
            yoff = int(np.round((bounds[1] - dsInfo.yMin) / dsInfo.dy))
            if yoff < 0:
                raise GeoKitRasterError("The given boundary exceeds the raster's yMin value")

            ywin = int(np.round((bounds[3] - dsInfo.yMin) / dsInfo.dy)) - yoff
            if ywin > dsInfo.yWinSize:
                raise GeoKitRasterError("The given boundary exceeds the raster's yMax value")

    else:
        xoff = 0
        yoff = 0
        xwin = None
        ywin = None

    # get Data
    if maskBand:
        data = mb.ReadAsArray(xoff=xoff, yoff=yoff, win_xsize=xwin, win_ysize=ywin)
    else:
        data = sourceBand.ReadAsArray(xoff=xoff, yoff=yoff, win_xsize=xwin, win_ysize=ywin)
        if dsInfo.scale is not None and dsInfo.scale != 1.0:
            data = data * dsInfo.scale
        if dsInfo.offset is not None and dsInfo.offset != 0.0:
            data = data + dsInfo.offset

    # Correct 'nodata' values
    if autocorrect:
        noData = sourceBand.GetNoDataValue()
        data = data.astype(np.float64)
        data[data == noData] = np.nan

    # make sure we are returning data in the 'flipped-y' orientation
    if not isFlipped(source):
        data = data[::-1, :]

    # Done
    if returnBounds:
        return data, bounds
    else:
        return data

extractValues

extractValues(
    source: load_raster_input | list[load_raster_input],
    points: tuple[float, float]
    | Geometry
    | list[tuple[float, float]]
    | list[Geometry]
    | Location
    | LocationSet,
    pointSRS: srs_input | None = None,
    winRange: int = 0,
    noDataOkay: bool = True,
    _onlyValues: bool = False,
) -> ptValue | DataFrame | numeric | ndarray

Extracts the value of a raster at a given point or collection of points. Can also extract a window of values if desired.

  • If the given raster is not in the 'flipped-y' orientation, the result will be automatically flipped
Notes

Generally speaking, interpolateValues() should be used instead of this function

Parameters:

  • source

    (Anything acceptable by loadRaster() or list) –

    The raster datasource, can be a filepath, a raster dataset etc., see RASTER.loadRaster() for details. Alternatively, a list of multiple such raster datasources.

  • points

    ((X, Y) or [(X1, Y1), (X2, Y2), ...] or Location or LocationSet()) –

    Coordinates for the points to extract * All points must be in the same SRS * !REMEMBER! For lat and lon coordinates, X is lon and Y is lat (opposite of what you may think...)

  • pointSRS

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the point to create * If not given, longitude/latitude is assumed * Only useful when 'points' is not a LocationSet

  • winRange

    (int, default: 0 ) –

    The window range (in pixels) to extract the values centered around the closest raster index to the indicated locations. * A winRange of 0 will only extract the closest raster value * A winRange of 1 will extract a window of shape (3,3) * A winRange of 3 will extract a window of shape (7,7)

  • noDataOkay

    (bool, default: True ) –

    If True, an error is raised if a 'noData' value is extracted If False, numpy.nan is inserted whenever a 'noData' value is extracted

  • _onlyValues

    (bool, default: False ) –

    Return only the extracted data and omit xOffeset, yOffset, inBounds

Returns:

  • * If only a single location is given:

    namedtuple -> (data : The extracted data at the location xOffset : The X index distance from the location to the center of the closest raster pixel yOffset : The Y index distance from the location to the center of the closest raster pixel inBounds: Flag for whether or not the location is within The raster's bounds )

  • * If Multiple locations are given:

    pandas.DataFrame * Columns are (data, xOffset, yOffset, inBounds) - See above for column descriptions * Index is 0...N if 'points' input is not a LocationSet or returns numpy array if _onlyValues=True

Source code in geokit/core/raster.py
def extractValues(
    source: load_raster_input | list[load_raster_input],
    points: tuple[float, float]
    | ogr.Geometry
    | list[tuple[float, float]]
    | list[ogr.Geometry]
    | Location
    | LocationSet,
    pointSRS: srs_input | None = None,
    winRange: int = 0,
    noDataOkay: bool = True,
    _onlyValues: bool = False,
) -> ptValue | pd.DataFrame | numeric | np.ndarray:
    """Extracts the value of a raster at a given point or collection of points.
       Can also extract a window of values if desired.

    * If the given raster is not in the 'flipped-y' orientation, the result will
      be automatically flipped

    Notes
    -----
    Generally speaking, interpolateValues() should be used instead of this function

    Parameters
    ----------
    source : Anything acceptable by loadRaster() or list
        The raster datasource, can be a filepath, a raster dataset etc., see
        RASTER.loadRaster() for details. Alternatively, a list of multiple
        such raster datasources.

    points : (X,Y) or [(X1,Y1), (X2,Y2), ...] or Location or LocationSet()
        Coordinates for the points to extract
        * All points must be in the same SRS
        * !REMEMBER! For lat and lon coordinates, X is lon and Y is lat
          (opposite of what you may think...)

    pointSRS : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the point to create
          * If not given, longitude/latitude is assumed
          * Only useful when 'points' is not a LocationSet

    winRange : int
        The window range (in pixels) to extract the values centered around the
        closest raster index to the indicated locations.
        * A winRange of 0 will only extract the closest raster value
        * A winRange of 1 will extract a window of shape (3,3)
        * A winRange of 3 will extract a window of shape (7,7)

    noDataOkay: bool
        If True, an error is raised if a 'noData' value is extracted
        If False, numpy.nan is inserted whenever a 'noData' value is extracted

    _onlyValues: bool
        Return only the extracted data and omit xOffeset, yOffset, inBounds

    Returns
    -------
    * If only a single location is given:
        namedtuple -> (data : The extracted data at the location
                       xOffset : The X index distance from the location to the
                                 center of the closest raster pixel
                       yOffset : The Y index distance from the location to the
                                 center of the closest raster pixel
                       inBounds: Flag for whether or not the location is within
                                 The raster's bounds
                        )
    * If Multiple locations are given:
        pandas.DataFrame
            * Columns are (data, xOffset, yOffset, inBounds)
                - See above for column descriptions
            * Index is 0...N if 'points' input is not a LocationSet
        or returns numpy array if _onlyValues=True
    """
    # Be sure we have a raster and srs
    if not isinstance(source, list):
        source = [source]
    # make sure all source entries can be interpreted by loadRaster
    try:
        source = [loadRaster(s) for s in source]
    except:
        raise TypeError("At least one source cannot be loaded by geokit.raster.loadRaster().")
    raster_srs = None
    for s in source:
        # load file to make sure it can be interpreted by loadRaster
        if raster_srs is None:
            raster_srs = rasterInfo(s).srs
        else:
            assert raster_srs.IsSame(rasterInfo(s).srs), "All source entries must have the same SRS."

    # Ensure we have a list of point geometries
    points_as_list_of_geom = _convertPointsToListOfOGRPoints(
        points=points, point_input_SRS=pointSRS, output_srs=raster_srs
    )
    if len(points_as_list_of_geom) > 1:
        asSingle = False
    elif len(points_as_list_of_geom) == 1:
        asSingle = True
    else:
        raise GEOM.GeoKitGeomError("No points have been passed to the function extractValues()")

    # get the srcs that are actually overlapping with our points
    src_mapper = {}
    if len(source) > 1:
        for s in source:
            # get bounds and filter indices of points that fall into bounds
            _bounds = rasterInfo(s).bounds
            _indices = [
                i
                for i, p in enumerate(points_as_list_of_geom)
                if (_bounds[0] <= p.GetX() <= _bounds[2]) and (_bounds[1] <= p.GetY() <= _bounds[3])
            ]
            # add source as key plus list of overlapped point indices
            src_mapper[s] = _indices
    else:
        # we have only one source which must be applied to all points
        src_mapper[source[0]] = list(range(len(points_as_list_of_geom)))
        # srcs = source * len(points)

    # iterate over all unique srcs and extract the raster values for the affected points _src by _src

    values = [np.nan] * len(points_as_list_of_geom)  # initialize values with nan for every point
    inBounds = [False] * len(points_as_list_of_geom)  # initialize inbounds with False for every point
    xOffset = [np.nan] * len(points_as_list_of_geom)  # initialize offsets with nan for every point
    yOffset = [np.nan] * len(points_as_list_of_geom)  # initialize offsets with nan for every point

    for _src, _inds in src_mapper.items():
        # get the indices of the points for which data has been extracted already
        _xtrct = [i for i, v in enumerate(values) if np.all(pd.notnull(v))]
        # deduct them from the inds here (they may have been extracted already from an overlapping _src tile)
        _inds = list(set(_inds) - set(_xtrct))

        if len(_inds) == 0:
            # no indices left to extract from this _src
            continue

        # get the points with this _src via list indices
        _points = [points_as_list_of_geom[i] for i in _inds]

        # get the raster info for this _src
        _info = rasterInfo(_src)

        # Get x/y values as numpy arrays
        x = np.array([pt.GetX() for pt in _points])
        y = np.array([pt.GetY() for pt in _points])

        # Calculate x/y indexes
        xValues = (x - (_info.xMin + 0.5 * _info.pixelWidth)) / _info.pixelWidth
        xIndexes = np.round(xValues)
        _xOffset = xValues - xIndexes

        if _info.yAtTop:
            yValues = ((_info.yMax - 0.5 * _info.pixelWidth) - y) / abs(_info.pixelHeight)
            yIndexes = np.round(yValues)
            _yOffset = yValues - yIndexes
        else:
            yValues = (y - (_info.yMin + 0.5 * _info.pixelWidth)) / _info.pixelHeight
            yIndexes = np.round(yValues)
            _yOffset = -1 * (yValues - yIndexes)

        # Calculate the starts and window size
        xStarts = xIndexes - winRange
        yStarts = yIndexes - winRange
        window = 2 * winRange + 1

        _inBounds = xStarts >= 0
        _inBounds = _inBounds & (yStarts >= 0)
        _inBounds = _inBounds & (xStarts + window <= _info.xWinSize)
        _inBounds = _inBounds & (yStarts + window <= _info.yWinSize)

        # Read values
        _values = []
        band = _src.GetRasterBand(1)

        for xi, yi, ib in zip(xStarts, yStarts, _inBounds):
            if not ib:
                data = np.ones((window, window)) * np.nan
            else:
                # Open and read from raster
                data = band.ReadAsArray(xoff=xi, yoff=yi, win_xsize=window, win_ysize=window)
                if (_info.scale != None) and (_info.scale != 1.0):
                    data = data * _info.scale
                if (_info.offset != None) and (_info.offset != 0.0):
                    data = data + _info.offset

                # Look for nodata
                if _info.noData is not None:
                    nodata = data == _info.noData
                    if nodata.any():
                        if noDataOkay:
                            # data will neaed to be a float type to represent a nodata value
                            data = data.astype(np.float64)
                            data[nodata] = np.nan
                        else:
                            raise GeoKitRasterError(
                                "No data values found in extractValues with 'noDataOkay' set to False"
                            )

                # flip if not in the 'flipped-y' orientation
                if not _info.yAtTop:
                    data = data[::-1, :]

            if winRange == 0:
                # If winRange is 0, there's no need to return a 2D matrix
                data = data[0][0]

            # Append to values
            _values.append(data)

            # check if not _inbounds, then replace values with nan
            for i in range(len(_values)):
                if not _inBounds[i]:
                    _values[i] = np.nan * np.ones_like(_values[i])

        # write _values, _inbounds and offsets into the overall container at the respective indices
        for i, v, ib, x, y in zip(_inds, _values, _inBounds, _xOffset, _yOffset):
            values[i] = v
            inBounds[i] = ib
            xOffset[i] = x
            yOffset[i] = y

    if np.isnan(values).any():
        # we still have NaN values left
        msg = f"WARNING: {np.isnan(values).sum()} of the given points (or extraction windows) exceed/s the source's limits. Values were replaced with nan."
        warnings.warn(msg, UserWarning)

    # Done!
    if asSingle:  # A single point was given, so return a single result
        if _onlyValues:
            return values[0]
        else:
            return ptValue(values[0], xOffset[0], yOffset[0], inBounds[0])
    else:
        if _onlyValues:
            return np.array(values)
        else:
            return pd.DataFrame(
                dict(data=values, xOffset=xOffset, yOffset=yOffset, inBounds=inBounds),
                index=None,
            )

extractVerticies

extractVerticies(geom)

Get all vertices found on the geometry as a Nx2 numpy.ndarray.

Source code in geokit/core/geom.py
def extractVerticies(geom):
    """Get all vertices found on the geometry as a Nx2 numpy.ndarray."""
    isMulti = "MULTI" in geom.GetGeometryName()
    # Check geometry type
    if "LINE" in geom.GetGeometryName():
        if isMulti:
            pts = []
            for gi in range(geom.GetGeometryCount()):
                pts.append(geom.GetGeometryRef(gi).GetPoints())
        else:
            pts = geom.GetPoints()
    elif "POLYGON" in geom.GetGeometryName():
        if isMulti:
            pts = []
            for gi in range(geom.GetGeometryCount()):
                newGeom = geom.GetGeometryRef(gi).GetBoundary()
                pts.append(extractVerticies(newGeom))
        else:
            newGeom = geom.GetBoundary()
            pts = extractVerticies(newGeom)

    elif "POINT" in geom.GetGeometryName():
        if isMulti:
            pts = []
            for gi in range(geom.GetGeometryCount()):
                pts.append(geom.GetGeometryRef(gi).GetPoints())
        else:
            pts = geom.GetPoints()

    else:
        raise GeoKitGeomError("Cannot extract points from geometry ")

    if isMulti:
        out = np.concatenate(pts)
    else:
        out = np.array(pts)

    if out.shape[1] == 3:  # This can happen when POINTs are extracted
        out = out[:, :2]
    return out

fixOutOfBoundsGeoms

fixOutOfBoundsGeoms(geom, how='shift')

This function allows to deal with polygons that protrude over the SRS bounds at +/-180° longitude respectively +/-90° latitude. Polygon areas that exceed those bounds are either clipped or shifted to the "opposite end of the map" with shapes at the poles being inverted and shifted by 180° to create a "fold-over" effect.

geom : osgeo-ogr.Geometry Geometry to fix. how : str, optional The way how to deal with sub shapes extending over the bounds: 'shift' : split off and shift to the "opposite end of the map" 'clip' : clip and remove extending shapes completely

Source code in geokit/core/geom.py
def fixOutOfBoundsGeoms(geom, how="shift"):
    """
    This function allows to deal with polygons that protrude over the SRS bounds
    at +/-180° longitude respectively +/-90° latitude. Polygon areas that exceed
    those bounds are either clipped or shifted to the "opposite end of the map"
    with shapes at the poles being inverted and shifted by 180° to create a
    "fold-over" effect.

    geom : osgeo-ogr.Geometry
        Geometry to fix.
    how : str, optional
        The way how to deal with sub shapes extending over the bounds:
        'shift' :   split off and shift to the "opposite end of the map"
        'clip' :    clip and remove extending shapes completely
    """
    assert isinstance(geom, ogr.Geometry), f"geom must be an osgeo.ogr.Geometry"
    assert how in ["clip", "shift"], f"how must be in 'clip', 'shift'"
    # get the envelope and srs of original geom
    env = geom.GetEnvelope()
    _srs = geom.GetSpatialReference()
    assert _srs.IsSame(SRS.loadSRS(4326)), f"SRS must be EPSG:4326"

    if env[0] >= -180 and env[1] <= 180 and env[2] >= -90 and env[3] <= 90:
        # the polygon is completely within bounds already, return as is
        return geom

    # else we need to clip and shift/rotate
    geom_fixed = copy(geom)
    # HORIZONTZAL BOUNDS
    if env[0] < -180 or env[1] > 180:
        # we need to clip & shift in HORIZONTAL direction
        basebox_tripleheight = polygon([(-180, -90 * 3), (-180, 90 * 3), (180, 90 * 3), (180, -90 * 3)], srs=4326)
        geom_fixed = geom_fixed.Intersection(basebox_tripleheight)  # clip off outer parts
        if env[0] < -180 and how == "shift":
            # extends over left edge, get the left part, shift and merge
            left_part = shift(basebox_tripleheight, lonShift=-360).Intersection(geom)
            if geom_fixed.IsEmpty():
                geom_fixed = shift(left_part, lonShift=360)  # overwrite when no center part
            else:
                geom_fixed = geom_fixed.Union(shift(left_part, lonShift=360))
        if env[1] > 180 and how == "shift":
            # extends over right edge, get the right part, shift and merge
            right_part = shift(basebox_tripleheight, lonShift=+360).Intersection(geom)
            if geom_fixed.IsEmpty():
                geom_fixed = shift(right_part, lonShift=-360)  # overwrite when no center part
            else:
                geom_fixed = geom_fixed.Union(shift(right_part, lonShift=-360))
    # VERTICAL BOUNDS
    if env[2] < -90 or env[3] > 90:
        # we need to clip in VERTICAL direction and rotate (horizontal issue are fixed already)
        def fold_over_pole(geom):
            """Aux function to fold a geometry over the +/-90° latitude line and rotate it."""
            env = geom.GetEnvelope()
            center_lon = (env[0] + env[1]) / 2  # x value of center axis of whole geom

            def fold_polygon(polygon):
                """Function that folds a polygon geometry over 90° lat line."""

                def _fold_ring(ring):
                    """Core function for every linear ring."""
                    new_ring = ogr.Geometry(ogr.wkbLinearRing)
                    for i in range(ring.GetPointCount()):
                        x, y, *z = ring.GetPoint(i)
                        # if needed, mirror y at +/-90° line and flip x value around center axis + shift by 180° (fold over)
                        if -90 <= y <= 90:  # all good
                            y_new = y
                            x_new = x
                        else:  # rotate and flip x values, fold y values
                            _x_new = x + 2 * (center_lon - x)
                            x_new = (_x_new + 180) % 360
                            if y > 90:
                                y_new = 180 - y
                                y_new = min(y_new, 90.0 - 1e-6)  # avoid that geoms touch each other at the pole
                            elif y < -90:
                                y_new = -180 - y
                                y_new = max(y_new, -90.0 + 1e-6)  # avoid that geoms touch each other at the pole
                        new_ring.AddPoint(x_new, y_new)  # create new ring point
                    return new_ring

                new_geom = ogr.Geometry(ogr.wkbPolygon)
                outer_ring = _fold_ring(polygon.GetGeometryRef(0))
                new_geom.AddGeometry(outer_ring)
                # now do this for every potential other (inner) ring
                for i in range(1, polygon.GetGeometryCount()):
                    inner_ring = _fold_ring(polygon.GetGeometryRef(i))
                    new_geom.AddGeometry(inner_ring)
                return new_geom

            if geom.GetGeometryName() == "POLYGON":
                # apply function directly
                new_geom = fold_polygon(geom)
            elif geom.GetGeometryName() == "MULTIPOLYGON":
                new_geom = ogr.Geometry(ogr.wkbMultiPolygon)
                # fold iteratively every single sub poly
                for i in range(geom.GetGeometryCount()):
                    sub_geom = geom.GetGeometryRef(i)
                    rotated = fold_polygon(sub_geom)
                    new_geom.AddGeometry(rotated)
            else:
                raise NotImplementedError(f"Geometry type '{geom.GetGeometryName()}' not supported")
            # assign SRS and return
            new_geom.AssignSpatialReference(geom.GetSpatialReference())
            return new_geom

        basebox = polygon([(-180, -90), (-180, 90), (180, 90), (180, -90)], srs=4326)
        geom_fixed_horizontally = copy(geom_fixed)
        geom_fixed = geom_fixed.Intersection(basebox)
        if env[2] < -90 and how == "shift":
            # clip off the bottom part and rotate to the other side (by 180°)
            bottom_part = shift(basebox, latShift=-180).Intersection(geom_fixed_horizontally)
            bottom_part_shifted = fold_over_pole(bottom_part)
            geom_fixed = geom_fixed.Union(bottom_part_shifted)
        if env[3] > 90 and how == "shift":
            # clip off the top part and rotate to the other side (by 180°)
            top_part = shift(basebox, latShift=+180).Intersection(geom_fixed_horizontally)
            top_part_shifted = fold_over_pole(top_part)
            geom_fixed = geom_fixed.Union(top_part_shifted)
    # assign srs and return
    geom_fixed.AssignSpatialReference(_srs)
    return geom_fixed

flatten

flatten(geoms)

Flatten a list of geometries into a single geometry object.

Combine geometries by iteratively union-ing neighbors (according to index) * example, given a list of geometries (A,B,C,D,E,F,G,H,I,J): [ A B C D E F G H I J ] [ AB CD EF GH IJ ] [ ABCD EFGH IJ ] [ ABCDEFGH IJ ] [ ABCDEFGHIJ ] <- This becomes the resulting geometry

Example:
* A list of Polygons/Multipolygons will become a single Multipolygon
* A list of Linestrings/MultiLinestrings will become a single MultiLinestring
Source code in geokit/core/geom.py
def flatten(geoms):
    """Flatten a list of geometries into a single geometry object.

    Combine geometries by iteratively union-ing neighbors (according to index)
     * example, given a list of geometries (A,B,C,D,E,F,G,H,I,J):
          [ A  B  C  D  E  F  G  H  I  J ]
          [  AB    CD    EF    GH    IJ  ]
          [    ABCD        EFGH      IJ  ]
          [        ABCDEFGH          IJ  ]
          [               ABCDEFGHIJ     ]  <- This becomes the resulting geometry

    Example:
    --------
        * A list of Polygons/Multipolygons will become a single Multipolygon
        * A list of Linestrings/MultiLinestrings will become a single MultiLinestring
    """
    if not isinstance(geoms, list):
        geoms = list(geoms)
        try:  # geoms is not a list, but it might be iterable
            geoms = list(geoms)
        except:
            raise ValueError("argument must be a list of geometries")

    if len(geoms) == 0:
        return None

    # Begin flattening
    while len(geoms) > 1:
        newGeoms = []
        for gi in range(0, len(geoms), 2):
            try:
                if not geoms[gi].IsValid():
                    warnings.warn("WARNING: Invalid Geometry encountered", UserWarning)
                if not geoms[gi + 1].IsValid():
                    warnings.warn("WARNING: Invalid Geometry encountered", UserWarning)

                newGeoms.append(geoms[gi].Union(geoms[gi + 1]))
            except IndexError:  # should only occur when length of geoms is odd
                newGeoms.append(geoms[gi])

        geoms = newGeoms
    return geoms[0]

gradient

gradient(
    source, mode="total", factor=1, asMatrix=False, **kwargs
)

Calculate a raster's gradient and return as a new dataset or simply a matrix.

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource

  • mode

    (str; optional, default: 'total' ) –

    Determines the type of gradient to compute * Options are.... "total" : Calculates the absolute gradient as a ratio

    "slope" : Same as 'total'

    "north-south" : Calculates the "north-facing" gradient as a ratio where negative numbers indicate a south facing gradient

    "east-west" : Calculates the "east-facing" gradient as a ratio where negative numbers indicate a west facing gradient

    "aspect" : calculates the gradient's direction in radians (0 is east)

    "dir" : same as 'aspect'

  • factor

    (numeric or latlonToM, default: 1 ) –

    The scaling factor relating the units of the x & y dimensions to the z dimension * If factor is 'latlonToM', the x & y units are assumed to be degrees (lat & lon) and the z units are assumed to be meters. A factor is then computed for coordinates at the source's center. * Example: If x,y units are meters and z units are feet, factor should be 0.3048

  • asMatrix

    (bool, default: False ) –

    If True, makes the returned value a matrix If False, makes the returned value a raster dataset

  • **kwargs

    (All extra key word arguments are passed on to a final call to, default: {} ) –

    'createRaster' * Only useful when 'asMatrix' is True

Returns:

  • * If 'asMatrix' is True: numpy.ndarray
  • * If 'asMatrix' is False: gdal.Dataset
Source code in geokit/core/raster.py
def gradient(source, mode="total", factor=1, asMatrix=False, **kwargs):
    """Calculate a raster's gradient and return as a new dataset or simply a matrix.

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource

    mode : str; optional
        Determines the type of gradient to compute
        * Options are....
          "total" : Calculates the absolute gradient as a ratio

          "slope" : Same as 'total'

          "north-south" : Calculates the "north-facing" gradient as a ratio where
                          negative numbers indicate a south facing gradient

          "east-west" : Calculates the "east-facing" gradient as a ratio where
                        negative numbers indicate a west facing gradient

          "aspect" : calculates the gradient's direction in radians (0 is east)

          "dir" : same as 'aspect'

    factor : numeric or 'latlonToM'
        The scaling factor relating the units of the x & y dimensions to the z
        dimension
        * If factor is 'latlonToM', the x & y units are assumed to be degrees
          (lat & lon) and the z units are assumed to be meters. A factor is then
          computed for coordinates at the source's center.
        * Example: If x,y units are meters and z units are feet, factor should
          be 0.3048

    asMatrix : bool
        If True, makes the returned value a matrix
        If False, makes the returned value a raster dataset

    **kwargs : All extra key word arguments are passed on to a final call to
        'createRaster'
        * Only useful when 'asMatrix' is True

    Returns
    -------
    * If 'asMatrix' is True: numpy.ndarray
    * If 'asMatrix' is False: gdal.Dataset
    """
    # Make sure source is a source
    source = loadRaster(source)

    # Check mode
    acceptable = [
        "total",
        "slope",
        "north-south",
        "east-west",
        "dir",
        "ew",
        "ns",
        "aspect",
    ]
    if mode not in acceptable:
        raise ValueError("'mode' not understood. Must be one of: ", acceptable)

    # Get the factor
    sourceInfo = rasterInfo(source)
    if factor == "latlonToM":
        latMid = (sourceInfo.yMax + sourceInfo.yMin) / 2
        R_EARTH = 6371000
        DEGtoRAD = np.pi / 180

        yFactor = R_EARTH * DEGtoRAD  # Get arc length in meters/Degree
        xFactor = R_EARTH * DEGtoRAD * np.cos(latMid * DEGtoRAD)  # ditto...
    else:
        try:
            xFactor, yFactor = factor
        except:
            yFactor = factor
            xFactor = factor

    # Calculate gradient
    arr = extractMatrix(source)

    if mode in ["north-south", "ns", "total", "slope", "dir", "aspect"]:
        ns = np.zeros(arr.shape)
        ns[1:-1, :] = (arr[2:, :] - arr[:-2, :]) / (2 * sourceInfo.dy * yFactor)
        if mode in ["north-south", "ns"]:
            output = ns

    if mode in ["east-west", "total", "slope", "dir", "aspect"]:
        ew = np.zeros(arr.shape)
        ew[:, 1:-1] = (arr[:, :-2] - arr[:, 2:]) / (2 * sourceInfo.dx * xFactor)
        if mode in ["east-west", "ew"]:
            output = ew

    if mode == "total" or mode == "slope":
        output = np.sqrt(ns * ns + ew * ew)

    if mode == "dir" or mode == "aspect":
        output = np.arctan2(ns, ew)

    # Done!
    if asMatrix:
        return output
    else:
        return createRaster(
            bounds=sourceInfo.bounds,
            pixelWidth=sourceInfo.dx,
            pixelHeight=sourceInfo.dy,
            srs=sourceInfo.srs,
            data=output,
            **kwargs,
        )

indexToCoord

indexToCoord(
    yi: int | ndarray,
    xi: int | ndarray,
    source=None,
    asPoint: bool = False,
    bounds=None,
    dx=None,
    dy=None,
    yAtTop=True,
    srs=None,
) -> Geometry | tuple[int | int] | ndarray

Convert the index of a raster to coordinate values.

Parameters:

  • xi

    (int) –

    The x index * a numpy array of ints is also acceptable

  • yi

    (int) –

    The y index * a numpy array of ints is also acceptable

  • source

    (Anything acceptable by loadRaster(), default: None ) –

    The contentual raster datasource

  • asPoint

    (bool, default: False ) –

    Instruct program to return point geometries instead of x,y coordinates

Returns:

  • * If 'asPoint' is True: ogr.Geometry
  • * If 'asPoint' is False: tuple -> (x,y) coordinates
Source code in geokit/core/raster.py
def indexToCoord(
    yi: int | np.ndarray,
    xi: int | np.ndarray,
    source=None,
    asPoint: bool = False,
    bounds=None,
    dx=None,
    dy=None,
    yAtTop=True,
    srs=None,
) -> ogr.Geometry | tuple[int | int] | np.ndarray:
    """Convert the index of a raster to coordinate values.

    Parameters
    ----------
    xi : int
        The x index
        * a numpy array of ints is also acceptable

    yi : int
        The y index
        * a numpy array of ints is also acceptable

    source : Anything acceptable by loadRaster()
        The contentual raster datasource

    asPoint : bool
        Instruct program to return point geometries instead of x,y coordinates

    Returns
    -------
    * If 'asPoint' is True: ogr.Geometry
    * If 'asPoint' is False: tuple -> (x,y) coordinates
    """
    if source is not None:
        # Get source info
        if not isinstance(source, RasterInfo):
            source = rasterInfo(source)

        xMin = source.xMin
        # xMax = source.xMax
        yMin = source.yMin
        yMax = source.yMax
        dx = source.dx
        dy = source.dy
        yAtTop = source.yAtTop
    else:
        try:
            xMin, yMin, xMax, yMax = bounds
        except:
            xMin, yMin, xMax, yMax = bounds.xyXY

    # Calculate coordinates
    if yAtTop:
        x = xMin + dx * (xi + 0.5)
        y = yMax - dy * (yi + 0.5)
    else:
        x = xMin + dx * (xi + 0.5)
        y = yMin + dy * (yi + 0.5)

    # make the output
    if asPoint:
        try:  # maybe x and y are iterable
            output = [GEOM.point((xx, yy), srs=srs) for xx, yy in zip(x, y)]
        except TypeError:  # x and y should be a single point
            output = GEOM.point((x, y), srs=srs)
    else:
        output = np.column_stack([x, y])

    # Done!
    return output

interpolateValues

interpolateValues(
    source: load_raster_input,
    points: tuple[float, float]
    | Geometry
    | list[tuple[float, float]]
    | list[Geometry]
    | Location
    | LocationSet,
    pointSRS: srs_input = "latlon",
    mode: Literal[
        "near",
        "linear-spline",
        "cubic-spline",
        "average",
        "func",
    ] = "near",
    func: Callable | None = None,
    winRange: int | None = None,
    **kwargs,
)

Interpolates the value of a raster at a given point or collection of points.

Supports various interpolation schemes: 'near', 'linear-spline', 'cubic-spline', 'average', or user-defined

Parameters:

  • source

    (Anything acceptable by loadRaster() or list) –

    The raster datasource, can be a filepath, a raster dataset etc., see RASTER.loadRaster() for details. Alternatively, a list of multiple such raster datasources.

  • points

    ((X, Y) or [(X1, Y1), (X2, Y2), ...] or Location or LocationSet()) –

    Coordinates for the points to extract * All points must be in the same SRS * !REMEMBER! For lat and lon coordinates, X is lon and Y is lat (opposite of what you may think...)

  • pointSRS

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: 'latlon' ) –

    The srs of the point to create * If not given, longitude/latitude is assumed * Only useful when 'points' is not a LocationSet

  • mode

    (str; optional, default: 'near' ) –

    The interpolation scheme to use * options are... "near" - Just gets the nearest value (this is default) "linear-spline" - calculates a linear spline in between points "cubic-spline" - calculates a cubic spline in between points "average" - calculates average across a window "func" - uses user-provided calculator

  • func

    (Callable | None, default: None ) –

    A user defined interpolation function * Only utilized when 'mode' equals "func" * The function must take three arguments in this order... - A 2 dimensional data matrix - A x-index-offset - A y-index-offset * See the example below for more information

  • winRange

    (int, default: None ) –

    The window range (in pixels) to extract the values centered around the closest raster index to the indicated locations. * A winRange of 0 will only extract the closest raster value * A winRange of 1 will extract a window of shape (3,3) * A winRange of 3 will extract a window of shape (7,7) * Only utilized when 'mode' equals "func" * All interpolation schemes have a predefined window range which is appropriate to their use - near -> 0 - linear-spline -> 2 - cubic-spline -> 4 - average -> 3 - func -> 3

Returns:

  • * If only a single location is given: numeric

    namedtuple -> (data : The extracted data at the location xOffset : The X index distance from the location to the center of the closest raster pixel yOffset : The Y index distance from the location to the center of the closest raster pixel inBounds: Flag for whether or not the location is within The raster's bounds )

  • * If Multiple locations are given: numpy.ndrray -> (N,)
    • where N is the number of locations
Example:

"Interpolate" according to the median value in a 5x5 window

def medianFinder( data, xOff, yOff ): return numpy.median(data)

result = interpolateValues( , , mode='func', func=medianFinder, winRange=2)

Source code in geokit/core/raster.py
def interpolateValues(
    source: load_raster_input,
    points: tuple[float, float]
    | ogr.Geometry
    | list[tuple[float, float]]
    | list[ogr.Geometry]
    | Location
    | LocationSet,
    pointSRS: srs_input = "latlon",
    mode: Literal["near", "linear-spline", "cubic-spline", "average", "func"] = "near",
    func: Callable | None = None,
    winRange: int | None = None,
    **kwargs,
):
    """Interpolates the value of a raster at a given point or collection of points.

    Supports various interpolation schemes:
        'near', 'linear-spline', 'cubic-spline', 'average', or user-defined

    Parameters
    ----------
    source : Anything acceptable by loadRaster() or list
        The raster datasource, can be a filepath, a raster dataset etc., see
        RASTER.loadRaster() for details. Alternatively, a list of multiple
        such raster datasources.

    points : (X,Y) or [(X1,Y1), (X2,Y2), ...] or Location or LocationSet()
        Coordinates for the points to extract
        * All points must be in the same SRS
        * !REMEMBER! For lat and lon coordinates, X is lon and Y is lat
          (opposite of what you may think...)

    pointSRS : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the point to create
          * If not given, longitude/latitude is assumed
          * Only useful when 'points' is not a LocationSet

    mode : str; optional
        The interpolation scheme to use
        * options are...
          "near" - Just gets the nearest value (this is default)
          "linear-spline" - calculates a linear spline in between points
          "cubic-spline" - calculates a cubic spline in between points
          "average" - calculates average across a window
          "func" - uses user-provided calculator

    func - function
        A user defined interpolation function
        * Only utilized when 'mode' equals "func"
        * The function must take three arguments in this order...
          - A 2 dimensional data matrix
          - A x-index-offset
          - A y-index-offset
        * See the example below for more information

    winRange : int
        The window range (in pixels) to extract the values centered around the
        closest raster index to the indicated locations.
        * A winRange of 0 will only extract the closest raster value
        * A winRange of 1 will extract a window of shape (3,3)
        * A winRange of 3 will extract a window of shape (7,7)
        * Only utilized when 'mode' equals "func"
        * All interpolation schemes have a predefined window range which is
          appropriate to their use
            - near -> 0
            - linear-spline -> 2
            - cubic-spline -> 4
            - average -> 3
            - func -> 3

    Returns
    -------
    * If only a single location is given: numeric
        namedtuple -> (data : The extracted data at the location
                       xOffset : The X index distance from the location to the
                                 center of the closest raster pixel
                       yOffset : The Y index distance from the location to the
                                 center of the closest raster pixel
                       inBounds: Flag for whether or not the location is within
                                 The raster's bounds
                        )
    * If Multiple locations are given: numpy.ndrray -> (N,)
        - where N is the number of locations

    Example:
    --------
    "Interpolate" according to the median value in a 5x5 window

    >>> def medianFinder( data, xOff, yOff ):
    >>>     return numpy.median(data)
    >>>
    >>> result = interpolateValues( <source>, <points>, mode='func',
    >>>                             func=medianFinder, winRange=2)
    """
    # Determine what the user probably wants as an output
    if isinstance(points, (tuple, ogr.Geometry, Location)):
        asSingle = True
        # make points a list of length 1 so that the rest works (will be unpacked later)
    elif isinstance(points, list):
        if len(points) == 1:
            asSingle = True
        else:
            asSingle = False
    elif isinstance(points, LocationSet):
        if points.count == 1:
            asSingle = True
        else:
            asSingle = False

    else:  # Assume points is already an iterable of some sort
        raise GeoKitRasterError(
            "The following datatype has been provided as point but something different was expected: "
            + str(type(points))
        )

    # Do interpolation
    if mode == "near":
        # Simple get the nearest value
        win = 0 if winRange is None else winRange
        values = extractValues(source, points, pointSRS=pointSRS, winRange=win, _onlyValues=True, **kwargs)
        if asSingle is True:
            result = [values]
        else:
            result = values

    elif mode == "linear-spline":  # use a spline interpolation scheme
        # setup inputs
        win = 2 if winRange is None else winRange
        x = np.linspace(-1 * win, win, 2 * win + 1)
        y = np.linspace(-1 * win, win, 2 * win + 1)

        # get raw data
        values = extractValues(source, points, pointSRS=pointSRS, winRange=win, **kwargs)
        if asSingle is True:
            assert isinstance(values, ptValue)
            data_frame_values = pd.DataFrame(
                dict(
                    dict(data=[values.data], xOffset=values.xOffset, yOffset=values.yOffset, inBounds=values.inBounds),
                ),
                index=None,
            )
        else:
            data_frame_values = values
        # Calculate interpolated values
        result = []
        for v in data_frame_values.itertuples(index=False):
            rbs = RectBivariateSpline(y, x, v.data, kx=1, ky=1)

            result.append(rbs(v.yOffset, v.xOffset)[0][0])

    elif mode == "cubic-spline":  # use a spline interpolation scheme
        # setup inputs
        win = 4 if winRange is None else winRange
        x = np.linspace(-1 * win, win, 2 * win + 1)
        y = np.linspace(-1 * win, win, 2 * win + 1)

        # Get raw data
        values = extractValues(source, points, pointSRS=pointSRS, winRange=win, **kwargs)

        if asSingle is True:
            assert isinstance(values, ptValue)
            data_frame_values = pd.DataFrame(
                dict(
                    dict(data=[values.data], xOffset=values.xOffset, yOffset=values.yOffset, inBounds=values.inBounds),
                ),
                index=None,
            )
        else:
            data_frame_values = values
        # Calculate interpolated values
        result = []
        for v in data_frame_values.itertuples(index=False):
            rbs = RectBivariateSpline(y, x, v.data)

            result.append(rbs(v.yOffset, v.xOffset)[0][0])

    elif mode == "average":  # Get the average in a window
        win = 3 if winRange is None else winRange
        values = extractValues(source, points, pointSRS=pointSRS, winRange=win, **kwargs)
        if asSingle is True:
            assert isinstance(values, ptValue)
            data_frame_values = pd.DataFrame(
                dict(
                    dict(data=[values.data], xOffset=values.xOffset, yOffset=values.yOffset, inBounds=values.inBounds),
                ),
                index=None,
            )
        else:
            data_frame_values = values
        result = []
        for v in data_frame_values.itertuples(index=False):
            result.append(v.data.mean())

    elif mode == "func":  # Use a general function processor
        if func is None:
            raise GeoKitRasterError("'func' mode chosen, but no func kwargs was given")
        win = 3 if winRange is None else winRange
        values = extractValues(source, points, pointSRS=pointSRS, winRange=win, **kwargs)
        if asSingle is True:
            assert isinstance(values, ptValue)
            data_frame_values = pd.DataFrame(
                dict(
                    dict(data=[values.data], xOffset=values.xOffset, yOffset=values.yOffset, inBounds=values.inBounds),
                ),
                index=None,
            )
        else:
            data_frame_values = values
        result = []
        for v in data_frame_values.itertuples(index=False):
            result.append(func(v.data, v.xOffset, v.yOffset))

    else:
        raise GeoKitRasterError("Interpolation mode not understood: ", mode)

    # Done!
    if asSingle:
        return result[0]
    else:
        return np.array(result)

isRaster

isRaster(source)

Test if loadRaster fails for the given input.

Parameters:

  • source

    (str) –

    The path to the raster file to load

Returns:

  • bool -> True if the given input is a raster
Source code in geokit/core/util.py
def isRaster(source):
    """
    Test if loadRaster fails for the given input.

    Parameters
    ----------
    source : str
        The path to the raster file to load

    Returns
    -------
    bool -> True if the given input is a raster
    """
    if isinstance(source, gdal.Dataset):
        try:
            if source.GetLayerCount() == 0:
                return True  # This should always be true?
            else:
                return False
        except:
            return False
    elif isinstance(source, str):
        d = gdal.IdentifyDriver(source)

        meta = d.GetMetadata()
        if meta.get("DCAP_RASTER", False) == "YES":
            return True
        else:
            return False
    else:
        return False

isVector

isVector(source)

Test if loadVector fails for the given input.

Parameters:

  • source

    (str) –

    The path to the vector file to load

Returns:

  • bool -> True if the given input is a vector
Source code in geokit/core/util.py
def isVector(source):
    """
    Test if loadVector fails for the given input.

    Parameters
    ----------
    source : str
        The path to the vector file to load

    Returns
    -------
    bool -> True if the given input is a vector
    """
    if isinstance(source, gdal.Dataset):
        if source.GetLayerCount() > 0:
            return True
        else:
            return False
    elif isinstance(source, str):
        d = gdal.IdentifyDriver(source)

        meta = d.GetMetadata()
        if meta.get("DCAP_VECTOR", False) == "YES":
            return True
        else:
            return False
    else:
        return False

line

line(points, srs=4326)

Creates an OGR Line object from a given set of points.

Parameters:

  • Points

    ([(x,y), ], Nx2 numpy.ndarray or list of osgeo.ogr.Geometry points.) –

    The points defining the line

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: 4326 ) –

    The srs of the line to create

Returns:

  • Geometry
Source code in geokit/core/geom.py
def line(points, srs=4326):
    """Creates an OGR Line object from a given set of points.

    Parameters
    ----------
    Points : [(x,y), ], Nx2 numpy.ndarray or list of osgeo.ogr.Geometry points.
        The points defining the line

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the line to create

    Returns
    -------
    ogr.Geometry
    """
    # Make the complete geometry
    g = ogr.Geometry(ogr.wkbLineString)
    if not srs is None:
        g.AssignSpatialReference(SRS.loadSRS(srs))

    # Make the line
    if all([isinstance(p, ogr.Geometry) for p in points]):
        # convert points into a list of coordinate tuples in correct srs
        points = [(transform(p, toSRS=srs).GetX(), transform(p, toSRS=srs).GetY()) for p in points]
    [g.AddPoint(x, y) for x, y in points]
    # g.AddGeometry(otr)

    # Ensure valid
    if not g.IsValid():
        raise GeoKitGeomError("Polygon is invalid")

    # Done!
    return g

listLayers

listLayers(source)

Returns the layer names for each layer that is stored in a geopackage.

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector datasource to read from

Returns:

  • list

    A list of layer names for the source geopackage.

Source code in geokit/core/vector.py
def listLayers(
    source,
):
    """Returns the layer names for each layer that is stored in a geopackage.

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector datasource to read from

    Returns
    -------
    list
        A list of layer names for the source geopackage.
    """
    layer_names = []
    ds = loadVector(source)

    # Loop over the layers to get their names.
    for i in range(ds.GetLayerCount()):
        name = ds.GetLayer(i).GetName()
        layer_names.append(name)
    return layer_names

loadRaster

loadRaster(source: load_raster_input, mode=0) -> Dataset

Load a raster dataset from a path to a file on disc.

Parameters:

  • source

    (str or Dataset) –
    • If a string is given, it is assumed as a path to a raster file on disc
    • If a gdal.Dataset is given, it is assumed to already be an open raster and is returned immediately

Returns:

  • Dataset
Source code in geokit/core/raster.py
def loadRaster(source: load_raster_input, mode=0) -> gdal.Dataset:
    """
    Load a raster dataset from a path to a file on disc.

    Parameters
    ----------
    source : str or gdal.Dataset
        * If a string is given, it is assumed as a path to a raster file on disc
        * If a gdal.Dataset is given, it is assumed to already be an open raster
          and is returned immediately

    Returns
    -------
    gdal.Dataset
    """
    if isinstance(source, pathlib.Path):
        source = str(source)
    if isinstance(source, str):
        ds = gdal.Open(source, mode)
    else:
        ds = source

    if ds is None:
        raise GeoKitRasterError("Could not load input dataSource: ", str(source))
    return ds

loadSRS

loadSRS(
    source: srs_input,
    geom: Geometry | None = None,
    **kwargs,
) -> SpatialReference

Load a spatial reference system (SRS) from various sources.

Parameters:

  • source

    (str | int | SpatialReference | None) –

    The SRS to load. Can be: * osr.SpatialReference object * EPSG integer ID * Standardized SRS string like 'EPSG:4326' * Common SRS name in SRSCOMMON (e.g. 'europe_laea') * WKT string * 'laea' — creates a Lambert Azimuthal Equal Area projection (optionally centered on a geometry)

  • geom

    (Geometry, default: None ) –

    Geometry to center the projection on if 'laea' is used.

  • **kwargs

    Passed directly to SRSCOMMON.laea(**kwargs), allowing customization such as lon, lat, or name.

Returns:

  • SpatialReference
Source code in geokit/core/srs.py
def loadSRS(source: srs_input, geom: ogr.Geometry | None = None, **kwargs) -> osr.SpatialReference:
    """
    Load a spatial reference system (SRS) from various sources.

    Parameters
    ----------
    source : str | int | osr.SpatialReference | None
        The SRS to load. Can be:
          * osr.SpatialReference object
          * EPSG integer ID
          * Standardized SRS string like 'EPSG:4326'
          * Common SRS name in SRSCOMMON (e.g. 'europe_laea')
          * WKT string
          * 'laea' — creates a Lambert Azimuthal Equal Area projection
            (optionally centered on a geometry)

    geom : osgeo.ogr.Geometry, optional
        Geometry to center the projection on if 'laea' is used.

    **kwargs :
        Passed directly to `SRSCOMMON.laea(**kwargs)`, allowing
        customization such as `lon`, `lat`, or `name`.

    Returns
    -------
    osr.SpatialReference
    """
    # Do initial check of source
    if isinstance(source, osr.SpatialReference):
        return source
    elif source is None:
        return None

    # Create an empty SRS
    srs = osr.SpatialReference()

    # Check if source is a string
    if isinstance(source, str):
        src_upper = source.strip().upper()

        # Handle special LAEA case
        if src_upper == "LAEA":
            # merge geom into kwargs if provided
            if geom is not None and "geom" not in kwargs:
                kwargs["geom"] = geom
            return SRSCOMMON.laea(**kwargs)

        elif hasattr(SRSCOMMON, source):
            # assume a name for one of the common SRS's was given
            srs = SRSCOMMON[source]
        else:
            try:
                # try handling as a standardized epsg or esri etc. code
                srs = osr.SpatialReference()
                _val = srs.SetFromUserInput(source)
                assert _val == 0
            except:
                srs.ImportFromWkt(source)  # assume a Wkt string was input
    elif isinstance(source, int):
        srs.ImportFromEPSG(source)
    else:
        raise GeoKitSRSError("Unknown srs source type: ", type(source))

    if gdal.__version__ >= "3.0.0":
        srs.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)

    # assert that the srs is valid (may be invalid if e.g. wrong integer codes were passed)
    if srs.Validate() != 0:
        raise RuntimeError(f"Created srs is invalid.")

    return srs

loadVector

loadVector(x: load_vector_input) -> Dataset

Load a vector dataset from a path to a file on disc.

Parameters:

  • source

    (str or Dataset) –
    • If a string is given, it is assumed as a path to a vector file on disc
    • If a gdal.Dataset is given, it is assumed to already be an open vector and is returned immediately

Returns:

  • Dataset
Source code in geokit/core/vector.py
def loadVector(x: load_vector_input) -> gdal.Dataset:
    """
    Load a vector dataset from a path to a file on disc.

    Parameters
    ----------
    source : str or gdal.Dataset
        * If a string is given, it is assumed as a path to a vector file on disc
        * If a gdal.Dataset is given, it is assumed to already be an open vector
          and is returned immediately

    Returns
    -------
    gdal.Dataset
    """
    if isinstance(x, str) or isinstance(x, pathlib.Path):
        if not os.path.exists(x):
            raise FileNotFoundError(f"Vector file, directory, or resource not found: {x}")

        # Since we know the path exists, try to open it.
        ds = gdal.OpenEx(x)
        if ds is None:
            # This error now clearly means path exists, but GDAL failed to open it or the file is corrupted.
            raise GeoKitVectorError(f"Could not load input dataSource: {x}")

    elif x is None:
        # Raise and error if 'None' is passed as the object
        raise GeoKitVectorError("Input dataSource cannot be None.")

    elif isinstance(x, gdal.Dataset):
        # If it is an already-opened GDAL Dataset object.
        ds = x

    else:
        # Handle any other invalid type
        raise TypeError(f"Invalid input type: Expected str, or gdal.Dataset, got {type(x)}")

    return ds

makeBox

makeBox(*args, **kwargs)

Alias for geokit.geom.box(...).

Source code in geokit/core/geom.py
def makeBox(*args, **kwargs):
    """Alias for geokit.geom.box(...)."""
    msg = "makeBox will be removed soon. Switch to 'box'"
    warnings.warn(msg, Warning)
    return box(*args, **kwargs)

makeEmpty

makeEmpty(*args, **kwargs)

Alias for geokit.geom.empty(...).

Source code in geokit/core/geom.py
def makeEmpty(*args, **kwargs):
    """Alias for geokit.geom.empty(...)."""
    msg = "makeEmpty will be removed soon. Switch to 'empty'"
    warnings.warn(msg, Warning)
    return empty(*args, **kwargs)

makeLine

makeLine(*args, **kwargs)

Alias for geokit.geom.line(...).

Source code in geokit/core/geom.py
def makeLine(*args, **kwargs):
    """Alias for geokit.geom.line(...)."""
    msg = "makeLine will be removed soon. Switch to 'line'"
    warnings.warn(msg, Warning)
    return line(*args, **kwargs)

makePoint

makePoint(*args, **kwargs)

Alias for geokit.geom.point(...).

Source code in geokit/core/geom.py
def makePoint(*args, **kwargs):
    """Alias for geokit.geom.point(...)."""
    msg = "makePoint will be removed soon. Switch to 'point'"
    warnings.warn(msg, Warning)
    return point(*args, **kwargs)

makePolygon

makePolygon(*args, **kwargs)

Alias for geokit.geom.polygon(...).

Source code in geokit/core/geom.py
def makePolygon(*args, **kwargs):
    """Alias for geokit.geom.polygon(...)."""
    msg = "makePolygon will be removed soon. Switch to 'polygon'"
    warnings.warn(msg, Warning)
    return polygon(*args, **kwargs)

mutateRaster

mutateRaster(
    source: load_raster_input,
    processor: Callable | None = None,
    bounds: tuple[numeric, numeric, numeric, numeric]
    | None = None,
    boundsSRS: srs_input = "latlon",
    autocorrect: bool = False,
    output: str | None = None,
    dtype: geokit_c_data_types_literal | None = None,
    **create_raster_kwargs,
)

Process all pixels in a raster according to a given function. The boundaries of the resulting raster can be changed as long as the new boundaries are within the scope of the original raster, but the resolution cannot.

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource

  • processor

    (Callable | None, default: None ) –

    The function performing the mutation of the raster's data * The function will take single argument (a 2D numpy.ndarray) * The function must return a numpy.ndarray of the same size as the input * The return type must also be containable within a Float32 (int and boolean is okay) * See example below for more info

  • bounds

    (tuple[numeric, numeric, numeric, numeric] | None, default: None ) –

    The boundary to clip the raster to before mutating * If given as an Extent, the extent is always cast to the source's native srs before mutating * If given as a tuple, (xMin, yMin, xMax, yMax) is expected - Units must be in the srs specified by 'boundsSRS' * This boundary must fit within the boundary of the rasters source * The boundary is always fitted to the source's grid, so the returned values do not necessarily match to the boundary which is provided

  • boundsSRS

    (srs_input, default: 'latlon' ) –

    The srs of the 'bounds' argument * This is ignored if the 'bounds' argument is an Extent object or is None

  • autocorrect

    (bool; optional, default: False ) –

    If True, then before mutating the matrix extracted from the source will have pixels equal to its 'noData' value converted to numpy.nan * Data type will always result in a float, so be careful with large matrices

  • output

    (str; optional, default: None ) –

    A path to an output file * If output is None, the raster will be created in memory and a dataset handle will be returned * If output is given, the raster will be written to disk and nothing will be returned

  • dtype

    (Type, str, or numpy-dtype; optional, default: None ) –

    If given, forces the processed data to be a particular datatype * Example - A python numeric type such as bool, int, or float - A Numpy datatype such as numpy.uint8 or numpy.float64 - a String such as "Byte", "UInt16", or "Double"

  • **kwargs

    • All kwargs are passed on to a call to createRaster()
Example:

If you wanted to assign suitability factors based on a raster containing integer identifiers

def calcSuitability( data ): # create an output matrix outputMatrix = numpy.zeros( data.shape )

# do the processing
outputMatrix[ data == 1 ] = 0.1
outputMatrix[ data == 2 ] = 0.2
outputMatrix[ data == 10] = 0.4
outputMatrix[ np.logical_and(data > 15, data < 20)  ] = 0.5

# return the output matrix
return outputMatrix

result = processRaster( , processor=calcSuitability )

Source code in geokit/core/raster.py
def mutateRaster(
    source: load_raster_input,
    processor: Callable | None = None,
    bounds: tuple[numeric, numeric, numeric, numeric] | None = None,
    boundsSRS: srs_input = "latlon",
    autocorrect: bool = False,
    output: str | None = None,
    dtype: geokit_c_data_types_literal | None = None,
    **create_raster_kwargs,
):
    """Process all pixels in a raster according to a given function. The boundaries
    of the resulting raster can be changed as long as the new boundaries are within
    the scope of the original raster, but the resolution cannot.

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource

    processor: function; optional
        The function performing the mutation of the raster's data
        * The function will take single argument (a 2D numpy.ndarray)
        * The function must return a numpy.ndarray of the same size as the input
        * The return type must also be containable within a Float32 (int and
          boolean is okay)
        * See example below for more info

    bounds: tuple or Extent
        The boundary to clip the raster to before mutating
        * If given as an Extent, the extent is always cast to the source's native
          srs before mutating
        * If given as a tuple, (xMin, yMin, xMax, yMax) is expected
            - Units must be in the srs specified by 'boundsSRS'
        * This boundary must fit within the boundary of the rasters source
        * The boundary is always fitted to the source's grid, so the returned
          values do not necessarily match to the boundary which is provided

    boundsSRS: Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the 'bounds' argument
        * This is ignored if the 'bounds' argument is an Extent object or is None

    autocorrect : bool; optional
        If True, then before mutating the matrix extracted from the source will have
        pixels equal to its 'noData' value converted to numpy.nan
        * Data type will always result in a float, so be careful with large
          matrices

    output : str; optional
        A path to an output file
        * If output is None, the raster will be created in memory and a dataset
          handle will be returned
        * If output is given, the raster will be written to disk and nothing will
          be returned

    dtype : Type, str, or numpy-dtype; optional
        If given, forces the processed data to be a particular datatype
        * Example
          - A python numeric type  such as bool, int, or float
          - A Numpy datatype such as numpy.uint8 or numpy.float64
          - a String such as "Byte", "UInt16", or "Double"

    **kwargs:
        * All kwargs are passed on to a call to createRaster()

    Example:
    --------
    If you wanted to assign suitability factors based on a raster containing
    integer identifiers

    >>> def calcSuitability( data ):
    >>>     # create an output matrix
    >>>     outputMatrix = numpy.zeros( data.shape )
    >>>
    >>>     # do the processing
    >>>     outputMatrix[ data == 1 ] = 0.1
    >>>     outputMatrix[ data == 2 ] = 0.2
    >>>     outputMatrix[ data == 10] = 0.4
    >>>     outputMatrix[ np.logical_and(data > 15, data < 20)  ] = 0.5
    >>>
    >>>     # return the output matrix
    >>>     return outputMatrix
    >>>
    >>> result = processRaster( <source-path>, processor=calcSuitability )
    """
    # open the dataset and get SRS
    workingDS = loadRaster(source)

    # Get ds info
    dsInfo = rasterInfo(workingDS)

    # Read data into array
    sourceData, bounds = extractMatrix(
        source,
        bounds=bounds,
        boundsSRS=boundsSRS,
        autocorrect=autocorrect,
        returnBounds=True,
    )
    workingExtent = dsInfo.bounds if (bounds is None) else bounds

    # Perform processing
    if processor:
        processedData = processor(sourceData)

    else:
        processedData = sourceData

    # Ensure returned matrix is okay
    if processedData.shape != sourceData.shape:
        raise GeoKitRasterError(
            "Processed matrix does not have the correct shape \nIs {0} \nShould be {1}",
            format(processedData.shape, sourceData.shape),
        )
    del sourceData
    list_of_numbers = [processedData.min(), processedData.max()]
    minimum_gdal_type_list = [str(processedData.dtype)]
    if isinstance(dtype, str):
        minimum_gdal_type_list.append(dtype)

    gdal_data_string = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_string(
        list_of_numbers=list_of_numbers, minimum_gdal_type_list=minimum_gdal_type_list
    )
    # Create an output raster
    if output is None:
        return UTIL.quickRaster(
            dy=dsInfo.dy,
            dx=dsInfo.dx,
            bounds=workingExtent,
            dtype=gdal_data_string,
            srs=dsInfo.srs,
            data=processedData,
            **create_raster_kwargs,
        )

    else:
        createRaster(
            pixelHeight=dsInfo.dy,
            pixelWidth=dsInfo.dx,
            bounds=workingExtent,
            srs=dsInfo.srs,
            data=processedData,
            output=output,
            dtype=gdal_data_string,
            **create_raster_kwargs,
        )

        return output

mutateVector

mutateVector(
    source,
    processor=None,
    srs=None,
    geom=None,
    where=None,
    fieldDef=None,
    output=None,
    keepAttributes=True,
    _slim: bool = False,
    **create_vector_kwargs,
) -> None | Dataset | str

Process a vector dataset according to an arbitrary function.

Note:

If this is called without a processor, it simply clips the vector source to the selection criteria given by 'geom' and 'where' as well as translates all geometries to 'srs'

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector datasource to read from

  • processor

    (function; optional, default: None ) –

    The function which mutates each feature * If no function is given, this stage is skipped * The function will take 1 arguments: a pandas.Series object containing one 'geom' key indicating the geometry and the other keys indicating attributes * The function must return something understandable by pd.Series containing the geometry under the index 'geom' and any other keys - These will be used to update the old geometries and attributes * The attribute dictionary's values should only be numerics and strings * See example below for more info

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the vector to create * If not given, the source's inherent srs is used * If the given SRS is different from the source's SRS, all feature geometries will be cast to the given SRS before processing

  • geom

    (ogr.Geometry; optional, default: None ) –

    The geometry to search within * All features are extracted which touch this geometry

  • where

    (str; optional, default: None ) –

    An SQL-like where statement to apply to the source * Feature attribute name do not need quotes * String values should be wrapped in 'single quotes' Example: If the source vector has a string attribute called "ISO" and a integer attribute called "POP", you could use....

    where = "ISO='DEU' AND POP>1000"
    
  • fieldDef

    (dict; optional, default: None ) –

    A dictionary specifying the datatype of each attribute when written into the final dataset * Options are defined from ogr.OFT[...] - ex. Integer, Real, String * The ogrType() function can be used to map typical python and numpy types to appropriate ogr types

  • output

    (str; optional, default: None ) –

    A path on disk to create the output vector * If output is None, the vector dataset will be created in memory * Assumed to be of "ESRI Shapefile" format * Will create a number of files with different extensions

  • keepAttributes

    (bool; optional, default: True ) –

    If True, the old attributes will be kept in the output vector * Unless they are over written by the processor If False, only the newly specified attributes are kept

  • _slim

    (bool, default: False ) –

Returns:

  • * If 'output' is None: gdal.Dataset
  • * If 'output' is given: None
Example:

Say you had a vector source which contains point geometries, and where each feature also had an float-attribute called "value". You want to create a new vector set wherein you have circle geometries at the same locations as the original points and whose radius is equal to the original features' "value" attribute. Furthermore, let's say you only want to do this for feature's who's "value" attribute is greater than zero and less than 10. Do as such...

def growPoints( row ): # Create a new geom newGeom = row.geom.Buffer(row.radius)

# Return the new geometry/attribute set
return { 'geom':newGeom }

result = processVector( , where="value>0 AND value<10", processor=growPoints )

Source code in geokit/core/vector.py
def mutateVector(
    source,
    processor=None,
    srs=None,
    geom=None,
    where=None,
    fieldDef=None,
    output=None,
    keepAttributes=True,
    _slim: bool = False,
    **create_vector_kwargs,
) -> None | gdal.Dataset | str:
    """Process a vector dataset according to an arbitrary function.

    Note:
    -----
    If this is called without a processor, it simply clips the vector source to
    the selection criteria given by 'geom' and 'where' as well as translates
    all geometries to 'srs'

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector datasource to read from

    processor : function; optional
        The function which mutates each feature
        * If no function is given, this stage is skipped
        * The function will take 1 arguments: a pandas.Series object containing
          one 'geom' key indicating the geometry and the other keys indicating
          attributes
        * The function must return something understandable by pd.Series
          containing the geometry under the index 'geom' and any other keys
            - These will be used to update the old geometries and attributes
        * The attribute dictionary's values should only be numerics and strings
        * See example below for more info

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the vector to create
          * If not given, the source's inherent srs is used
          * If the given SRS is different from the source's SRS, all feature
            geometries will be cast to the given SRS before processing

    geom : ogr.Geometry; optional
        The geometry to search within
        * All features are extracted which touch this geometry

    where : str; optional
        An SQL-like where statement to apply to the source
        * Feature attribute name do not need quotes
        * String values should be wrapped in 'single quotes'
        Example: If the source vector has a string attribute called "ISO" and
                 a integer attribute called "POP", you could use....

            where = "ISO='DEU' AND POP>1000"

    fieldDef : dict; optional
        A dictionary specifying the datatype of each attribute when written into
        the final dataset
        * Options are defined from ogr.OFT[...]
          - ex. Integer, Real, String
        * The ogrType() function can be used to map typical python and numpy types
          to appropriate ogr types

    output : str; optional
        A path on disk to create the output vector
        * If output is None, the vector dataset will be created in memory
        * Assumed to be of "ESRI Shapefile" format
        * Will create a number of files with different extensions

    keepAttributes : bool; optional
        If True, the old attributes will be kept in the output vector
            * Unless they are over written by the processor
        If False, only the newly specified attributes are kept

    _slim: bool

    Returns
    -------
    * If 'output' is None: gdal.Dataset
    * If 'output' is given: None

    Example:
    --------
    Say you had a vector source which contains point geometries, and where each
    feature also had an float-attribute called "value". You want to create a new
    vector set wherein you have circle geometries at the same locations as the
    original points and whose radius is equal to the original features' "value"
    attribute. Furthermore, let's say you only want to do this for feature's who's
    "value" attribute is greater than zero and less than 10. Do as such...

    >>> def growPoints( row ):
    >>>     # Create a new geom
    >>>     newGeom = row.geom.Buffer(row.radius)
    >>>
    >>>     # Return the new geometry/attribute set
    >>>     return { 'geom':newGeom }
    >>>
    >>> result = processVector( <source-path>, where="value>0 AND value<10",
    >>>                         processor=growPoints )
    >>>
    """
    # Extract filtered features
    geoms = extractFeatures(source, geom=geom, where=where, srs=srs)
    if geoms.size == 0:
        return None

    # Hold on to the SRS in case we need it
    if srs is None:
        vecds = loadVector(source)
        veclyr = vecds.GetLayer()
        srs = veclyr.GetSpatialRef()

    # Do processing
    if not processor is None:
        result = geoms.apply(lambda x: pd.Series(processor(x)), axis=1)
        if keepAttributes:
            for c in result.columns:
                geoms[c] = result[c].values
        else:
            geoms = result

        if not "geom" in geoms:
            raise GeoKitVectorError("There is no 'geom' field in the resulting vector table")

        # make sure the geometries have an srs
        if not geoms.geom[0].GetSpatialReference():
            srs = SRS.loadSRS(srs)
            geoms.geom.apply(lambda x: x.AssignSpatialReference(srs))

    # Create a new shapefile from the results

    if _slim is True:
        return createVector(geoms.geom)
    else:
        return createVector(geoms, srs=srs, output=output, fieldDef=fieldDef, **create_vector_kwargs)

ogrType

ogrType(s)

Tries to determine the corresponding OGR type according to the input.

Source code in geokit/core/vector.py
def ogrType(s):
    """Tries to determine the corresponding OGR type according to the input."""
    if isinstance(s, str):
        if hasattr(ogr, s):
            return s
        elif s.lower() in _ogrStrToType:
            return _ogrStrToType[s.lower()]
        elif hasattr(ogr, "OFT%s" % s):
            return "OFT%s" % s
        return "OFTString"

    elif s is str:
        return "OFTString"
    elif isinstance(s, np.dtype):
        return ogrType(str(s))
    elif isinstance(s, np.generic):
        return ogrType(s.dtype)
    elif s is bool:
        return "OFTInteger"
    elif s is int:
        return "OFTInteger64"
    elif isinstance(s, int):
        return _ogrIntToType[s]
    elif s is float:
        return "OFTReal"
    elif isinstance(s, Iterable):
        return ogrType(s[0])

    raise ValueError("OGR type could not be determined")

partition

partition(geom, targetArea, growStep=None, _startPoint=0)

Partition a Polygon into some number of pieces whose areas should be close to the targetArea.

WARNING: Not tested for several version. Will probably be removed later

Inputs: geom : The geometry to partition - a single ogr Geometry object of POLYGON type

targetArea - float : The ideal area of each partition
    * Most of the geometries will be around this area, but they can also be anywhere in the range 0 and 2x

growStep - float : The incremental buffer to add while searching for a suitable partition
    * Choose carefully!
        - A large growStep will make the algorithm run faster
        - A small growStep will produce a more accurate result
    * If no growStep is given, a decent one will be calculated
Source code in geokit/core/geom.py
def partition(geom, targetArea, growStep=None, _startPoint=0):
    """Partition a Polygon into some number of pieces whose areas should be close
    to the targetArea.

    WARNING: Not tested for several version. Will probably be removed later

    Inputs:
        geom : The geometry to partition
            - a single ogr Geometry object of POLYGON type

        targetArea - float : The ideal area of each partition
            * Most of the geometries will be around this area, but they can also be anywhere in the range 0 and 2x

        growStep - float : The incremental buffer to add while searching for a suitable partition
            * Choose carefully!
                - A large growStep will make the algorithm run faster
                - A small growStep will produce a more accurate result
            * If no growStep is given, a decent one will be calculated
    """
    if growStep is None:
        growStep = np.sqrt(targetArea / np.pi) / 2

    # Be sure we are working with a polygon
    if geom.GetGeometryName() == "POLYGON":
        pass
    elif geom.GetGeometryName() == "MULTIPOLYGON":
        results = []
        for gi in range(geom.GetGeometryCount()):
            tmpResults = partition(geom.GetGeometryRef(gi), targetArea, growStep)
            results.extend(tmpResults)

        return results
    else:
        raise GeoKitGeomError("Geometry is not a polygon or multipolygon object")

    # Check the geometry's size
    gArea = geom.Area()
    if gArea < 1.5 * targetArea:
        return [
            geom.Clone(),
        ]

    # Find the most starting boundary coordinate
    boundary = geom.Boundary()
    if boundary.GetGeometryCount() == 0:
        coords = boundary.GetPoints()
    else:
        coords = boundary.GetGeometryRef(0).GetPoints()

    y = np.array([c[1] for c in coords])
    x = np.array([c[0] for c in coords])

    if _startPoint == 0:  # start from the TOP-LEFT
        yStart = y.max()
        xStart = x[y == yStart].min()
    elif _startPoint == 1:  # Start from the LEFT-TOP
        xStart = x.min()
        yStart = y[x == xStart].max()
    elif _startPoint == 2:  # Start from the RIGHT-TOP
        xStart = x.max()
        yStart = y[x == xStart].max()
    elif _startPoint == 3:  # Start from the BOT-RIGHT
        yStart = y.min()
        xStart = x[y == yStart].max()
    elif _startPoint == 4:  # Start from the BOT-LEFT
        yStart = y.min()
        xStart = x[y == yStart].min()
    elif _startPoint == 5:  # Start from the LEFT-BOT
        xStart = x.min()
        yStart = y[x == xStart].min()
    else:
        raise GeoKitGeomError("Start point failure. There may be an infinite loop in one of the geometries")

    start = point(xStart, yStart, srs=geom.GetSpatialReference())

    # start searching
    tmp = start.Buffer(float(growStep))
    tmp.Simplify(growStep)
    searchGeom = tmp.Intersection(geom)
    sgArea = searchGeom.Area()

    if gArea < 2 * targetArea:  # use a slightly smalled target area when the whole geometry
        #  is close to twice the target area in order to increase the
        #  likelihood of a usable leftover
        workingTarget = 0.9 * targetArea
    else:
        workingTarget = targetArea

    while sgArea < workingTarget:
        tmp = searchGeom.Buffer(float(growStep))
        tmp.Simplify(growStep)
        newGeom = tmp.Intersection(geom)
        newArea = newGeom.Area()

        if newArea > workingTarget * 1.1:
            dA = (newArea - sgArea) / growStep
            weightedGrowStep = (workingTarget - sgArea) / dA

            tmp = start.Buffer(float(weightedGrowStep))
            tmp.Simplify(growStep)
            searchGeom = tmp.Intersection(geom)
            break

        searchGeom = newGeom
        sgArea = newArea

    # fix the search geometry
    #  - For some reason the searchGeometry will sometime create a geometry and a linestring,
    #    in these cases the real geometry was always the second item...
    if searchGeom.GetGeometryName() == "GEOMETRYCOLLECTION":
        for gi in range(searchGeom.GetGeometryCount()):
            g = searchGeom.GetGeometryRef(gi)
            if g.GetGeometryName() == "POLYGON":
                searchGeom = g.Clone()
                break

    # Check the left over geometry, maybe some poops have been created and we need to glue them together
    outputGeom = searchGeom.Simplify(growStep / 20)
    geomToDo = []

    leftOvers = geom.Difference(searchGeom)
    if leftOvers.GetGeometryName() == "MULTIPOLYGON":
        for gi in range(leftOvers.GetGeometryCount()):
            leftOver = leftOvers.GetGeometryRef(gi)
            if leftOver.Area() < targetArea * 0.5:
                outputGeom = outputGeom.Union(leftOver)
            else:
                geomToDo.append(leftOver)
    elif leftOvers.GetGeometryName() == "POLYGON":
        geomToDo.append(leftOvers)
    else:
        raise GeoKitGeomError("FATAL ERROR: Difference did not result in a polygon")

    # make an output array
    if outputGeom.Area() < targetArea * 1.5:
        output = [outputGeom]
    else:
        # the search geom plus some (or maybe all) of the left over poops total an area which is too large,
        #  so it will need recomputing. But in order to decrease the liklihhod of an infinite loop,
        #  use a difference starting point than the one used before
        #  - This will loop a maximum of 6 times before an exception is raised
        output = partition(outputGeom, targetArea, growStep, _startPoint + 1)

    for g in geomToDo:
        tmpOutput = partition(g, targetArea, growStep)
        output.extend(tmpOutput)

    # Done!
    return output

point

point(*args, srs: srs_input = 'latlon')

Make a simple point geometry.

Parameters:

  • *args

    ((numeric, numeric or (numeric, numeric)), default: () ) –

    The X and Y coordinate of the point to create

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: 'latlon' ) –

    The srs of the point to create * If not given, longitude/latitude is assumed * srs MUST be given as a keyword argument

Returns:

  • Geometry
Example:

point(x, y [,srs]) point( (x, y) [,srs] )

Source code in geokit/core/geom.py
def point(*args, srs: srs_input = "latlon"):
    """Make a simple point geometry.

    Parameters
    ----------
    *args : numeric, numeric or (numeric, numeric)
        The X and Y coordinate of the point to create

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the point to create
          * If not given, longitude/latitude is assumed
          * srs MUST be given as a keyword argument

    Returns
    -------
    ogr.Geometry

    Example:
    --------
    point(x, y [,srs])
    point( (x, y) [,srs] )
    """
    if len(args) == 1:
        x, y = args[0]
    elif len(args) == 2:
        x = args[0]
        y = args[1]
    else:
        raise GeoKitGeomError('Too many positional inputs. Did you mean to specify "srs="?')

    """make a point geometry from given coordinates (x,y) and srs"""
    pt = ogr.Geometry(ogr.wkbPoint)
    pt.AddPoint(float(x), float(y))
    if not srs is None:
        pt.AssignSpatialReference(SRS.loadSRS(srs))
    return pt

polygon

polygon(
    outerRing, *args, srs: srs_input | None = "default"
)

Creates an OGR Polygon object from a given set of points.

Parameters:

  • outerRing

    ([(x,y), ] or [ogr.Geometry, ] or Nx2 numpy.ndarray) –

    The polygon's outer edge

  • *args

    ([(x,y), ] or [ogr.Geometry, ] or Nx2 numpy.ndarray, default: () ) –

    The inner edges of the polygon * Each input forms a single edge * Inner rings cannot interset the outer ring or one another * NOTE! For proper drawing in matplotlib, inner rings must be given in the opposite orientation as the outer ring (clockwise vs counterclockwise)

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: 'default' ) –

    The srs of the polygon to create. By default "default", i.e. if point geometries are passed, srs will be extracted from first point of outer ring, if points are passed as (x, y) tuples, EPSG:4326 will be assigned by default unless given otherwise. If given as None, no srs will be assigned

Returns:

  • Geometry
Example:

Make a diamond cut out of a box...

box = [(-2,-2), (-2,2), (2,2), (2,-2), (-2,-2)] diamond = [(0,1), (-0.5,0), (0,-1), (0.5,0), (0,1)]

geom = polygon( box, diamond )

Source code in geokit/core/geom.py
def polygon(outerRing, *args, srs: srs_input | None = "default"):
    """Creates an OGR Polygon object from a given set of points.

    Parameters
    ----------
    outerRing : [(x,y), ] or [ogr.Geometry, ] or Nx2 numpy.ndarray
        The polygon's outer edge

    *args : [(x,y), ] or [ogr.Geometry, ] or Nx2 numpy.ndarray
        The inner edges of the polygon
          * Each input forms a single edge
          * Inner rings cannot interset the outer ring or one another
          * NOTE! For proper drawing in matplotlib, inner rings must be given in
            the opposite orientation as the outer ring (clockwise vs
            counterclockwise)

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the polygon to create. By default "default", i.e. if
        point geometries are passed, srs will be extracted from first point of outer ring,
        if points are passed as (x, y) tuples, EPSG:4326 will be assigned
        by default unless given otherwise. If given as None, no srs will be assigned

    Returns
    -------
    ogr.Geometry

    Example:
    --------
    Make a diamond cut out of a box...

      box = [(-2,-2), (-2,2), (2,2), (2,-2), (-2,-2)]
      diamond = [(0,1), (-0.5,0), (0,-1), (0.5,0), (0,1)]

      geom = polygon( box, diamond )
    """
    # check if we have all point geometries
    pointGeometries = all([isinstance(_p, ogr.Geometry) for _p in outerRing])
    if srs == "default":
        if pointGeometries:
            # we have geometries, set srs to the srs of the first outer ring point
            srs = outerRing[0].GetSpatialReference()
        else:
            # set srs to EPSG:4326 as standard
            srs = SRS.loadSRS(4326)
    elif srs is None:
        pass
    else:
        srs = SRS.loadSRS(srs)

    # Make the complete geometry
    g = ogr.Geometry(ogr.wkbPolygon)
    if not srs is None:
        g.AssignSpatialReference(srs)

    # Make the outer ring
    otr = ogr.Geometry(ogr.wkbLinearRing)
    if not srs is None:
        otr.AssignSpatialReference(srs)
    # convert to tuples if we have point geometries at hand
    if pointGeometries:
        outerRing = [(_p.GetX(), _p.GetY()) for _p in outerRing]
    else:
        assert all([isinstance(x, tuple) for x in outerRing]), (
            f"All outerRing entries must be (x,y) tuples in given (or default) srs."
        )
    [otr.AddPoint(float(x), float(y)) for x, y in outerRing]
    g.AddGeometry(otr)

    # Make the inner rings (maybe)
    for innerRing in args:
        # extract (x, y) tuples first if needed
        if all([isinstance(_p, ogr.Geometry) for _p in innerRing]):
            innerRing = [(_p.GetX(), _p.GetY()) for _p in innerRing]
        tmp = ogr.Geometry(ogr.wkbLinearRing)
        if not srs is None:
            tmp.AssignSpatialReference(srs)
        [tmp.AddPoint(float(x), float(y)) for x, y in innerRing]

        g.AddGeometry(tmp)

    # Make sure geom is valid
    g.CloseRings()
    if not g.IsValid():
        raise GeoKitGeomError("Polygon is invalid")

    # Done!
    return g

polygonizeMask

polygonizeMask(
    mask, bounds=None, srs=None, flat=True, shrink=True
)

Create a geometry set from a matrix mask.

Each True-valued group of pixels will be converted to a geometry

Parameters:

  • mask

    (matrix_like) –

    The mask which will be turned into a geometry set * Must be 2 dimensional * Must be boolean type * True values are interpreted as 'in the geometry'

  • bounds

    ((xMin, yMin, xMax, yMax) or Extent, default: None ) –

    Determines the boundary context for the given mask and will scale the resulting geometry's coordinates accordingly * If a boundary is not given, the geometry coordinates will correspond to the mask's indices * If the boundary is given as an Extent object, an srs input is not required

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the geometries to create

  • flat

    (bool, default: True ) –

    If True, flattens the resulting geometries into a single geometry

  • shrink

    (bool, default: True ) –

    If True, shrink all geoms by a tiny amount in order to avoid geometry overlapping issues * The total amount shrunk should be very very small * Generally this should be left as True unless it is ABSOLUTELY necessary to maintain the same area

Returns:

  • If 'flat' is True: ogr.Geometry
  • else ( [Geometry] ) –
Source code in geokit/core/geom.py
def polygonizeMask(mask, bounds=None, srs=None, flat=True, shrink=True):
    """Create a geometry set from a matrix mask.

    Each True-valued group of pixels will be converted to a geometry

    Parameters
    ----------
    mask : matrix_like
        The mask which will be turned into a geometry set
          * Must be 2 dimensional
          * Must be boolean type
          * True values are interpreted as 'in the geometry'

    bounds : (xMin, yMin, xMax, yMax) or geokit.Extent
        Determines the boundary context for the given mask and will scale
        the resulting geometry's coordinates accordingly
          * If a boundary is not given, the geometry coordinates will
            correspond to the mask's indices
          * If the boundary is given as an Extent object, an srs input is not
            required

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the geometries to create

    flat : bool
        If True, flattens the resulting geometries into a single geometry

    shrink : bool
        If True, shrink all geoms by a tiny amount in order to avoid geometry
        overlapping issues
          * The total amount shrunk should be very very small
          * Generally this should be left as True unless it is ABSOLUTELY
            necessary to maintain the same area

    Returns
    -------
    If 'flat' is True: ogr.Geometry
    else: [ogr.Geometry,  ]
    """
    # Make sure we have a boolean numpy matrix
    if not isinstance(mask, np.ndarray):
        mask = np.array(mask)

    if not (mask.dtype == bool or mask.dtype == np.uint8):
        raise GeoKitGeomError("Mask must be a 2D boolean numpy ndarray")

    # Do vectorization
    result = polygonizeMatrix(matrix=mask, bounds=bounds, srs=srs, flat=flat, shrink=shrink, _raw=True)[0]
    if flat:
        result = result[0]

    # Done!
    return result

polygonizeMatrix

polygonizeMatrix(
    matrix: ndarray,
    bounds: tuple[numeric, numeric, numeric, numeric]
    | None = None,
    srs: srs_input | None = None,
    flat: bool = False,
    shrink: bool = True,
    _raw: bool = False,
) -> DataFrame | tuple[list, list]

Create a geometry set from a matrix of integer values.

Each unique-valued group of pixels will be converted to a geometry

Parameters:

  • matrix

    (matrix_like) –

    The matrix which will be turned into a geometry set * Must be 2 dimensional * Must be integer or boolean type

  • bounds

    ((xMin, yMin, xMax, yMax) or Extent, default: None ) –

    Determines the boundary context for the given matrix and will scale the resulting geometry's coordinates accordingly * If a boundary is not given, the geometry coordinates will correspond to the mask's indices * If the boundary is given as an Extent object, an srs input is not required

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs context for the given matrix and of the geometries to create

  • flat

    (bool, default: False ) –

    If True, flattens the resulting geometries which share a contiguous matrix value into a single geometry object

  • shrink

    (bool, default: True ) –

    If True, shrink all geoms by a tiny amount in order to avoid geometry overlapping issues * The total amount shrunk should be very very small * Generally this should be left as True unless it is ABSOLUTELY necessary to maintain the same area

  • _raw

    (bool, default: False ) –

    return a tuple with with two lists instead of a data frame. The first list contains the The contiguous-valued geometries and the second list the value the value for each geometry

Returns:

  • pandas.DataFrame -> With columns:

    'geom' -> The contiguous-valued geometries 'value' -> The value for each geometry

  • | tuple[ list | list ]
  • The first list contains the contiguous-valued geometries. The seconds
  • list contains the value for each geometry.
Source code in geokit/core/geom.py
def polygonizeMatrix(
    matrix: np.ndarray,
    bounds: tuple[
        numeric,
        numeric,
        numeric,
        numeric,
    ]
    | None = None,
    srs: srs_input | None = None,
    flat: bool = False,
    shrink: bool = True,
    _raw: bool = False,
) -> pd.DataFrame | tuple[list, list]:
    """Create a geometry set from a matrix of integer values.

    Each unique-valued group of pixels will be converted to a geometry

    Parameters
    ----------
    matrix : matrix_like
        The matrix which will be turned into a geometry set
          * Must be 2 dimensional
          * Must be integer or boolean type

    bounds : (xMin, yMin, xMax, yMax) or geokit.Extent
        Determines the boundary context for the given matrix and will scale
        the resulting geometry's coordinates accordingly
          * If a boundary is not given, the geometry coordinates will
            correspond to the mask's indices
          * If the boundary is given as an Extent object, an srs input is not
            required

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs context for the given matrix and of the geometries to create

    flat : bool
        If True, flattens the resulting geometries which share a contiguous matrix
        value into a single geometry object

    shrink : bool
        If True, shrink all geoms by a tiny amount in order to avoid geometry
        overlapping issues
          * The total amount shrunk should be very very small
          * Generally this should be left as True unless it is ABSOLUTELY
            necessary to maintain the same area
    _raw: bool
        return a tuple with with two lists instead of a data frame. The first
        list contains the The contiguous-valued geometries and the second list the value
        the value for each geometry

    Returns
    -------
    pandas.DataFrame -> With columns:
                            'geom' -> The contiguous-valued geometries
                            'value' -> The value for each geometry
    | tuple[ list | list ]
    The first list contains the contiguous-valued geometries. The seconds
    list contains the value for each geometry.
    """
    # Make sure we have a boolean numpy matrix
    if not isinstance(matrix, np.ndarray):
        matrix = np.array(matrix)
    if matrix.dtype == bool or matrix.dtype == np.uint8:
        dtype = "GDT_Byte"
    elif np.issubdtype(matrix.dtype, np.integer):
        dtype = "GDT_Int32"
    else:
        raise GeoKitGeomError("matrix must be a 2D boolean or integer numpy ndarray")

    # Make boundaries if not given
    if bounds is None:
        # bounds in xMin, yMin, xMax, yMax
        bounds = (0, 0, matrix.shape[1], matrix.shape[0])
        pixelHeight = 1
        pixelWidth = 1

    try:  # first try for a tuple
        xMin, yMin, xMax, yMax = bounds
    except:  # next assume the user gave an extent object
        try:
            xMin, yMin, xMax, yMax = bounds.xyXY
            srs = bounds.srs
        except:
            raise GeoKitGeomError("Could not understand 'bounds' input")

    pixelHeight = (yMax - yMin) / matrix.shape[0]
    pixelWidth = (xMax - xMin) / matrix.shape[1]

    if not srs is None:
        srs = SRS.loadSRS(srs)

    # Make a raster dataset and pull the band/maskBand objects

    # used 'round' instead of 'int' because this matched GDAL behavior better
    cols = int(round((xMax - xMin) / pixelWidth))
    rows = int(round((yMax - yMin) / abs(pixelHeight)))
    originX = xMin
    originY = yMax  # Always use the "Y-at-Top" orientation

    # Open the driver
    driver = gdal.GetDriverByName("Mem")  # create a raster in memory
    raster = driver.Create("", cols, rows, 1, getattr(gdal, dtype))

    if raster is None:
        raise GeoKitGeomError("Failed to create temporary raster")

    raster.SetGeoTransform((originX, abs(pixelWidth), 0, originY, 0, -1 * abs(pixelHeight)))

    # Set the SRS
    if not srs is None:
        rasterSRS = SRS.loadSRS(srs)
        raster.SetProjection(rasterSRS.ExportToWkt())

    # Set data into band
    band = raster.GetRasterBand(1)
    band.SetNoDataValue(0)
    band.WriteArray(matrix)

    band.FlushCache()
    raster.FlushCache()

    # rasDS = createRaster(bounds=bounds, data=matrix, noDataValue=0, pixelWidth=pixelWidth, pixelHeight=pixelHeight, srs=srs)

    # Do a polygonize
    rasBand = raster.GetRasterBand(1)
    maskBand = rasBand.GetMaskBand()

    vecDS = gdal.GetDriverByName("Memory").Create("", 0, 0, 0, gdal.GDT_Unknown)
    vector_layer = vecDS.CreateLayer("mem", srs=srs)

    field = ogr.FieldDefn("DN", ogr.OFTInteger)
    vector_layer.CreateField(field)

    # Polygonize geometry
    result = gdal.Polygonize(rasBand, maskBand, vector_layer, 0)
    if result != 0:
        raise GeoKitGeomError("Failed to polygonize geometry")

    # Check how many features were created
    feature_count = vector_layer.GetFeatureCount()

    if feature_count == 0:
        # raise GlaesError("No features in created in temporary layer")
        message = "No features created in temporary layer"
        warnings.warn(message, UserWarning)
        if _raw is True:
            return ([], [])
        elif _raw is False:
            return pd.DataFrame(dict(geom=[], value=[]))

        else:
            raise Exception("_raw is suped to be True or False but is: " + str(type(_raw)))

    # Extract geometries and values
    geoms = []
    rid = []
    for i in range(feature_count):
        ftr = vector_layer.GetFeature(i)
        geoms.append(ftr.GetGeometryRef().Clone())
        rid.append(ftr.items()["DN"])

    # Do shrink, maybe
    if shrink:
        # Compute shrink factor
        shrinkFactor = -0.00001 * (xMax - xMin) / matrix.shape[1]
        geoms = [g.Buffer(float(shrinkFactor)) for g in geoms]

    # Do flatten, maybe
    if flat:
        geoms = np.array(geoms)
        rid = np.array(rid)

        finalGeoms = []
        finalRID = []
        for _rid in set(rid):
            smallGeomSet = geoms[rid == _rid]
            finalGeoms.append(flatten(smallGeomSet) if len(smallGeomSet) > 1 else smallGeomSet[0])
            finalRID.append(_rid)
    else:
        finalGeoms = geoms
        finalRID = rid

    # Cleanup
    vector_layer = None
    vecDS = None
    maskBand = None
    rasBand = None
    raster = None

    # Done!
    if _raw:
        return finalGeoms, finalRID
    else:
        return pd.DataFrame(dict(geom=finalGeoms, value=finalRID))

polygonizeRaster

polygonizeRaster(source, srs=None, flat=False, shrink=True)

Polygonize a raster or an integer-valued data matrix.

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource to polygonize * The Datatype MUST be of boolean of integer type

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the polygons to create * If not given, the raster's internal srs is assumed

  • flat

    (bool, default: False ) –

    If True, flattens the resulting geometries which share a contiguous value into a single geometry object

  • shrink

    (bool, default: True ) –

    If True, shrink all geoms by a tiny amount in order to avoid geometry overlapping issues * The total amount shrunk should be very very small * Generally this should be left as True unless it is ABSOLUTELY necessary to maintain the same area

Returns:

  • pandas.DataFrame -> With columns:

    'geom' -> The contiguous-valued geometries 'value' -> The value for each geometry

Source code in geokit/core/raster.py
def polygonizeRaster(source, srs=None, flat=False, shrink=True):
    """Polygonize a raster or an integer-valued data matrix.

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource to polygonize
        * The Datatype MUST be of boolean of integer type

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the polygons to create
          * If not given, the raster's internal srs is assumed

    flat : bool
        If True, flattens the resulting geometries which share a contiguous
        value into a single geometry object

    shrink : bool
        If True, shrink all geoms by a tiny amount in order to avoid geometry
        overlapping issues
          * The total amount shrunk should be very very small
          * Generally this should be left as True unless it is ABSOLUTELY
            necessary to maintain the same area

    Returns
    -------
    pandas.DataFrame -> With columns:
                            'geom' -> The contiguous-valued geometries
                            'value' -> The value for each geometry
    """
    # Load the information we will need
    source = loadRaster(source)
    band = source.GetRasterBand(1)
    maskBand = band.GetMaskBand()
    if srs is None:
        srs = SRS.loadSRS(source.GetProjectionRef())

    # Do polygonize
    vecDS = gdal.GetDriverByName("Memory").Create("", 0, 0, 0, gdal.GDT_Unknown)
    vecLyr = vecDS.CreateLayer("mem", srs=srs)

    # vecDS = gdal.GetDriverByName("ESRI Shapefile").Create("deleteme.tif", 0, 0, 0, gdal.GDT_Unknown )
    # vecLyr = vecDS.CreateLayer("layer",srs=srs)

    vecField = ogr.FieldDefn("DN", ogr.OFTInteger)
    vecLyr.CreateField(vecField)

    # Polygonize geometry
    result = gdal.Polygonize(band, maskBand, vecLyr, 0)
    if result != 0:
        raise GeoKitGeomError("Failed to polygonize geometry")

    # Check the geoms
    ftrN = vecLyr.GetFeatureCount()
    if ftrN == 0:
        # raise GlaesError("No features in created in temporary layer")
        msg = "No features in created in temporary layer"
        warnings.warn(msg, UserWarning)
        return

    # Extract geometries and values
    geoms = []
    rid = []
    for i in range(ftrN):
        ftr = vecLyr.GetFeature(i)
        geoms.append(ftr.GetGeometryRef().Clone())
        rid.append(ftr.items()["DN"])

    # Do shrink, maybe
    if shrink:
        # Compute shrink factor
        shrinkFactor = -0.00001
        geoms = [g.Buffer(float(shrinkFactor)) for g in geoms]

    # Do flatten, maybe
    if flat:
        geoms = np.array(geoms)
        rid = np.array(rid)

        finalGeoms = []
        finalRID = []
        for _rid in set(rid):
            smallGeomSet = geoms[rid == _rid]
            finalGeoms.append(GEOM.flatten(smallGeomSet) if len(smallGeomSet) > 1 else smallGeomSet[0])
            finalRID.append(_rid)
    else:
        finalGeoms = geoms
        finalRID = rid

    # Cleanup
    vecLyr = None
    vecDS = None
    maskBand = None

    # Done!
    return pd.DataFrame(dict(geom=finalGeoms, value=finalRID))

rasterCellNo

rasterCellNo(
    points,
    source=None,
    bounds=None,
    cellWidth=None,
    cellHeight=None,
)

Returns the raster cell number for one or multiple points defined by geometry or lon/lat. Cell numeration starting in the top left corner cell of the raster with (0,0). Cells with (-1,-1) are out of bounds.

Args: points (osgeo.ogr.Geometry, tuple, iterable): Can be an osgeo.ogr.Geometry point or an iterable thereof, else a (lon, lat) tuple (in EPSG:4326) or an iterable thereof. source (gdal.Dataset, str, optional): A gdal.Dataset type raster or a str formatted path to a raster file. Defaults to None. bounds (tuple, optional): Raster boundaries in EPSG:4326 in the form of (minX, minY, maxX, maxY). Defaults to None. cellWidth (int, float, optional): The cell width in EPSG:4326 units. Defaults to None. cellHeight (int, float, optional): The cell height in EPSG:4326 units. Defaults to None. NOTE: If source is given, all of the others must be None, else they must be given.

Returns:

  • tuple or iterable: tuple with (X, Y) cell No or an iterable thereof if multiple points were given.
Source code in geokit/core/raster.py
def rasterCellNo(points, source=None, bounds=None, cellWidth=None, cellHeight=None):
    """
    Returns the raster cell number for one or multiple points defined by geometry or lon/lat. Cell numeration
    starting in the top left corner cell of the raster with (0,0). Cells with (-1,-1) are out of bounds.

    Args:
        points (osgeo.ogr.Geometry, tuple, iterable): Can be an osgeo.ogr.Geometry point or an iterable thereof, else a (lon, lat) tuple (in EPSG:4326) or an iterable thereof.
        source (gdal.Dataset, str, optional): A gdal.Dataset type raster or a str formatted path to a raster file. Defaults to None.
        bounds (tuple, optional): Raster boundaries in EPSG:4326 in the form of (minX, minY, maxX, maxY). Defaults to None.
        cellWidth (int, float, optional): The cell width in EPSG:4326 units. Defaults to None.
        cellHeight (int, float, optional): The cell height in EPSG:4326 units. Defaults to None.
    NOTE: If source is given, all of the others must be None, else they must be given.

    Returns
    -------
        tuple or iterable: tuple with (X, Y) cell No or an iterable thereof if multiple points were given.
    """
    # check and preprocess points inputs
    if isinstance(points, ogr.Geometry):
        points = [points]
    elif isinstance(points, tuple) and len(points) == 2:
        points = [points]
    assert hasattr(points, "__iter__"), (
        "points must be an osgeo.ogr.Geometry POINT object, a tuple of (lon, lat) or an iterable of any of them."
    )

    if isinstance(points[0], ogr.Geometry):
        if not all([p.GetGeometryName() == "POINT" for p in points]):
            raise TypeError("Only POINT geometries allowed")
        if not all([p.GetSpatialReference().IsSame(SRS.loadSRS(4326)) for p in points]):
            raise ValueError("SRS of all points must be EPSG:4326")
        # convert to lons and lats
        points = [(p.GetX(), p.GetY()) for p in points]
    else:
        assert all(
            [isinstance(x, tuple) and len(x) == 2 and all([isinstance(_x, (int, float)) for _x in x]) for x in points]
        ), (
            "All entries in points must be (lon, lat) tuples with length of 2 and int or float coordinates if given as tuples or iterable thereof."
        )

    # get bounds, cellWidth and cellHeight from the inputs
    if source is not None:
        if not (bounds is None and cellWidth is None and cellHeight is None):
            raise ValueError("bounds, cellWidth and cellHeight must be None when source raster is given.")
        if isinstance(source, str) and not os.path.isfile(source):
            raise FileNotFoundError("source must be an existing raster file if given as string.")
        try:
            sourceRasterInfo = rasterInfo(source)
            bounds = sourceRasterInfo.bounds
            cellWidth = sourceRasterInfo.pixelWidth
            cellHeight = sourceRasterInfo.pixelHeight
            rasterWidth = sourceRasterInfo.xWinSize
            rasterHeight = sourceRasterInfo.yWinSize
        except:
            raise TypeError("source must be path to a gdal.Dataset raster or a gdal.Dataset raster itself if not None.")
        if not sourceRasterInfo.srs.IsSame(SRS.loadSRS(4326)):
            raise ValueError("raster source must be in epsg:4326")
    else:
        if bounds is None or cellWidth is None or cellHeight is None:
            raise ValueError("If source is None, bounds, cellWidth and cellHeight must be given.")
        assert (
            isinstance(bounds, tuple)
            and len(bounds) == 4
            and all([isinstance(x, (int, float)) for x in bounds])
            and bounds[0] < bounds[2]
            and bounds[1] < bounds[3]
        ), "bounds must be a tuple of length = 4 with int or float entries like such: (minX, minY, maxX, maxY)"
        assert isinstance(cellHeight, (int, float)), "cellHeight must be an int or float."
        assert isinstance(cellWidth, (int, float)), "cellWidth must be an int or float."

        # calculate the raster width and height in Nos of cells
        rasterWidth = (bounds[2] - bounds[0]) / cellWidth
        rasterHeight = (bounds[3] - bounds[1]) / cellHeight
        assert np.isclose(rasterWidth % cellWidth, 0) or np.isclose(rasterWidth % cellWidth, cellWidth), (
            f"rasterWidth {rasterWidth} is not a multiple of cellWidth {cellWidth}"
        )
        assert np.isclose(rasterHeight % cellHeight, 0) or np.isclose(rasterHeight % cellHeight, cellHeight), (
            f"rasterHeight {rasterHeight} is not a multiple of cellHeight {cellHeight}"
        )

    # calculate the cell Nos
    cellNos = list()
    outOfBounds = False
    for point in points:
        X = int(np.floor((point[0] - bounds[0]) / cellWidth))
        Y = int(np.floor((bounds[3] - point[1]) / cellHeight))

        # if any dimension out of bounds
        if (X < 0 or X >= rasterWidth) or (Y < 0 or Y >= rasterHeight):
            X = Y = -1
            outOfBounds = True
        # append to cell No list
        cellNos.append((X, Y))

    if outOfBounds:
        warnings.warn("points contain out-of-bounds locations!")

    # return cellNos as tuple for single point, else as list of tuples
    if len(cellNos) == 1:
        return cellNos[0]
    else:
        return cellNos

rasterInfo

rasterInfo(
    sourceDS: load_raster_input,
    compute_statistics: bool = False,
) -> RasterInfo

Returns a named tuple containing information relating to the input raster.

Parameters:

  • sourceDS

    (Anything acceptable by loadRaster()) –

    The raster datasource

  • compute_statistics

    (bool; optional, default: False ) –

    If True, the maximum and minimum value of the raster data are computed. This is computationally expensive for large rasters and repeated calls of this function on the same raster should be avoided.

Returns:

  • namedtuple -> ( srs: The spatial reference system (as an OGR object)

    dtype: The datatype flipY: A flag which indicates that the raster starts at the 'bottom' as opposed to at the 'top' bounds: The (xMin, yMin, xMax, and yMax) values as a tuple xMin: The minimal X boundary yMin:The minimal Y boundary xMax:The maximal X boundary yMax: The maximal Y boundary pixelWidth: The raster's pixelWidth pixelHeight: The raster's pixelHeight dx:The raster's pixelWidth dy: The raster's pixelHeight noData: The noData value used by the raster scale: The scale value used by the raster offset: The offset value used by the raster xWinSize: The width of the raster is pixels yWinSize: The height of the raster is pixels meta: The raster's meta data )

Source code in geokit/core/raster.py
def rasterInfo(sourceDS: load_raster_input, compute_statistics: bool = False) -> RasterInfo:
    """Returns a named tuple containing information relating to the input raster.

    Parameters
    ----------
    sourceDS : Anything acceptable by loadRaster()
        The raster datasource
    compute_statistics : bool; optional
        If True, the maximum and minimum value of the raster data are computed.
        This is computationally expensive for large rasters and repeated calls of
        this function on the same raster should be avoided.

    Returns
    -------
    namedtuple -> ( srs: The spatial reference system (as an OGR object)
                    dtype: The datatype
                    flipY: A flag which indicates that the raster starts at the
                             'bottom' as opposed to at the 'top'
                    bounds: The (xMin, yMin, xMax, and yMax) values as a tuple
                    xMin: The minimal X boundary
                    yMin:The minimal Y boundary
                    xMax:The maximal X boundary
                    yMax: The maximal Y boundary
                    pixelWidth: The raster's pixelWidth
                    pixelHeight: The raster's pixelHeight
                    dx:The raster's pixelWidth
                    dy: The raster's pixelHeight
                    noData: The noData value used by the raster
                    scale: The scale value used by the raster
                    offset: The offset value used by the raster
                    xWinSize: The width of the raster is pixels
                    yWinSize: The height of the raster is pixels
                    meta: The raster's meta data )
    """
    output = {}
    sourceDS = loadRaster(sourceDS)
    # get srs
    if sourceDS.GetProjectionRef() == "":
        # return None directly if raster has no srs
        srs = None
    else:
        # generate an srs object if we have srs information
        srs = SRS.loadSRS(sourceDS.GetProjectionRef())
    output["srs"] = srs

    # get extent and resolution
    sourceBand = sourceDS.GetRasterBand(1)
    # Is required to get the maximum and minimum value of the raster data

    output["dtype"] = sourceBand.DataType
    output["noData"] = sourceBand.GetNoDataValue()
    output["scale"] = sourceBand.GetScale()
    output["offset"] = sourceBand.GetOffset()

    if compute_statistics is True:
        try:
            sourceBand.ComputeStatistics(0)

            maximum_value = sourceBand.GetMaximum()
            minimum_value = sourceBand.GetMinimum()

            output["maximum_value"] = maximum_value
            output["minimum_value"] = minimum_value
        except:
            output["maximum_value"] = output["noData"]
            output["minimum_value"] = output["noData"]
    else:
        output["maximum_value"] = None
        output["minimum_value"] = None

    xSize = sourceBand.XSize
    ySize = sourceBand.YSize

    xOrigin, dx, _, yOrigin, _, dy = sourceDS.GetGeoTransform()

    xMin = xOrigin
    xMax = xOrigin + dx * xSize

    if dy < 0:
        yMax = yOrigin
        yMin = yMax + dy * ySize
        dy = -1 * dy
        output["flipY"] = True
        output["yAtTop"] = True
    else:
        yMin = yOrigin
        yMax = yOrigin + dy * ySize
        output["flipY"] = False
        output["yAtTop"] = False

    output["pixelWidth"] = dx
    output["pixelHeight"] = dy
    output["dx"] = dx
    output["dy"] = dy
    output["xMin"] = xMin
    output["xMax"] = xMax
    output["yMin"] = yMin
    output["yMax"] = yMax
    output["xWinSize"] = xSize
    output["yWinSize"] = ySize
    output["bounds"] = (xMin, yMin, xMax, yMax)
    output["meta"] = sourceDS.GetMetadata_Dict()
    output["source"] = sourceDS.GetDescription()
    output["data_type_name_str"] = gdal.GetDataTypeName(output["dtype"])

    # clean up
    del sourceBand, sourceDS

    raster_info = RasterInfo(**output)

    return raster_info

rasterStats

rasterStats(
    source, cutline=None, ignoreValue=None, **kwargs
)

Compute basic statistics of the values contained in a raster dataset.

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource

  • cutline

    (ogr.Geometry; optional, default: None ) –

    The geometry over which to cut out the raster's data * Must be a Polygon or MultiPolygon

  • ignoreValue

    (numeric, default: None ) –

    A value to ignore when computing the statistics * If the raster source has a 'no Data' value, it is automatically ignored

  • **kwargs

    • All kwargs are passed on to warp() when 'geom' is given
    • See gdal.WarpOptions for more details
    • For example, 'allTouched' may be useful

Returns:

  • Results from a call to scipy.stats.describe
Source code in geokit/core/raster.py
def rasterStats(source, cutline=None, ignoreValue=None, **kwargs):
    """Compute basic statistics of the values contained in a raster dataset.

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource

    cutline : ogr.Geometry; optional
        The geometry over which to cut out the raster's data
        * Must be a Polygon or MultiPolygon

    ignoreValue : numeric
        A value to ignore when computing the statistics
        * If the raster source has a 'no Data' value, it is automatically
          ignored

    **kwargs
        * All kwargs are passed on to warp() when 'geom' is given
        * See gdal.WarpOptions for more details
        * For example, 'allTouched' may be useful

    Returns
    -------
    Results from a call to scipy.stats.describe
    """
    from scipy.stats import describe

    source = loadRaster(source)

    # Get the matrix to calculate over
    if cutline is not None:
        source = warp(source, cutline=cutline, noData=ignoreValue, **kwargs)

    rawData = extractMatrix(source)
    dataInfo = rasterInfo(source)

    # exclude nodata and ignore values
    sel = np.ones(rawData.shape, dtype="bool")

    if ignoreValue is not None:
        np.logical_and(rawData != ignoreValue, sel, sel)

    if dataInfo.noData is not None:
        np.logical_and(rawData != dataInfo.noData, sel, sel)

    # compute statistics
    data = rawData[sel].flatten()
    return describe(data)

rasterize

rasterize(
    source: str | Path | Geometry | Dataset,
    pixelWidth: numeric,
    pixelHeight: numeric,
    srs: srs_input | None = None,
    bounds: tuple[numeric, numeric, numeric, numeric]
    | Extent
    | None = None,
    where: str | None = None,
    value: numeric | str = 1,
    output: str | None = None,
    dtype: geokit_c_data_types_literal | None = None,
    compress=True,
    noData=None,
    overwrite: bool = True,
    **kwargs,
) -> Dataset | str

Rasterize a vector datasource onto a raster context.

Note:

When creating an 'in memory' raster vs one which is saved to disk, a slightly different algorithm is used which can sometimes add an extra row of pixels. Be aware of this if you intend to compare value-matricies directly from rasters generated with this function.

Parameters:

  • source

    (str or Geometry) –

    If str, the path to the vector file to load If ogr.Geometry, an Polygon geometry - Will be immediately turned into a vector

  • pixelWidth

    (numeric) –

    The pixel width of the raster in the working srs * Is 'srs' is not given, these are the units of the source's inherent srs

  • pixelHeight

    (numeric) –

    The pixel height of the raster in the working srs * Is 'srs' is not given, these are the units of the source's inherent srs

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the point to create * If 'bounds' is an Extent object, the bounds' internal srs will override this input

  • bounds

    ((xMin, yMix, xMax, yMax) or Extent; optional, default: None ) –

    The geographic extents spanned by the raster * If not given, the whole bounds spanned by the input is used

  • where

    (str; optional, default: None ) –

    An SQL-like where statement to use to filter the vector before rasterizing

  • value

    ((numeric, str), default: 1 ) –

    The values to burn into the raster * If a numeric is given, all pixels are burned with the specified value * If a string is given, then one the feature attribute names is expected

  • output

    (str; optional, default: None ) –

    A path to an output file * If output is None, the raster will be created in memory and a dataset handle will be returned * If output is given, the raster will be written to disk and nothing will be returned

  • dtype

    (str; optional, default: None ) –

    The datatype of the represented by the created raster's band * Options are: Byte, Int16, Int32, Int64, Float32, Float64 * If dtype is None and data is None, the assumed datatype is a 'Byte' * If dtype is None and data is not None, the datatype will be inferred from the given data

  • compress

    (bool, default: True ) –

    A flag instructing the output raster to use a compression algorithm * only useful if 'output' has been defined * "DEFLATE" used for Linux/Mac, "LZW" used for Windows

  • noData

    (numeric; optional, default: None ) –

    Specifies which value should be considered as 'no data' in the created raster * Must be the same datatype as the 'dtype' input (or that which is derived)

  • overwrite

    (bool, default: True ) –

    A flag to overwrite a pre-existing output file * If set to False and an 'output' is specified which already exists, an error will be raised

Returns:

  • * If 'output' is None: gdal.Dataset
  • * If 'output' is a string: The path to the output is returned (for easy opening)
Source code in geokit/core/vector.py
def rasterize(
    source: str | pathlib.Path | ogr.Geometry | gdal.Dataset,
    pixelWidth: numeric,
    pixelHeight: numeric,
    srs: srs_input | None = None,
    bounds: tuple[numeric, numeric, numeric, numeric] | Extent | None = None,
    where: str | None = None,
    value: numeric | str = 1,
    output: str | None = None,
    dtype: geokit_c_data_types_literal | None = None,
    compress=True,
    noData=None,
    overwrite: bool = True,
    **kwargs,
) -> gdal.Dataset | str:
    """Rasterize a vector datasource onto a raster context.

    Note:
    -----
    When creating an 'in memory' raster vs one which is saved to disk, a slightly
    different algorithm is used which can sometimes add an extra row of pixels. Be
    aware of this if you intend to compare value-matricies directly from rasters
    generated with this function.

    Parameters
    ----------
    source : str or ogr.Geometry
        If str, the path to the vector file to load
        If ogr.Geometry, an Polygon geometry
            - Will be immediately turned into a vector

    pixelWidth : numeric
        The pixel width of the raster in the working srs
        * Is 'srs' is not given, these are the units of the source's inherent srs

    pixelHeight : numeric
        The pixel height of the raster in the working srs
        * Is 'srs' is not given, these are the units of the source's inherent srs

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the point to create
        * If 'bounds' is an Extent object, the bounds' internal srs will override
          this input

    bounds : (xMin, yMix, xMax, yMax) or Extent; optional
        The geographic extents spanned by the raster
        * If not given, the whole bounds spanned by the input is used

    where : str; optional
        An SQL-like where statement to use to filter the vector before rasterizing

    value : numeric, str
        The values to burn into the raster
        * If a numeric is given, all pixels are burned with the specified value
        * If a string is given, then one the feature attribute names is expected

    output : str; optional
        A path to an output file
        * If output is None, the raster will be created in memory and a dataset
          handle will be returned
        * If output is given, the raster will be written to disk and nothing will
          be returned

    dtype : str; optional
        The datatype of the represented by the created raster's band
        * Options are: Byte, Int16, Int32, Int64, Float32, Float64
        * If dtype is None and data is None, the assumed datatype is a 'Byte'
        * If dtype is None and data is not None, the datatype will be inferred
          from the given data

    compress : bool
        A flag instructing the output raster to use a compression algorithm
        * only useful if 'output' has been defined
        * "DEFLATE" used for Linux/Mac, "LZW" used for Windows

    noData : numeric; optional
        Specifies which value should be considered as 'no data' in the created
        raster
        * Must be the same datatype as the 'dtype' input (or that which is derived)

    overwrite : bool
        A flag to overwrite a pre-existing output file
        * If set to False and an 'output' is specified which already exists,
          an error will be raised

    Returns
    -------
    * If 'output' is None: gdal.Dataset
    * If 'output' is a string: The path to the output is returned (for easy opening)
    """
    # Normalize some inputs
    if isinstance(source, ogr.Geometry):
        source = createVector(source)
    else:
        source = loadVector(source)

    # Get the vector's info
    vecinfo = vectorInfo(source)

    if srs is None:
        srs = vecinfo.srs
        srsOkay = True
    else:
        srs = SRS.loadSRS(srs)
        if srs.IsSame(vecinfo.srs):
            srsOkay = True
        else:
            srsOkay = False

    # Look for bounds input
    if bounds is None:
        bounds = vecinfo.bounds
        if not srsOkay:
            bounds = GEOM.boundsToBounds(bounds, vecinfo.srs, srs)
    else:
        try:
            bounds = bounds.xyXY  # Get a tuple from an Extent object
        except:
            pass  # Bounds should already be a tuple

    bounds = UTIL.fitBoundsTo(bounds, pixelWidth, pixelHeight)

    # Collect rasterization options
    if output is None and not "bands" in kwargs:
        kwargs["bands"] = [1]

    list_of_data_types = []

    if isinstance(dtype, str):
        list_of_data_types.append(dtype)

    list_of_numbers = []

    if isinstance(value, str):
        kwargs["attribute"] = value
        data_type_of_field_as_string = vecinfo.attribute_data_types_str
        list_of_data_types.append(data_type_of_field_as_string[value])

    else:
        kwargs["burnValues"] = [
            value,
        ]
        list_of_numbers.append(value)

    if isinstance(noData, (numeric, bool)):
        list_of_numbers.append(noData)

    # minimum_data_type = dtype
    # just to raise error early if invalid
    # Do 'in memory' rasterization
    # We need to follow this path in both cases since the below fails when simultaneously rasterizing and writing to disk (I couldn't figure out why...)
    if output is None or not srsOkay:
        minimum_data_type_string = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_string(
            list_of_numbers=list_of_numbers, minimum_gdal_type_list=list_of_data_types
        )
        # Create temporary output file
        outputDS = UTIL.quickRaster(
            bounds=bounds,
            srs=srs,
            dx=pixelWidth,
            dy=pixelHeight,
            dtype=minimum_data_type_string,
            noData=noData,
        )

        # Do rasterize
        tmp = gdal.Rasterize(outputDS, source, where=where, **kwargs)
        if tmp == 0:
            raise GeoKitRasterError("Rasterization failed!")
        outputDS.FlushCache()

        if output is None:
            return outputDS
        else:
            ri = RASTER.rasterInfo(outputDS)
            RASTER.createRasterLike(ri, output=output, data=RASTER.extractMatrix(outputDS))
            return output

    # Do a rasterization to a file on disk
    else:
        # Check for existing file
        if os.path.isfile(output):
            if overwrite == True:
                os.remove(output)
                if os.path.isfile(output + ".aux.xml"):  # Because QGIS....
                    os.remove(output + ".aux.xml")
            else:
                raise GeoKitRasterError("Output file already exists: %s" % output)

        # Arrange some inputs
        aligned = kwargs.pop("targetAlignedPixels", True)

        if not "creationOptions" in kwargs:
            if compress:
                creation_options = RASTER.COMPRESSION_OPTION
            else:
                creation_options = []
        else:
            creation_options = kwargs.pop("creationOptions")

        # Fix the bounds issue by making them  just a little bit smaller, which should be fixed by gdalwarp
        bounds = (
            bounds[0] + 0.001 * pixelWidth,
            bounds[1] + 0.001 * pixelHeight,
            bounds[2] - 0.001 * pixelWidth,
            bounds[3] - 0.001 * pixelHeight,
        )

        minimum_data_type_constant = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_constant(
            list_of_numbers=list_of_numbers, minimum_gdal_type_list=list_of_data_types
        )
        # Do rasterize
        tmp = gdal.Rasterize(
            output,
            source,
            outputBounds=bounds,
            xRes=pixelWidth,
            yRes=pixelHeight,
            outputSRS=srs,
            noData=noData,
            where=where,
            creationOptions=creation_options,
            targetAlignedPixels=aligned,
            outputType=minimum_data_type_constant,
            **kwargs,
        )

        if not UTIL.isRaster(tmp):
            raise GeoKitRasterError("Rasterization failed!")

        return output

saveRasterAsTif

saveRasterAsTif(source: Dataset, output: str, **kwargs)

Write a osgeo.gdal.Dataset in memory to a GeoTiff file to disk.

Parameters:

  • source

    (Dataset) –
  • output

    (str) –

    A path to an output file

Returns:

  • str

    Path to the saved file on disk.

Source code in geokit/core/raster.py
def saveRasterAsTif(source: gdal.Dataset, output: str, **kwargs):
    """Write a osgeo.gdal.Dataset in memory to a GeoTiff file to disk.

    Parameters
    ----------
    source : osgeo.gdal.Dataset

    output : str
        A path to an output file

    Returns
    -------
    str
        Path to the saved file on disk.
    """
    # assert os.path.isdir(os.path.dirname(output)), 'Output folder does not exist!'
    assert output.split(".")[-1] in [
        "tif",
        "tiff",
    ], "Wrong type specified, use *.tif or *.tiff"

    sourceInfo = rasterInfo(source)
    data = extractMatrix(source)

    return createRaster(
        bounds=sourceInfo.bounds,
        pixelWidth=sourceInfo.dx,
        pixelHeight=sourceInfo.dy,
        noData=sourceInfo.noData,
        dtype=sourceInfo.data_type_name_str,
        srs=sourceInfo.srs,
        data=data,
        output=output,
        **kwargs,
    )

scaleMatrix

scaleMatrix(mat, scale, strict=True)

Scale a 2-dimensional matrix. For example, a 2x2 matrix, with a scale of 2, will become a 4x4 matrix. Or scaling a 24x24 matrix with a scale of -3 will produce an 8x8 matrix.

  • Scaling UP (positive) results in a dimensionally larger matrix where each value is repeated scale^2 times
  • scaling DOWN (negative) results in a dimensionally smaller matrix where each value is the average of the associated 'up-scaled' block

Returns:

  • ndarray

Examples:

INPUT Scaling Factor Output

| 1 2 | 2 | 1 1 2 2 | | 3 4 | | 1 1 2 2 | | 3 3 4 4 | | 3 3 4 4 |

| 1 1 1 1 | -2 | 1.5 2.0 | | 2 2 3 3 | | 5.25 6.75| | 4 4 5 5 | | 6 7 8 9 |

| 1 1 1 1 | -3 | 2.55 3.0 | | 2 2 3 3 | * strict=False | 7.0 9 | | 4 4 5 5 |

| 6 7 8 9 | padded
           | 1 1 1 1 0 0 |
           | 2 2 3 3 0 0 |
           | 4 4 5 5 0 0 |
           | 6 7 8 9 0 0 |
           | 0 0 0 0 0 0 |
           | 0 0 0 0 0 0 |
Source code in geokit/core/util.py
def scaleMatrix(mat, scale, strict=True):
    """Scale a 2-dimensional matrix. For example, a 2x2 matrix, with a scale of 2,
    will become a 4x4 matrix. Or scaling a 24x24 matrix with a scale of -3 will
    produce an 8x8 matrix.

    * Scaling UP (positive) results in a dimensionally larger matrix where each
      value is repeated scale^2 times
    * scaling DOWN (negative) results in a dimensionally smaller matrix where each
      value is the average of the associated 'up-scaled' block

    Parameters
    ----------
        mat : numpy.ndarray or [[numeric,],]
            The data to be scaled
              * Must be two dimensional

        scale : int or (int, int)
            The dimensional scaling factor for either both, or independently for
            the Y and X dimensions
              * If scaling down, the scaling factors must be a factor of the their
                associated dimension in the input matrix (unless 'strict' is set
                to False)

        strict : bool
            Whether or not to force a fail when scaling-down by a scaling factor
            which is not a dimensional factor
              * Really intended for internal use...
              * When scaling down by a non-dimensional factor, the matrix will be
                padded with zeros such that the new matrix has dimensional sizes
                which are divisible by the scaling factor. The points which are
                not at the right or bottom boundary are averaged, same as before.
                The points which lie on the edge however, are also averaged across
                all the values which lie in those pixels, but they are corrected
                so that the averaging does NOT take into account the padded zeros.

    Returns
    -------
    numpy.ndarray

    Examples
    --------
    INPUT       Scaling Factor      Output
    --------------------------------------

    | 1 2 |             2           | 1 1 2 2 |
    | 3 4 |                         | 1 1 2 2 |
                                    | 3 3 4 4 |
                                    | 3 3 4 4 |

    | 1 1 1 1 |        -2           | 1.5  2.0 |
    | 2 2 3 3 |                     | 5.25 6.75|
    | 4 4 5 5 |
    | 6 7 8 9 |

    | 1 1 1 1 |        -3           | 2.55  3.0 |
    | 2 2 3 3 |   * strict=False    | 7.0    9  |
    | 4 4 5 5 |

    | 6 7 8 9 |       *padded*
                    --------------------------
                   | 1 1 1 1 0 0 |
                   | 2 2 3 3 0 0 |
                   | 4 4 5 5 0 0 |
                   | 6 7 8 9 0 0 |
                   | 0 0 0 0 0 0 |
                   | 0 0 0 0 0 0 |
    """
    # unpack scale
    try:
        yScale, xScale = scale
    except:
        yScale, xScale = scale, scale

    # check for ints
    if not (isinstance(xScale, int) and isinstance(yScale, int)):
        raise ValueError("scale must be integer types")

    if xScale == 0 and yScale == 0:
        return mat  # no scaling (it would just be silly to call this)
    elif xScale > 0 and yScale > 0:  # scale up
        out = np.zeros((mat.shape[0] * yScale, mat.shape[1] * xScale), dtype=mat.dtype)
        for yo in range(yScale):
            for xo in range(xScale):
                out[yo::yScale, xo::xScale] = mat

    elif xScale < 0 and yScale < 0:  # scale down
        xScale = -1 * xScale
        yScale = -1 * yScale
        # ensure scale is a factor of both xSize and ySize
        if strict:
            if not (mat.shape[0] % yScale == 0 and mat.shape[1] % xScale == 0):
                raise GeoKitError("Matrix can only be scaled down by a factor of it's dimensions")
            yPad = 0
            xPad = 0
        else:
            # get the amount to pad in the y direction
            yPad = yScale - mat.shape[0] % yScale
            # get the amount to pad in the x direction
            xPad = xScale - mat.shape[1] % xScale

            if yPad == yScale:
                yPad = 0
            if xPad == xScale:
                xPad = 0

            # Do y-padding
            if yPad > 0:
                mat = np.concatenate((mat, np.zeros((yPad, mat.shape[1]))), 0)
            if xPad > 0:
                mat = np.concatenate((mat, np.zeros((mat.shape[0], xPad))), 1)

        out = np.zeros((mat.shape[0] // yScale, mat.shape[1] // xScale), dtype="float")
        for yo in range(yScale):
            for xo in range(xScale):
                out += mat[yo::yScale, xo::xScale]
        out = out / (xScale * yScale)

        # Correct the edges if a padding was provided
        if yPad > 0:
            # fix the right edge EXCLUDING the bot-left point
            out[:-1, -1] *= yScale / (yScale - yPad)
        if xPad > 0:
            # fix the bottom edge EXCLUDING the bot-left point
            out[-1, :-1] *= xScale / (xScale - xPad)
        if yPad > 0:
            out[-1, -1] *= yScale * xScale / (yScale - yPad) / (xScale - xPad)  # fix the bot-left point

    else:  # we have both a scaleup and a scale down
        raise GeoKitError("Dimensions must be scaled in the same direction")

    return out

shift

shift(geom, lonShift=0, latShift=0)

Shift a polygon in longitudinal and/or latitudinal direction.

Inputs: geom : The geometry to be shifted - a single ogr Geometry object of POINT, LINESTRING, POLYGON or MULTIPOLYGON type - NOTE: Accepts only 2D geometries, z value must be zero.

lonShift - (int, float) : The shift in longitudinal direction in units of the geom srs, may be positive or negative

latShift - (int, float) : The shift in latitudinal direction in units of the geom srs, may be positive or negative
Returns :

osgeo.ogr.Geometry object of the input type with shifted coordinates

Source code in geokit/core/geom.py
def shift(geom, lonShift=0, latShift=0):
    """Shift a polygon in longitudinal and/or latitudinal direction.

    Inputs:
        geom : The geometry to be shifted
            - a single ogr Geometry object of POINT, LINESTRING, POLYGON or MULTIPOLYGON type
            - NOTE: Accepts only 2D geometries, z value must be zero.

        lonShift - (int, float) : The shift in longitudinal direction in units of the geom srs, may be positive or negative

        latShift - (int, float) : The shift in latitudinal direction in units of the geom srs, may be positive or negative

    Returns :
    ---------
    osgeo.ogr.Geometry object of the input type with shifted coordinates
    """
    if not isinstance(geom, ogr.Geometry):
        raise TypeError(f"geom must be of type osgeo.ogr.Geometry")
    geom = geom.Clone()  # do not modify input geom
    # first get srs of input geom
    _srs = geom.GetSpatialReference()
    # get the dimension of the geometry
    dims = geom.GetCoordinateDimension()
    assert dims in [2, 3], f"Only 2D and 3D points are supported, but got {dims}D"

    # define sub method to shift collection of single points
    def _movePoints(pointCollection, lonShift, latShift):
        """Auxiliary function shifting individual points."""
        points = list()
        for i in range(len(str(pointCollection).split(","))):
            points.append(pointCollection.GetPoint(i))
        # shift the points individually
        points_shifted = list()
        for p in points:
            assert p[2] == 0, f"All z-values must be zero!"
            points_shifted.append((p[0] + lonShift, p[1] + latShift))
        return points_shifted

    # first check if geom is a point and shift
    if "POINT" in geom.GetGeometryName():
        p = geom.GetPoint()
        point_shifted = point((p[0] + lonShift, p[1] + latShift), srs=_srs)
        if dims == 2:
            point_shifted.FlattenTo2D()
        return point_shifted
    # else check if line and adapt
    elif "LINESTRING" in geom.GetGeometryName() and not "MULTILINE" in geom.GetGeometryName():
        assert not geom.IsEmpty(), f"Line is empty"
        line_shifted = line(
            _movePoints(pointCollection=geom, lonShift=lonShift, latShift=latShift),
            srs=_srs,
        )
        if dims == 2:
            line_shifted.FlattenTo2D()
        return line_shifted
    # else check if we have a (multi)polygon
    elif "POLYGON" in geom.GetGeometryName():
        assert not geom.IsEmpty(), f"Polygon is empty"
        if not "MULTIPOLYGON" in geom.GetGeometryName():
            # only a simple polygon, generate single entry list to allow iteration
            geom = [geom]
        # iterate over individual polygons
        for ip, poly in enumerate(geom):
            assert "POLYGON" in poly.GetGeometryName(), f"MULTIPOLYGON is not composed of only POLYGONS"
            # iterate over sub linear rings
            for ir, ring in enumerate(poly):
                assert "LINEARRING" in ring.GetGeometryName(), (
                    f"POLYGON (or sub polygon of MULTIPOLYGON) is not composed of only LINEARRINGS"
                )
                poly_shifted = polygon(
                    _movePoints(pointCollection=ring, lonShift=lonShift, latShift=latShift),
                    srs=_srs,
                )
                if ip == 0 and ir == 0:
                    multi_shifted = poly_shifted
                else:
                    multi_shifted = multi_shifted.Union(poly_shifted)
        # the shifted polygon should have the same dimensions as the input
        if dims == 2:
            multi_shifted.FlattenTo2D()
        return multi_shifted
    else:
        raise TypeError(f"geom must be a 'POINT', 'LINESTRING', 'POLYGON' or 'MULTIPOLYGON' osgeo.ogr.Geometry")

sieve

sieve(
    source,
    threshold: int = 100,
    connectedness: Literal[4, 8] = 8,
    mask="none",
    quiet_flag: bool = False,
    output: str | None = None,
    **kwargs,
) -> Dataset | str

Removes raster polygons smaller than a provided threshold size (in pixels) and replaces them with the pixel value of the largest neighbour polygon. It is useful if you have a large amount of small areas on your raster map.

Returns:

  • * If 'output' is None : gdal.Dataset
  • * If 'output' is a string : The path to the output is returned (for easy opening)

Parameters:

  • source

    (Anything acceptable by loadRaster()) –
  • threshold

    (int, default: 100 ) –
  • connectedness

    (Literal[4, 8], default: 8 ) –
                 adjacent for polygon membership purposes or 8 indicating they are.
    
  • mask

        value other than zero will be considered suitable for inclusion in polygons.
    
  • quiet_flag

    (bool, default: False ) –
  • output

    (str; optional, default: None ) –

    The path on disk where the new raster should be created

  • **kwargs

    • All kwargs are passed on to SieveFilter()
    • See gdal.SieveFilter for more details
Source code in geokit/core/raster.py
def sieve(
    source,
    threshold: int = 100,
    connectedness: Literal[4, 8] = 8,
    mask="none",
    quiet_flag: bool = False,
    output: str | None = None,
    **kwargs,
) -> gdal.Dataset | str:
    """
    Removes raster polygons smaller than a provided threshold size (in pixels) and
    replaces them with the pixel value of the largest neighbour polygon.
    It is useful if you have a large amount of small areas on your raster map.

    Returns
    -------
    * If 'output' is None : gdal.Dataset
    * If 'output' is a string : The path to the output is returned (for easy opening)

    Parameters
    ----------
    source : Anything acceptable by loadRaster()

    threshold (int): minimum polygon size (number of pixels) to retain.

    connectedness (int): either 4 indicating that diagonal pixels are not considered directly
                         adjacent for polygon membership purposes or 8 indicating they are.

    mask (str): 'none' or 'default'. An optional mask band. All pixels in the mask band with a
                value other than zero will be considered suitable for inclusion in polygons.

    quiet_flag (bool): 0 or 1. Callback for reporting algorithm progress

    output : str; optional
        The path on disk where the new raster should be created

    **kwargs:
        * All kwargs are passed on to SieveFilter()
        * See gdal.SieveFilter for more details
    """
    gdal.AllRegister()

    try:
        gdal.SieveFilter
    except:
        print("")
        print('gdal.SieveFilter() not available.  You are likely using "old gen"')
        print("bindings or an older version of the next gen bindings.")
        print("")

    ### Open source file
    # if output is None:
    #     src_ds = gdal.Open( source, gdal.GA_Update )
    # else:
    #     src_ds = gdal.Open( source, gdal.GA_ReadOnly )

    src_ds = loadRaster(source)

    if src_ds is None:
        print("Unable to open %s " % source)

    srcband = src_ds.GetRasterBand(1)

    if mask == "default":
        maskband = srcband.GetMaskBand()
    elif mask == "none":
        maskband = None
    else:
        mask_ds = loadRaster(mask)
        maskband = mask_ds.GetRasterBand(1).GetMaskBand()

    ### Create output file if one is specified.

    if output is not None:
        format = "GTiff"
        driver = gdal.GetDriverByName(format)
        dst_ds = driver.Create(
            output,
            src_ds.RasterXSize,
            src_ds.RasterYSize,
            1,
            srcband.DataType,
            COMPRESSION_OPTION,
        )  # ["COMPRESS=LZW"]

    else:
        format = "Mem"
        driver = gdal.GetDriverByName(format)  # create a raster in memory
        dst_ds = driver.Create(
            "",
            src_ds.RasterXSize,
            src_ds.RasterYSize,
            1,
            srcband.DataType,
            COMPRESSION_OPTION,
        )  # ["COMPRESS=LZW"]

    wkt = src_ds.GetProjection()
    if wkt != "":
        dst_ds.SetProjection(wkt)
    dst_ds.SetGeoTransform(src_ds.GetGeoTransform())
    dstband = dst_ds.GetRasterBand(1)

    if quiet_flag:
        prog_func = None
    else:
        prog_func = gdal.TermProgress_nocb

    result = gdal.SieveFilter(
        srcband,
        maskband,
        dstband,
        threshold,
        connectedness,
        callback=prog_func,
        **kwargs,
    )

    dst_ds.FlushCache()

    # Return raster if in memory
    if output is None:
        return dst_ds

    # Return output path to raster if on disk
    else:
        src_ds = None
        dst_ds = None
        mask_ds = None
        return output

subTiles

subTiles(geom, zoom, checkIntersect=True, asGeom=False)

Generate a collection of tiles which encompass the passed geometry.

Parameters:

  • geom

    (Geometry) –

    The geometry to be analyzed

  • zoom

    (int) –

    The zoom level to generate tiles on

  • checkIntersect

    (bool, default: True ) –

    If True, only tiles which overlap the given geometry are returned

  • asGeom

    (bool, default: False ) –

    If True, geometry object corresponding to each tile is yielded, instead of (xi,yi,zoom) tuples

Returns:

  • If asGeom is False: Generates (xi, yi, zoom) tuples
  • If asGeom is True: Generates Geometry objects
Source code in geokit/core/geom.py
def subTiles(geom, zoom, checkIntersect=True, asGeom=False):
    """
    Generate a collection of tiles which encompass the passed geometry.

    Parameters
    ----------
    geom : ogr.Geometry
        The geometry to be analyzed

    zoom : int
        The zoom level to generate tiles on

    checkIntersect : bool
        If True, only tiles which overlap the given geometry are returned

    asGeom : bool
        If True, geometry object corresponding to each tile is yielded,
        instead of (xi,yi,zoom) tuples

    Returns
    -------
    If asGeom is False: Generates (xi, yi, zoom) tuples
    If asGeom is True:  Generates Geometry objects
    """
    geom4326 = transform(geom, toSRS=SRS.EPSG4326)
    if checkIntersect:
        geom3857 = transform(geom, toSRS=SRS.EPSG3857)

    xmin, xmax, ymin, ymax = geom4326.GetEnvelope()

    tl_xi, tl_yi = smopy.deg2num(ymax, xmin, zoom)
    br_xi, br_yi = smopy.deg2num(ymin, xmax, zoom)

    for xi in range(tl_xi, br_xi + 1):
        for yi in range(tl_yi, br_yi + 1):
            if checkIntersect or asGeom:
                gtile = tile(xi, yi, zoom)

            if checkIntersect:
                if not geom3857.Intersects(gtile):
                    continue

            if asGeom:
                yield gtile
            else:
                yield Tile(xi, yi, zoom)

tile

tile(xi, yi, zoom)

Generates a box corresponding to a tile used for "slippery maps".

Parameters:

  • xi

    (int) –

    The tile's X-index - Range depends on zoom value

  • yi

    (int) –

    The tile's Y-index - Range depends on zoom value

  • zoom

    (int) –

    The tile's zoom index - Range is between 0 and 18

Returns:

  • Geometry
Source code in geokit/core/geom.py
def tile(xi, yi, zoom):
    """Generates a box corresponding to a tile used for "slippery maps".

    Parameters
    ----------
    xi : int
        The tile's X-index
        - Range depends on zoom value

    yi : int
        The tile's Y-index
        - Range depends on zoom value

    zoom : int
        The tile's zoom index
        - Range is between 0 and 18

    Returns
    -------
    ogr.Geometry
    """
    tl = smopy.num2deg(xi - 0.0, yi + 1.0, zoom)[::-1]
    br = smopy.num2deg(xi + 1.0, yi - 0.0, zoom)[::-1]

    o = SRS.xyTransform([tl, br], fromSRS=SRS.EPSG4326, toSRS=SRS.EPSG3857, outputFormat="xy")

    return box(o.x.min(), o.y.min(), o.x.max(), o.y.max(), srs=SRS.EPSG3857)

tileAt

tileAt(x, y, zoom, srs)

Generates a box corresponding to a tile at the coordinates 'x' and 'y' in the given srs,.

Parameters:

  • x

    (float) –

    The X coordinate to search for a tile around

  • y

    (float) –

    The Y coordinate to search for a tile around

  • zoom

    (int) –

    The tile's zoom index - Range is between 0 and 18

  • srs

    (anything acceptable to SRS.loadSRS) –

    The SRS of the given 'x' & 'y' coordinates

Returns:

  • Geometry
Source code in geokit/core/geom.py
def tileAt(x, y, zoom, srs):
    """Generates a box corresponding to a tile at the coordinates 'x' and 'y'
     in the given srs,.

    Parameters
    ----------
    x : float
        The X coordinate to search for a tile around

    y : float
        The Y coordinate to search for a tile around

    zoom : int
        The tile's zoom index
        - Range is between 0 and 18

    srs : anything acceptable to SRS.loadSRS
        The SRS of the given 'x' & 'y' coordinates

    Returns
    -------
    ogr.Geometry
    """
    t = SRS.tileIndexAt(x=x, y=y, zoom=zoom, srs=srs)

    return tile(t.xi, t.yi, t.zoom)

tileIndexAt

tileIndexAt(x, y, zoom, srs)

Get the "slippery tile" index at the given zoom, around the coordinates ('x', 'y') within the specified 'srs'.

Source code in geokit/core/srs.py
def tileIndexAt(x, y, zoom, srs):
    """Get the "slippery tile" index at the given zoom, around the
    coordinates ('x', 'y') within the specified 'srs'.
    """
    srs = loadSRS(srs)

    iterable_input = isinstance(x, Iterable) or isinstance(y, Iterable)

    if not srs.IsSame(EPSG4326):
        pt = xyTransform(x, y, fromSRS=srs, toSRS=EPSG4326, outputFormat="xy")

        if iterable_input:
            x, y = pt.x, pt.y
        else:
            x, y = pt.x[0], pt.y[0]

    if iterable_input:
        x = np.array(x)
        y = np.array(y)

    xi, yi = smopy.deg2num(y, x, zoom)

    return Tile(xi, yi, zoom)

tileize

tileize(geom, zoom)

Deconstruct a given geometry into a set of tiled geometries.

Returns: Generator of ogr.Geometry objects

Source code in geokit/core/geom.py
def tileize(geom, zoom):
    """Deconstruct a given geometry into a set of tiled geometries.

    Returns: Generator of ogr.Geometry objects
    """
    geom = transform(geom, toSRS=SRS.EPSG3857)
    for tile_ in subTiles(geom, zoom, asGeom=True, checkIntersect=True):
        yield geom.Intersection(tile_)

transform

transform(
    geoms,
    toSRS,
    fromSRS=None,
    revert360degProj=False,
    segment=None,
) -> Geometry | list[Geometry]

Transform a geometry, or a list of geometries, from one SRS to another.

Parameters:

  • geoms

    (Geometry or [Geometry]) –

    The geometry or geometries to transform * All geometries must have the same spatial reference

  • toSRS

    (Anything acceptable to geokit.srs.loadSRS()) –

    The srs of the output geometries

  • fromSRS

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the input geometries * Only needed if an SRS cannot be inferred from the geometry inputs or is, for whatever reason, the geometry's SRS is wrong

  • revert360degProj

    (bool; optional, default: False ) –

    If True, will revert the 360° shift that is applied by PROJ to points beyond the antimeridian when transforming to EPSG:4326 to avoid invalid, distorted geometries when points are only partially shifted. Will then devliver a geometry that may partially be outside the -180° to 180° longitude range. By default False.

  • segment

    (float; optional, default: None ) –

    An optional segmentation length to apply to the input geometries BEFORE transformation occurs. The input geometries will be segmented such that no line segment is longer than the given segment size * Units are in the input geometry's native unit * Use this for a more detailed transformation!

Returns:

  • Geometry or [Geometry]
Note:

When inferring the SRS from the given geometries, only the FIRST geometry is checked for an existing SRS

Source code in geokit/core/geom.py
def transform(geoms, toSRS, fromSRS=None, revert360degProj=False, segment=None) -> ogr.Geometry | list[ogr.Geometry]:
    """Transform a geometry, or a list of geometries, from one SRS to another.

    Parameters
    ----------
    geoms : ogr.Geometry or [ogr.Geometry, ]
        The geometry or geometries to transform
          * All geometries must have the same spatial reference

    toSRS : Anything acceptable to geokit.srs.loadSRS()
        The srs of the output geometries

    fromSRS : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the input geometries
          * Only needed if an SRS cannot be inferred from the geometry inputs or
            is, for whatever reason, the geometry's SRS is wrong

    revert360degProj : bool; optional
        If True, will revert the 360° shift that is applied by PROJ to points
        beyond the antimeridian when transforming to EPSG:4326 to avoid invalid,
        distorted geometries when points are only partially shifted. Will then
        devliver a geometry that may partially be outside the -180° to 180°
        longitude range. By default False.

    segment : float; optional
        An optional segmentation length to apply to the input geometries BEFORE
        transformation occurs. The input geometries will be segmented such that
        no line segment is longer than the given segment size
          * Units are in the input geometry's native unit
          * Use this for a more detailed transformation!

    Returns
    -------
    ogr.Geometry or [ogr.Geometry, ]

    Note:
    -----
    When inferring the SRS from the given geometries, only the FIRST geometry
    is checked for an existing SRS
    """
    # make sure geoms is a list
    if isinstance(geoms, ogr.Geometry):
        returnSingle = True
        geoms = [
            geoms,
        ]
    else:  # assume geoms is iterable
        returnSingle = False
        try:
            geoms = list(geoms)
        except Exception as e:
            msg = "Geoms is neither an osgeo.ogr.Geometry instance nor an iterable that can be converted to a list."
            warnings.warn(msg, UserWarning)
            raise e

    # make sure geoms is a list
    if fromSRS is None:
        fromSRS = geoms[0].GetSpatialReference()
        assert all([g.GetSpatialReference().IsSame(fromSRS) for g in geoms]), f"geoms have different SRS."
        if fromSRS is None:
            raise GeoKitGeomError("Could not determine fromSRS from geometry")

    # load srs's
    fromSRS = SRS.loadSRS(fromSRS)
    toSRS = SRS.loadSRS(toSRS)

    # make a transformer
    trx = osr.CoordinateTransformation(fromSRS, toSRS)

    # Do transformation
    geoms = [g.Clone() for g in geoms]
    if not segment is None:
        [g.Segmentize(segment) for g in geoms]

    r = [g.Transform(trx) for g in geoms]
    if sum(r) > 0:  # check for errors
        raise GeoKitGeomError("Errors in geometry transformations")

    if revert360degProj and toSRS.IsSame(SRS.loadSRS(4326)):

        def _revert_antimeridian_proj(geom, max_width=180):
            """Shift back points across the antimeridian to undo the effect of the PROJ function."""

            def _shift_points(g):
                """Shifts points back by +/-360° if PROJ shifted them due to the antimeridian."""
                prev_x, _, _ = g.GetPoint(0)
                for j in range(1, g.GetPointCount()):
                    x, y, z = g.GetPoint(j)
                    dx = x - prev_x
                    if dx > max_width:
                        x -= 360
                    elif dx < -max_width:
                        x += 360
                    g.SetPoint(j, x, y, z)
                    prev_x = x
                return geom

            # apply at different levels (directly to lines, indirectly to rings per polygon or recursively to multi-polygons/lines)
            geom_type = geom.GetGeometryType()
            if geom_type in (ogr.wkbPolygon, ogr.wkbPolygon25D):
                for i in range(geom.GetGeometryCount()):
                    _shift_points(geom.GetGeometryRef(i))
            elif geom_type in (
                ogr.wkbMultiPolygon,
                ogr.wkbMultiPolygon25D,
                ogr.wkbMultiLineString,
                ogr.wkbMultiLineString25D,
            ):
                for i in range(geom.GetGeometryCount()):
                    _revert_antimeridian_proj(geom.GetGeometryRef(i), max_width)
            elif geom_type in (ogr.wkbLineString, ogr.wkbLineString25D):
                _shift_points(geom)
            # points are returned unchanged since here the projection across the antimeridian is desired
            return geom

        # polygons or lines crossing the antimeridian may have been distorted by PROJ operation
        geoms = [_revert_antimeridian_proj(geom=g) for g in geoms]

    # make sure all geoms are valid
    try:
        assert all([g.IsValid() for g in geoms])  # no msg, fall back to except only if needed to save time
    except:
        geoms = [g.Buffer(0) for g in geoms]  # trick to reset boundary of unnecessarily invalid geoms
        assert all([g.IsValid() for g in geoms]), f"Some geometries are invalid after transformation"

    # Done!
    if returnSingle:
        return geoms[0]
    else:
        return geoms

vectorInfo

vectorInfo(source) -> vecInfo

Extract general information about a vector source.

Determines:

Parameters:

  • source

    (Anything acceptable by loadVector()) –

    The vector datasource to read from

Returns:

  • namedtuple -> (srs : The source's SRS system,

    bounds : The source's boundaries (in the srs's units), xMin : The source's xMin boundaries (in the srs's units), yMin : The source's xMax boundaries (in the srs's units), xMax : The source's yMin boundaries (in the srs's units), yMax : The source's yMax boundaries (in the srs's units), count : The number of features in the source, attributes : The attribute titles for the source's features,)

Source code in geokit/core/vector.py
def vectorInfo(source) -> vecInfo:
    """Extract general information about a vector source.

    Determines:

    Parameters
    ----------
    source : Anything acceptable by loadVector()
        The vector datasource to read from

    Returns
    -------
    namedtuple -> (srs : The source's SRS system,
                   bounds : The source's boundaries (in the srs's units),
                   xMin : The source's xMin boundaries (in the srs's units),
                   yMin : The source's xMax boundaries (in the srs's units),
                   xMax : The source's yMin boundaries (in the srs's units),
                   yMax : The source's yMax boundaries (in the srs's units),
                   count : The number of features in the source,
                   attributes : The attribute titles for the source's features,)
    """
    info = {}

    vecDS = loadVector(source)
    vecLyr: ogr.Layer = vecDS.GetLayer()
    info["srs"] = vecLyr.GetSpatialRef()

    xMin, xMax, yMin, yMax = vecLyr.GetExtent()
    info["bounds"] = (xMin, yMin, xMax, yMax)
    info["xMin"] = xMin
    info["xMax"] = xMax
    info["yMin"] = yMin
    info["yMax"] = yMax

    info["count"] = vecLyr.GetFeatureCount()

    info["source"] = vecDS.GetDescription()

    info["attributes"] = []
    info["attribute_data_types_constant"] = {}
    info["attribute_data_types_str"] = {}
    layerDef: ogr.FeatureDefn = vecLyr.GetLayerDefn()
    for layer_number in range(layerDef.GetFieldCount()):
        field_definition: ogr.FieldDefn = layerDef.GetFieldDefn(layer_number)
        field_name = field_definition.GetName()
        field_type_constant = field_definition.GetType()
        field_type_constant_str = gdal.GetDataTypeName(field_type_constant)
        info["attributes"].append(field_name)
        info["attribute_data_types_constant"][field_name] = field_type_constant
        info["attribute_data_types_str"][field_name] = field_type_constant_str

    return vecInfo(**info)

warp

warp(
    source: load_raster_input,
    resampleAlg: Literal[
        "near",
        "bilinear",
        "cubic",
        "cubicspline",
        "lanczos",
        "average",
        "rms",
        "mode",
        "max",
        "min",
        "med",
        "Q1",
        "Q3",
        "sum",
    ] = "bilinear",
    cutline: str | Geometry | None = None,
    output: str | Path | None = None,
    pixelHeight: numeric | None = None,
    pixelWidth: numeric | None = None,
    srs: srs_input | None = None,
    bounds: tuple[numeric, numeric, numeric, numeric]
    | None = None,
    dtype: None | geokit_c_data_types_literal = None,
    noData: numeric | None = None,
    overwrite: bool = True,
    meta: None | dict[str, str] = None,
    **kwargs,
) -> Dataset | str

Warps a given raster source to another context.

  • Can be used to 'warp' a raster in memory to a raster on disk
Note:

Unless manually altered as keyword arguments, the gdal.Warp options 'targetAlignedPixels' and 'copyMetadata' are both set to True

Parameters:

  • source

    (Anything acceptable by loadRaster()) –

    The raster datasource to draw

  • srs

    (Anything acceptable to geokit.srs.loadSRS(); optional, default: None ) –

    The srs of the resulting raster * If not given, the raster's internal srs is assumed

  • resampleAlg

    (str; optional, default: 'bilinear' ) –

    The resampling algorithm to use when translating pixel values * Knowing which option to use can have significant impacts! * Options are: near , bilinear, cubic, cubicspline, lanczos, average, rms, mode, max, min, med, Q1, Q3, sum

  • cutline

    (str or ogr.Geometry; optional, default: None ) –

    The cutline to limit the drawn data too * If a string is given, it must be a path to a vector file * Values outside of the cutline are given the value 'cutlineFillValue' * Requires a warp

  • output

    (str; pathlib.Path optional, default: None ) –

    The path on disk where the new raster should be created

  • pixelHeight

    (numeric; optional, default: None ) –

    The pixel height (y-resolution) of the output raster * Only required if this value should be changed

  • pixelWidth

    (numeric; optional, default: None ) –

    The pixel width (x-resolution) of the output raster * Only required if this value should be changed

  • bounds

    (tuple; optional, default: None ) –

    The (xMin, yMin, xMax, yMax) limits of the output raster * Only required if this value should be changed

  • dtype

    (Type, str, or numpy-dtype; optional, default: None ) –

    If given, forces the processed data to be a particular datatype * Only required if this value should be changed * Example - A python numeric type such as bool, int, or float - A Numpy datatype such as numpy.uint8 or numpy.float64 - a String such as "Byte", "UInt16", or "Double"

  • noData

    (numeric; optional, default: None ) –

    Replaces all previous noData values with this value in the output raster.

  • meta

    (None | dict[str, str], default: None ) –

    output gdal.dataset using the SetMetadataItem method.

  • **kwargs

    • All keyword arguments are passed on to a call to gdal.WarpOptions
    • Use these to fine-tune the warping procedure
    • Key Options are (from gdal.WarpOptions): format --- output format ("GTiff", etc...) targetAlignedPixels --- whether to force output bounds to be multiple of output resolution workingType --- working type (gdal.GDT_Byte, etc...) warpMemoryLimit --- size of working buffer in bytes creationOptions --- list of creation options srcNodata --- source nodata value(s) dstNodata --- output nodata value(s) multithread --- whether to multithread computation and I/O operations cutlineWhere --- cutline WHERE clause cropToCutline --- whether to use cutline extent for output bounds setColorInterpretation --- whether to force color interpretation of input bands to output bands

Returns:

  • * If 'output' is None: gdal.Dataset
  • * If 'output' is a string: The path to the output is returned (for easy opening)
Source code in geokit/core/raster.py
def warp(
    source: load_raster_input,
    resampleAlg: Literal[
        "near",
        "bilinear",
        "cubic",
        "cubicspline",
        "lanczos",
        "average",
        "rms",
        "mode",
        "max",
        "min",
        "med",
        "Q1",
        "Q3",
        "sum",
    ] = "bilinear",
    cutline: str | ogr.Geometry | None = None,
    output: str | pathlib.Path | None = None,
    pixelHeight: numeric | None = None,
    pixelWidth: numeric | None = None,
    srs: srs_input | None = None,
    bounds: tuple[numeric, numeric, numeric, numeric] | None = None,
    dtype: None | geokit_c_data_types_literal = None,
    noData: numeric | None = None,
    overwrite: bool = True,
    meta: None | dict[str, str] = None,
    **kwargs,
) -> gdal.Dataset | str:
    """Warps a given raster source to another context.

    * Can be used to 'warp' a raster in memory to a raster on disk

    Note:
    -----
    Unless manually altered as keyword arguments, the gdal.Warp options
    'targetAlignedPixels' and 'copyMetadata' are both set to True

    Parameters
    ----------
    source : Anything acceptable by loadRaster()
        The raster datasource to draw

    srs : Anything acceptable to geokit.srs.loadSRS(); optional
        The srs of the resulting raster
          * If not given, the raster's internal srs is assumed

    resampleAlg : str; optional
        The resampling algorithm to use when translating pixel values
        * Knowing which option to use can have significant impacts!
        * Options are: near , bilinear, cubic,
        cubicspline, lanczos, average, rms, mode,
        max, min, med, Q1, Q3, sum

    cutline : str or ogr.Geometry; optional
        The cutline to limit the drawn data too
        * If a string is given, it must be a path to a vector file
        * Values outside of the cutline are given the value 'cutlineFillValue'
        * Requires a warp

    output : str; pathlib.Path optional
        The path on disk where the new raster should be created

    pixelHeight : numeric; optional
        The pixel height (y-resolution) of the output raster
        * Only required if this value should be changed

    pixelWidth : numeric; optional
        The pixel width (x-resolution) of the output raster
        * Only required if this value should be changed

    bounds : tuple; optional
        The (xMin, yMin, xMax, yMax) limits of the output raster
        * Only required if this value should be changed

    dtype : Type, str, or numpy-dtype; optional
        If given, forces the processed data to be a particular datatype
        * Only required if this value should be changed
        * Example
          - A python numeric type  such as bool, int, or float
          - A Numpy datatype such as numpy.uint8 or numpy.float64
          - a String such as "Byte", "UInt16", or "Double"

    noData : numeric; optional
        Replaces all previous noData values with this value in the output raster.

    meta: dict; optional: contains a key value pair that is passed to the
          output gdal.dataset using the SetMetadataItem method.

    **kwargs:
        * All keyword arguments are passed on to a call to gdal.WarpOptions
        * Use these to fine-tune the warping procedure
        * Key Options are (from gdal.WarpOptions):
            format --- output format ("GTiff", etc...)
            targetAlignedPixels --- whether to force output bounds to be multiple
                                    of output resolution
            workingType --- working type (gdal.GDT_Byte, etc...)
            warpMemoryLimit --- size of working buffer in bytes
            creationOptions --- list of creation options
            srcNodata --- source nodata value(s)
            dstNodata --- output nodata value(s)
            multithread --- whether to multithread computation and I/O operations
            cutlineWhere --- cutline WHERE clause
            cropToCutline --- whether to use cutline extent for output bounds
            setColorInterpretation --- whether to force color interpretation of
                                       input bands to output bands

    Returns
    -------
    * If 'output' is None: gdal.Dataset
    * If 'output' is a string: The path to the output is returned (for easy opening)
    """
    # open source and get info
    source = loadRaster(source)
    dsInfo = rasterInfo(sourceDS=source, compute_statistics=True)
    if dsInfo.scale != 1.0 or dsInfo.offset != 0.0:
        isAdjusted = True
    else:
        isAdjusted = False

    # Handle potentially missing arguments
    if srs is not None:
        srs = SRS.loadSRS(srs)
    if srs is None:
        srs = dsInfo.srs
        srsOkay = True
    else:
        if srs.IsSame(dsInfo.srs):
            srsOkay = True
        else:
            srsOkay = False

    if bounds is None:
        if srsOkay:
            bounds = dsInfo.bounds
        else:
            bounds = GEOM.boundsToBounds(dsInfo.bounds, dsInfo.srs, srs)

    if pixelHeight is None:
        if srsOkay:
            pixelHeight = dsInfo.dy
        else:
            pixelHeight = (bounds[3] - bounds[1]) / (dsInfo.yWinSize * 1.1)

    if pixelWidth is None:
        if srsOkay:
            pixelWidth = dsInfo.dx
        else:
            pixelWidth = (bounds[2] - bounds[0]) / (dsInfo.xWinSize * 1.1)
    bounds = UTIL.fitBoundsTo(bounds, pixelWidth, pixelHeight)

    if noData is None:
        noDataRead = dsInfo.noData
    else:
        noDataRead = noData
    list_of_numbers = []
    if isinstance(noDataRead, (numeric, bool)):
        list_of_numbers.append(noDataRead)
    elif noDataRead is None:
        pass
    else:
        raise GeoKitRasterError("noData must be a numeric or boolean value but got: %s" % str(type(noDataRead)))

    list_of_datatypes = []
    if isinstance(dtype, str):
        list_of_datatypes.append(dtype)
    elif dtype is None:
        pass
    else:
        raise GeoKitRasterError("dtype must be a gdal data type, string or None value but got: %s" % str(type(dtype)))
    list_of_datatypes.append(dsInfo.data_type_name_str)
    list_of_numbers.append(dsInfo.minimum_value)
    list_of_numbers.append(dsInfo.maximum_value)

    # If a cutline is given, create the output
    if cutline is not None:
        if isinstance(cutline, ogr.Geometry):
            tempdir = TemporaryDirectory()
            cutline = UTIL.quickVector(cutline, output=os.path.join(tempdir.name, "tmp.shp"))
        # cutline is already a path to a vector
        elif UTIL.isVector(cutline):
            tempdir = None
        else:
            raise GeoKitRasterError("cutline must be a Geometry or a path to a shape file")

    # Workflow depends on whether or not we have an output
    if isinstance(output, pathlib.Path):
        output = str(output)
    if isinstance(output, str):  # Simply do a translate
        if os.path.isfile(output):
            if overwrite is True:
                os.remove(output)
                if os.path.isfile(output + ".aux.xml"):  # Because QGIS....
                    os.remove(output + ".aux.xml")
            else:
                raise GeoKitRasterError("Output file already exists: %s" % output)

        gdal_data_type_constant = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_constant(
            list_of_numbers=list_of_numbers,
            minimum_gdal_type_list=list_of_datatypes,
            user_defined_minimum_gdal_type=dtype,
        )

        # # Check some for bad input configurations
        # if not srs is None:
        #     if (pixelHeight is None or pixelWidth is None):
        #         raise GeoKitRasterError("When warping between srs's and writing to a file, pixelWidth and pixelHeight must be given")

        # Arange inputs
        co = kwargs.pop("creationOptions", COMPRESSION_OPTION)
        copyMeta = kwargs.pop("copyMetadata", True)
        aligned = kwargs.pop("targetAlignedPixels", True)

        # Fix the bounds issue by making them  just a little bit smaller, which should be fixed by gdalwarp
        bounds = (
            bounds[0] + 0.001 * pixelWidth,
            bounds[1] + 0.001 * pixelHeight,
            bounds[2] - 0.001 * pixelWidth,
            bounds[3] - 0.001 * pixelHeight,
        )

        # Let gdalwarp do everything...
        gdal_warp_options = gdal.WarpOptions(
            outputType=gdal_data_type_constant,
            xRes=pixelWidth,
            yRes=pixelHeight,
            creationOptions=co,
            outputBounds=bounds,
            dstSRS=srs,
            dstNodata=noDataRead,
            resampleAlg=resampleAlg,
            copyMetadata=copyMeta,
            targetAlignedPixels=aligned,
            cutlineDSName=cutline,
            **kwargs,
        )

        result_dataset = gdal.Warp(destNameOrDestDS=output, srcDSOrSrcDSTab=source, options=gdal_warp_options)
        if not UTIL.isRaster(result_dataset):
            raise GeoKitRasterError("Failed to translate raster")

        destination_raster = output

    else:
        if "cropToCutline" in kwargs:
            msg = "The 'cropToCutline' option is not taken into account when writing to a raster in memory. Try using geokit.Extent.warp instead"
            warnings.warn(msg, UserWarning)
        gdal_data_type_string = MinimumCDataTypeHandler.get_valid_gdal_data_type_as_string(
            list_of_numbers=list_of_numbers,
            minimum_gdal_type_list=list_of_datatypes,
            user_defined_minimum_gdal_type=dtype,
        )

        # Warp to a raster in memory
        destination_raster = UTIL.quickRaster(
            bounds=bounds,
            srs=srs,
            dx=pixelWidth,
            dy=pixelHeight,
            dtype=gdal_data_type_string,
            noData=noDataRead,
        )

        # Do a warp
        result_dataset = gdal.Warp(destination_raster, source, resampleAlg=resampleAlg, cutlineDSName=cutline, **kwargs)

        destination_raster.FlushCache()
    # Do we have meta data?
    if meta is not None:
        if isinstance(destination_raster, str):
            loaded_output_raster = loadRaster(destination_raster, 1)
        else:
            loaded_output_raster = destination_raster

        for k, v in meta.items():
            loaded_output_raster.SetMetadataItem(k, v)

        # FlushCache writes all changes to disk
        loaded_output_raster.FlushCache()

    if cutline is not None:
        del tempdir
    return destination_raster

warpLike

warpLike(
    dataSource: load_raster_input,
    contextSource: load_raster_input,
    copyMetadata: bool = False,
    **kwargs,
)

Convenience function to warp a raster to the context of another raster as returned from a call to rasterInfo(contextSource).

dataSource : Anything acceptable by loadRaster() The raster data source to draw contextSource : Anything acceptable by loadRaster() The raster context source to draw, i.e. the data source raster will be warped to this pixelWidth, pixelHeight, bounds, extent, srs etc. copyMetadata : bool, optional If True, the metadata of the dataSource raster will be copied, else metadata will be empty or as possibly provided in kwargs. Defaults to False. **kwargs All kwargs will be passed on to raster.warp() NOTE: If no 'dtype' value as kwargs is given, dtype will be defined automatically based on the value range, this can be time-consuming depending on data size. Avoid by specifying dtype explicitly.

Source code in geokit/core/raster.py
def warpLike(dataSource: load_raster_input, contextSource: load_raster_input, copyMetadata: bool = False, **kwargs):
    """
    Convenience function to warp a raster to the context of another raster
    as returned from a call to rasterInfo(contextSource).

    dataSource : Anything acceptable by loadRaster()
        The raster data source to draw
    contextSource : Anything acceptable by loadRaster()
        The raster context source to draw, i.e. the data source raster will be
        warped to this pixelWidth, pixelHeight, bounds, extent, srs etc.
    copyMetadata : bool, optional
        If True, the metadata of the dataSource raster will be copied, else
        metadata will be empty or as possibly provided in kwargs. Defaults to False.
    **kwargs
        All kwargs will be passed on to raster.warp()
        NOTE: If no 'dtype' value as kwargs is given, dtype will be defined
        automatically based on the value range, this can be time-consuming
        depending on data size. Avoid by specifying dtype explicitly.
    """
    if UTIL.isRaster(dataSource):
        dataInfo = rasterInfo(dataSource)
    if not isinstance(dataInfo, RasterInfo):
        raise GeoKitRasterError("Could not understand dataSource")
    if UTIL.isRaster(contextSource):
        contextInfo = rasterInfo(contextSource)
    if not isinstance(contextInfo, RasterInfo):
        raise GeoKitRasterError("Could not understand contextSource")

    # first get data-related parameters from DATA source
    if copyMetadata:
        if "meta" in kwargs:
            raise GeoKitRasterError("If metadata is given as 'meta' in kwargs, copyMetadata cannot be True!")
        meta = dataInfo.meta
    else:
        meta = kwargs.pop("meta", None)

    # then get context related parameters from CONTEXT source
    bounds = kwargs.pop("bounds", contextInfo.bounds)
    pixelWidth = kwargs.pop("pixelWidth", contextInfo.pixelWidth)
    pixelHeight = kwargs.pop("pixelHeight", contextInfo.pixelHeight)
    srs = kwargs.pop("srs", contextInfo.srs)
    noData = kwargs.pop("noData", contextInfo.noData)

    return warp(
        source=dataSource,
        bounds=bounds,
        pixelWidth=pixelWidth,
        pixelHeight=pixelHeight,
        srs=srs,
        noData=noData,
        meta=meta,
        **kwargs,
    )

xyTransform

xyTransform(
    *args,
    toSRS: srs_input,
    fromSRS: srs_input,
    outputFormat: Literal["raw", "xy", "xyz"] = "raw",
) -> (
    list[tuple] | TransformedPointsXY | TransformedPointsXYZ
)

Transform xy points between coordinate systems.

Returns:

  • list of tuples, or namedtuple
    • See the point for the 'outputFormat' argument
Source code in geokit/core/srs.py
def xyTransform(
    *args, toSRS: srs_input, fromSRS: srs_input, outputFormat: Literal["raw", "xy", "xyz"] = "raw"
) -> list[tuple] | TransformedPointsXY | TransformedPointsXYZ:
    """Transform xy points between coordinate systems.

    Parameters
    ----------
        xy : A single, or an iterable of (x,y) tuples
            The coordinates to transform

        toSRS : Anything acceptable by geokit.srs.loadSRS
            The srs of the output points

        fromSRS : Anything acceptable by geokit.srs.loadSRS
            The srs of the input points

        outputFormat : str
            Determine return value format
            * if 'raw', the raw output from osr.TransformPoints is given
            * if 'xy', or 'xyz' the points are given as named tuples

    Returns
    -------
    list of tuples, or namedtuple
      * See the point for the 'outputFormat' argument
    """
    # load srs's
    fromSRS = loadSRS(fromSRS)
    toSRS = loadSRS(toSRS)

    # make a transformer
    trx = osr.CoordinateTransformation(fromSRS, toSRS)

    # Do transformation
    if len(args) == 0:
        raise GeoKitSRSError("no positional inputs given")
    elif len(args) == 1:
        xy = args[0]
        if isinstance(xy, tuple):
            x, y = xy
            out = [
                trx.TransformPoint(x, y),
            ]
        else:
            out = trx.TransformPoints(xy)
    elif len(args) == 2:
        x = np.array(args[0])
        y = np.array(args[1])
        xy = np.column_stack([x, y])

        out = trx.TransformPoints(xy)

    else:
        raise GeoKitSRSError("Too many positional inputs")
    # Done!
    if outputFormat == "raw":
        return out
    elif outputFormat == "xy":
        x = np.array([o[0] for o in out])
        y = np.array([o[1] for o in out])

        return TransformedPointsXY(x, y)

    elif outputFormat == "xyz":
        x = out[:, 0]
        y = out[:, 1]
        z = out[:, 2]

        return TransformedPointsXYZ(x, y, z)