Skip to content

io_

read_cad_gdf(path, crs=None)

Read a GeoDataFrame from a DXF/CAD file with warning suppression.

This is useful in tandem with Raster.clip and rastr.create.rasterize_z_gdf to create rasters from CAD files. Often CAD files represent surfaces which for GIS contexts are better represented as rasters. In other cases, CAD files represent geometries but their difficult representation means that it is often easier to first rasterize them, and then spatially join with a clean vector representation developed separately.

DXF files often have large geometries that can trigger warnings during reading. This function suppresses those warnings and ensures proper CRS handling.

Supports any format supported by geopandas.read_file which provides 3D geometries.

Parameters:

Name Type Description Default
path Path | str

Path to the CAD file.

required
crs CRS | str | None

Optional CRS for the output GeoDataFrame. If None, uses the CRS from the CAD file.

None

Returns:

Name Type Description
GeoDataFrame GeoDataFrame

The CAD data as a GeoDataFrame.

Raises:

Type Description
ValueError

If CRS is missing from CAD file and not provided, or if CRS is inconsistent between CAD file and provided CRS.

Source code in src/rastr/io_.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def read_cad_gdf(path: Path | str, crs: CRS | str | None = None) -> gpd.GeoDataFrame:
    """Read a GeoDataFrame from a DXF/CAD file with warning suppression.

    This is useful in tandem with `Raster.clip` and `rastr.create.rasterize_z_gdf` to
    create rasters from CAD files. Often CAD files represent surfaces which for GIS
    contexts are better represented as rasters. In other cases, CAD files represent
    geometries but their difficult representation means that it is often easier to first
    rasterize them, and then spatially join with a clean vector representation
    developed separately.

    DXF files often have large geometries that can trigger warnings during reading.
    This function suppresses those warnings and ensures proper CRS handling.

    Supports any format supported by `geopandas.read_file` which provides 3D geometries.

    Args:
        path: Path to the CAD file.
        crs: Optional CRS for the output GeoDataFrame. If None, uses the CRS from the
             CAD file.

    Returns:
        GeoDataFrame: The CAD data as a GeoDataFrame.

    Raises:
        ValueError: If CRS is missing from CAD file and not provided, or if CRS is
                    inconsistent between CAD file and provided CRS.
    """
    import geopandas as gpd

    path = Path(path)

    with warnings.catch_warnings():
        # The CAD-derived geometries are really messy, so we can get warnings
        # about large polygons, but we're going to handle those.
        warnings.filterwarnings(
            "ignore",
            message=".*Non closed ring detected.*",
            category=RuntimeWarning,
        )

        # Read the CAD file using geopandas
        gdf = gpd.read_file(path)

    # Handle CRS logic
    crs = CRS.from_user_input(crs) if crs is not None else None
    if crs is None:
        if gdf.crs is None:
            msg = (
                f"No CRS found in CAD file {path} and no CRS provided. "
                "Please provide a CRS parameter."
            )
            raise ValueError(msg)
    # Check if CRS are consistent
    elif gdf.crs is not None and not gdf.crs.equals(crs):
        # Reproject if inconsistent
        gdf = gdf.to_crs(crs)
    elif gdf.crs is None and crs is not None:
        gdf = gdf.set_crs(crs)

    return gdf

read_raster_inmem(raster_path, *, crs=None, cls=Raster)

Read raster data from a file and return an in-memory Raster object.

Parameters:

Name Type Description Default
raster_path Path | str

Path to the raster file.

required
crs CRS | str | None

Optional CRS to override the raster's native CRS.

None
cls type[R]

The Raster subclass to instantiate. This is mostly for internal use, but can be useful if you have a custom Raster subclass.

Raster
Source code in src/rastr/io_.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def read_raster_inmem(
    raster_path: Path | str,
    *,
    crs: CRS | str | None = None,
    cls: type[R] = Raster,
) -> R:
    """Read raster data from a file and return an in-memory Raster object.

    Args:
        raster_path: Path to the raster file.
        crs: Optional CRS to override the raster's native CRS.
        cls: The Raster subclass to instantiate. This is mostly for internal use,
             but can be useful if you have a custom `Raster` subclass.
    """
    crs = CRS.from_user_input(crs) if crs is not None else None

    with rasterio.open(raster_path, mode="r") as dst:
        # Read the entire array
        raw_arr: NDArray = dst.read()
        raw_arr = raw_arr.squeeze()

        # Extract metadata
        if crs is None:
            try:
                crs = CRS.from_user_input(dst.crs)
            except CRSError as e:
                msg = (
                    f"Invalid CRS from input raster and no override CRS provided "
                    f"(crs:{crs!r})."
                )
                raise ValueError(msg) from e
        transform = dst.transform
        nodata = dst.nodata

        # Cast integers to float16 to handle NaN values
        if np.issubdtype(raw_arr.dtype, np.integer):
            arr = raw_arr.astype(np.float16)
        else:
            arr = raw_arr

        if nodata is not None:
            arr[raw_arr == nodata] = np.nan

    raster_meta = RasterMeta(crs=crs, transform=transform)
    raster_obj = cls(arr=arr, raster_meta=raster_meta)
    return raster_obj

read_raster_mosaic_inmem(mosaic_dir, *, glob='*.tif', crs=None)

Read a raster mosaic from a directory and return an in-memory Raster object.

This assumes that all rasters have the same metadata, e.g. coordinate system, cell size, etc.

Source code in src/rastr/io_.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def read_raster_mosaic_inmem(
    mosaic_dir: Path | str, *, glob: str = "*.tif", crs: CRS | None = None
) -> Raster:
    """Read a raster mosaic from a directory and return an in-memory Raster object.

    This assumes that all rasters have the same metadata, e.g. coordinate system,
    cell size, etc.
    """
    mosaic_dir = Path(mosaic_dir)
    raster_paths = list(mosaic_dir.glob(glob))
    if not raster_paths:
        msg = f"No raster files found in {mosaic_dir} matching {glob}"
        raise FileNotFoundError(msg)

    # Sort raster_paths in alphabetical order by stem
    raster_paths.sort(key=lambda p: p.stem)

    # Open all TIFF datasets using context managers to ensure proper closure
    sources = []
    try:
        for raster_path in raster_paths:
            src = rasterio.open(raster_path)
            sources.append(src)

        # Merge into a single mosaic array & transform
        arr, transform = rasterio.merge.merge(sources)

        # Copy metadata from the first dataset
        out_meta = sources[0].meta.copy()
        out_meta.update(
            {
                "driver": "GTiff",
                "height": arr.shape[1],
                "width": arr.shape[2],
                "transform": transform,
            }
        )
        if crs is None:
            crs = CRS.from_user_input(sources[0].crs)

        nodata = sources[0].nodata
        raw_arr = arr.squeeze()

        # Cast integers to float16 to handle NaN values
        if np.issubdtype(raw_arr.dtype, np.integer):
            arr = raw_arr.astype(np.float16)
        else:
            arr = raw_arr

        if nodata is not None:
            arr[raw_arr == nodata] = np.nan

        raster_meta = RasterMeta(crs=crs, transform=transform)
        raster_obj = Raster(arr=arr, raster_meta=raster_meta)
        return raster_obj
    finally:
        for src in sources:
            src.close()

write_raster(raster, *, path, **kwargs)

Write the raster to a file.

Parameters:

Name Type Description Default
raster Raster

The Raster object to write.

required
path Path | str

Path to output file.

required
**kwargs Any

Additional keyword arguments to pass to rasterio.open(). If nodata is provided, NaN values in the raster will be replaced with the nodata value.

{}
Source code in src/rastr/io_.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def write_raster(raster: Raster, *, path: Path | str, **kwargs: Any) -> None:
    """Write the raster to a file.

    Args:
        raster: The Raster object to write.
        path: Path to output file.
        **kwargs: Additional keyword arguments to pass to `rasterio.open()`. If
                    `nodata` is provided, NaN values in the raster will be replaced
                    with the nodata value.
    """
    path = Path(path)

    suffix = path.suffix.lower()
    if suffix in (".tif", ".tiff"):
        driver = "GTiff"
    elif suffix in (".grd"):
        # https://grapherhelp.goldensoftware.com/subsys/ascii_grid_file_format.htm
        # e.g. Used by AnAqSim
        driver = "GSAG"
    else:
        msg = f"Unsupported file extension: {suffix}"
        raise ValueError(msg)

    # Handle nodata: use provided value or default to np.nan
    if "nodata" in kwargs:
        # Replace NaN values with the nodata value
        nodata_value = kwargs.pop("nodata")
        arr_to_write = np.where(np.isnan(raster.arr), nodata_value, raster.arr)
    else:
        nodata_value = np.nan
        arr_to_write = raster.arr

    with rasterio.open(
        path,
        "w",
        driver=driver,
        height=raster.arr.shape[0],
        width=raster.arr.shape[1],
        count=1,
        dtype=raster.arr.dtype,
        crs=raster.raster_meta.crs,
        transform=raster.raster_meta.transform,
        nodata=nodata_value,
        **kwargs,
    ) as dst:
        try:
            dst.write(arr_to_write, 1)
        except CPLE_BaseError as err:
            msg = f"Failed to write raster to file: {err}"
            raise OSError(msg) from err