├── .gitignore ├── LICENSE.md ├── README.md ├── __init__.py ├── geoarray ├── __init__.py ├── core.py ├── gdalfuncs.py ├── gdalio.py ├── gdalspatial.py ├── geotrans.py ├── spatial.py ├── utils.py └── wrapper.py ├── setup.py └── test ├── __init__.py ├── test_core.py ├── test_gdalio.py ├── test_methods.py ├── test_projection.py ├── test_utils.py └── test_wrapper.py /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled sources # 2 | # ---------------- 3 | *.pyc 4 | 5 | # emacs tmp files # 6 | # --------------- 7 | *~ 8 | \#* 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2015 David Schaefer 3 | 4 | This program is free software: you can redistribute it and/or modify 5 | it under the terms of the GNU General Public License as published by 6 | the Free Software Foundation, either version 3 of the License, or 7 | (at your option) any later version. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # geoarray 2 | 3 | # Purpose 4 | This python GDAL wrapper module provides a numpy.ma.MaskedArray subclass and a number of initializer 5 | functions to facilitate the work with array-like data in a geographically explicit context. 6 | 7 | # Requirements 8 | - GDAL >= 2.1 9 | - numpy >= 1.11 10 | 11 | # General 12 | This module tries to imitate the general numpy functionality as closly as possible. 13 | As a MaskedArray subclass a GeoArray Instance is (hopefully) usable wherever its parents are. 14 | 15 | # Usage 16 | 17 | ## I/O 18 | 19 | Existing files can be read with the ```fromfile``` function. 20 | 21 | ```python 22 | # read the dataset 23 | grid = ga.fromfile("yourfile.tif") 24 | ``` 25 | 26 | There are a bunch of wrapper functions like ```array, zeros, ones, empty, full``` which do 27 | what their numpy counterparts do. 28 | 29 | ```python 30 | # import the module to an handy alias 31 | import geoarray as ga 32 | 33 | # the most basic initialization gives not much more than a MaskedArray 34 | grid = ga.zeros((300,250)) 35 | 36 | # Add some geospatial information 37 | grid = ga.zeros((300,250), yorigin=1000, xorigin=850, cellsize=50) 38 | 39 | # The origin of the grid defaults to the upper left corner. 40 | # The options are "ul", "ll", "ur", "lr", i. e. "upper left", "lower left", "upper right", "lower right" 41 | grid = ga.zeros((300,250), yorigin=1000, xorigin=850, cellsize=50, origin="ll") 42 | 43 | # If no fill_value is given, the smallest value of the datatype is used 44 | grid = ga.zeros((300,250), yorigin=1000, xorigin=850, cellsize=50, origin="ll", fill_value=-9999) 45 | 46 | # You can add projection information as a pyproj compatible dictionary, a wkt string or epsg code 47 | grid = ga.zeros((300,250), yorigin=1000, xorigin=850, cellsize=50, origin="ll", fill_value=-9999, proj=3857) 48 | 49 | ``` 50 | 51 | ## Arithmetic 52 | 53 | As a subclass of MaskedArray (and therefore also of ndarray) GeoArray instances can be passed to 54 | all numpy functions and accept the usual operators 55 | 56 | ```python 57 | grid *= .8 58 | 59 | grid2 = grid + 42 60 | 61 | grid3 = np.exp(grid) 62 | ``` 63 | 64 | ## Transformations 65 | 66 | Coordinate transformations are as easy as 67 | ```python 68 | pgrid = ga.project(grid, proj=2062) 69 | ``` 70 | 71 | # Slicing 72 | GeoArray overrides the usual slicing behaviour in order to preserve the spatial context. The yorigin 73 | and xorigin attributes are updated according to the given origin of the instance. 74 | 75 | The geographic extend of the array is determined by the first/last given index: 76 | 77 | ```python 78 | # grid with ymax=15000 and ymin=12000 79 | grid = ga.zeros((300,250), origin="ul", cellsize=10, yorigin=15000, xorigin=10000) 80 | 81 | # grid with ymax=14980 and ymin=12020 82 | grid[2:-2] 83 | ``` 84 | 85 | Skipping cells along the coordinate axis, will alter the cellsize: 86 | ```python 87 | # grid with ymax=14980, ymin=12020 and cellsize=(-20,10) 88 | grid[2:-2:2] 89 | ``` 90 | 91 | Fancy slicing follows the same semantics: 92 | ```python 93 | # grid with ymax=15000, ymin=14720 and cellsize=(-70,10) 94 | grid[np.array([0,3,6,21])] 95 | ``` 96 | 97 | Note, that the *last* index limits the rectangle, *not* the highest: 98 | ```python 99 | # grid with ymax=15000, ymin=14920 and cellsize=(-20,10) 100 | grid[np.array([0,3,21,6])] 101 | ``` 102 | 103 | # Restrictions 104 | - GDAL supports many different raster data formats, but only the Geotiff, Arc/Info Ascii Grid, Erdas Imagine, SAGA and PNG formats are currently supported output formats. 105 | - When converting between data formats, GDAL automatically adjusts the datatypes and truncates values. You might loose information that way. 106 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .geoarray import * 5 | -------------------------------------------------------------------------------- /geoarray/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from .wrapper import ( 5 | array, 6 | zeros, 7 | ones, 8 | full, 9 | empty, 10 | zeros_like, 11 | ones_like, 12 | full_like, 13 | fromfile, 14 | fromdataset, 15 | ) 16 | 17 | from .gdalfuncs import ( 18 | resample, 19 | project, 20 | rescale, 21 | ) 22 | 23 | from .gdalio import ( 24 | _DRIVER_DICT, 25 | # fromfile, 26 | ) 27 | -------------------------------------------------------------------------------- /geoarray/core.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Author 6 | ------ 7 | David Schaefer 8 | 9 | Purpose 10 | ------- 11 | This module provides a numpy.ma.MaskedArray as a 12 | wrapper around gdal raster functionality 13 | 14 | """ 15 | import os 16 | import copy 17 | import numpy as np 18 | import warnings 19 | from numpy.ma import MaskedArray 20 | from .utils import _broadcastedMeshgrid, _broadcastTo 21 | from .gdalspatial import _Projection 22 | from .gdalio import _toDataset, _toFile, _writeData 23 | from .geotrans import _Geotrans 24 | from .spatial import SpatialMixin 25 | 26 | 27 | # Possible positions of the grid origin 28 | ORIGINS = ( 29 | "ul", # "ul" -> upper left 30 | "ur", # "ur" -> upper right 31 | "ll", # "ll" -> lower left 32 | "lr", # "lr" -> lower right 33 | ) 34 | 35 | _METHODS = ( 36 | # comparison 37 | "__lt__", "__le__", "__gt__", "__ge__", "__eq__", "__ne__", "__nonzero__", 38 | 39 | # unary 40 | "__neg__", "__pos__", "__abs__", "__invert__", 41 | 42 | # arithmetic 43 | "__add__", "__sub__", "__mul__", "__div__", "__truediv__", 44 | "__floordiv__", "__mod__", "__divmod__", "__pow__", "__lshift__", 45 | "__rshift__", "__and__", "__or__", "__xor__", # "__matmul__", 46 | 47 | # arithmetic, in-place 48 | "__iadd__", "__isub__", "__imul__", "__idiv__", "__itruediv__", 49 | "__ifloordiv__", "__imod__", "__ipow__", "__ilshift__", "__irshift__", 50 | "__iand__", "__ior__", "__ixor__", # "__imatmul__" 51 | ) 52 | 53 | 54 | def _checkMatch(func): 55 | def inner(*args): 56 | if len({a.proj for a in args if isinstance(a, GeoArray)}) > 1: 57 | warnings.warn("Incompatible map projections!", RuntimeWarning) 58 | if len({a.cellsize for a in args if isinstance(a, GeoArray)}) != 1: 59 | warnings.warn("Incompatible cellsizes", RuntimeWarning) 60 | if len({a.getCorner("ul") for a in args 61 | if isinstance(a, GeoArray)}) != 1: 62 | warnings.warn("Incompatible origins", RuntimeWarning) 63 | return func(*args) 64 | return inner 65 | 66 | 67 | class GeoArrayMeta(object): 68 | def __new__(cls, name, bases, attrs): 69 | for key in _METHODS: 70 | attrs[key] = _checkMatch(getattr(MaskedArray, key)) 71 | return type(name, bases, attrs) 72 | 73 | 74 | class GeoArray(SpatialMixin, MaskedArray): 75 | """ 76 | Arguments 77 | ---------- 78 | TODO 79 | 80 | Purpose 81 | ------- 82 | This numpy.ndarray subclass adds geographic context to data. 83 | A (hopfully growing) number of operations on the data I/O to/from 84 | different file formats (see the variable gdalfuncs._DRIVER_DICT) 85 | is supported. 86 | 87 | Restrictions 88 | ------------ 89 | Adding the geographic information to the data does (at the moment) 90 | not imply any additional logic. If the shapes of two grids allow 91 | the succesful execution of a certain operator/function your program 92 | will continue. It is within the responsability of the user to check 93 | whether a given operation makes sense within a geographic context 94 | (e.g. grids cover the same spatial domain, share a common projection, 95 | etc.) or not 96 | """ 97 | 98 | __metaclass__ = GeoArrayMeta 99 | 100 | def __new__( 101 | cls, data, geotrans=None, 102 | proj=None, fill_value=None, fobj=None, color_mode=None, # mask=None, 103 | yvalues=None, xvalues=None, mode="r", *args, **kwargs): 104 | 105 | # NOTE: The mask will always be calculated, even if its 106 | # already present or not needed at all... 107 | mask = (np.zeros_like(data, np.bool) 108 | if fill_value is None else data == fill_value) 109 | 110 | self = MaskedArray.__new__( 111 | cls, data=data, fill_value=fill_value, mask=mask, *args, **kwargs) 112 | self.unshare_mask() 113 | 114 | self.__dict__["geotrans"] = geotrans 115 | self.__dict__["proj"] = _Projection(proj) 116 | 117 | self.__dict__["color_mode"] = color_mode 118 | self.__dict__["mode"] = mode 119 | 120 | self.__dict__["_fobj"] = fobj 121 | self.__dict__["_yvalues"] = yvalues 122 | self.__dict__["_xvalues"] = xvalues 123 | 124 | return self 125 | 126 | def __array_finalize__(self, obj): 127 | if obj is None: 128 | return 129 | super(GeoArray, self).__array_finalize__(obj) 130 | self._update_from(obj) 131 | 132 | def _update_from(self, obj): 133 | 134 | super(GeoArray, self)._update_from(obj) 135 | 136 | self.__dict__["geotrans"] = getattr(obj, "geotrans", None) 137 | self.__dict__["proj"] = getattr(obj, "proj", None) 138 | self.__dict__["color_mode"] = getattr(obj, "color_mode", None) 139 | self.__dict__["mode"] = getattr(obj, "mode", None) 140 | self.__dict__["_fobj"] = getattr(obj, "_fobj", None) 141 | self.__dict__["_yvalues"] = getattr(obj, "_yvalues", None) 142 | self.__dict__["_xvalues"] = getattr(obj, "_xvalues", None) 143 | self.__dict__["_geolocation"] = getattr(obj, "_geolocation", None) 144 | 145 | @property 146 | def header(self): 147 | out = self._getArgs() 148 | out.update(out.pop("geotrans")._todict()) 149 | del out["data"] 150 | return out 151 | 152 | def _getShapeProperty(self, idx): 153 | try: 154 | return self.shape[idx] or 1 155 | except IndexError: 156 | return 1 157 | 158 | @property 159 | def nbands(self): 160 | return self._getShapeProperty(-3) 161 | 162 | @property 163 | def nrows(self): 164 | return self._getShapeProperty(-2) 165 | 166 | @property 167 | def ncols(self): 168 | return self._getShapeProperty(-1) 169 | 170 | @property 171 | def fobj(self): 172 | if self._fobj is None: 173 | self._fobj = _toDataset(self, mem=True) 174 | return self._fobj 175 | 176 | def getFillValue(self): 177 | return super(GeoArray, self).get_fill_value() 178 | 179 | def setFillValue(self, value): 180 | # change fill_value and update mask 181 | super(GeoArray, self).set_fill_value(value) 182 | self.mask = self.data == value 183 | if value != self.fill_value: 184 | warnings.warn( 185 | "Data types not compatible. New fill_value is: {:}" 186 | .format(self.fill_value)) 187 | 188 | # decorating the methods did not work out... 189 | fill_value = property(fget=getFillValue, fset=setFillValue) 190 | 191 | # def __getattribute__(self, key): 192 | # "Make descriptors work" 193 | # v = object.__getattribute__(self, key) 194 | # if hasattr(v, '__get__'): 195 | # return v.__get__(None, self) 196 | # return v 197 | 198 | # def __setattr__(self, key, value): 199 | # "Make descriptors work" 200 | # try: 201 | # object.__getattribute__(self, key).__set__(self, value) 202 | # except AttributeError: 203 | # object.__setattr__(self, key, value) 204 | 205 | def __setattr__(self, key, value): 206 | instance = self.geotrans if hasattr(self.geotrans, key) else self 207 | object.__setattr__(instance, key, value) 208 | 209 | 210 | def __getattr__(self, key): 211 | try: 212 | # return object.__getattribute__(self.geotrans, key) 213 | return getattr(self.geotrans, key) 214 | except AttributeError: 215 | raise AttributeError( 216 | "'GeoArray' object has no attribute '{:}'".format(key)) 217 | 218 | @property 219 | def fobj(self): 220 | if self._fobj is None: 221 | self._fobj = _toDataset(self, mem=True) 222 | return self._fobj 223 | 224 | def _getArgs(self, data=None, fill_value=None, 225 | geotrans=None, mode=None, color_mode=None, 226 | proj=None, fobj=None): 227 | 228 | return { 229 | "data" : data if data is not None else self.data, 230 | "geotrans" : geotrans if geotrans is not None else self.geotrans, 231 | "proj" : proj if proj is not None else self.proj, 232 | "fill_value" : fill_value if fill_value is not None else self.fill_value, 233 | "mode" : mode if mode is not None else self.mode, 234 | "color_mode" : color_mode if color_mode is not None else self.color_mode, 235 | "fobj" : fobj if fobj is not None else self._fobj} 236 | 237 | 238 | def fill(self, fill_value): 239 | """ 240 | works similar to MaskedArray.filled(value) but also changes 241 | the fill_value and returns an GeoArray instance 242 | """ 243 | return GeoArray( 244 | self._getArgs(data=self.filled(fill_value), fill_value=fill_value)) 245 | 246 | def __copy__(self): 247 | return GeoArray(**self._getArgs()) 248 | 249 | def __deepcopy__(self, memo): 250 | return GeoArray(**self._getArgs(data=self.data.copy())) 251 | 252 | def __getitem__(self, slc): 253 | 254 | data = MaskedArray.__getitem__(self, slc) 255 | 256 | # empty array 257 | if data.size == 0 or np.isscalar(data): 258 | return data 259 | 260 | geotrans = self.geotrans._getitem(slc) 261 | 262 | return GeoArray(**self._getArgs(data=data.data, geotrans=geotrans)) 263 | 264 | def flush(self): 265 | fobj = self._fobj 266 | if fobj is not None: 267 | fobj.FlushCache() 268 | if self.mode == "a": 269 | _writeData(self) 270 | 271 | def close(self): 272 | self.__del__() 273 | 274 | def __del__(self): 275 | # the virtual memory mapping needs to be released BEFORE the fobj 276 | self.flush() 277 | self._fobj = None 278 | 279 | def tofile(self, fname): 280 | _toFile(self, fname) 281 | -------------------------------------------------------------------------------- /geoarray/gdalfuncs.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import gdal, osr 5 | import numpy as np 6 | from math import ceil 7 | from .wrapper import array, full 8 | from .gdalio import _toDataset, _fromDataset 9 | from .gdalspatial import _Projection, _Transformer 10 | 11 | gdal.UseExceptions() 12 | gdal.PushErrorHandler('CPLQuietErrorHandler') 13 | 14 | _RESAMPLING = { 15 | # A documentation would be nice. 16 | # There seem to be more functions in GDAL > 2 17 | "average" : gdal.GRA_Average, 18 | "bilinear" : gdal.GRA_Bilinear, 19 | "cubic" : gdal.GRA_Cubic, 20 | "cubicspline" : gdal.GRA_CubicSpline, 21 | "lanczos" : gdal.GRA_Lanczos, 22 | "mode" : gdal.GRA_Mode, 23 | "nearest" : gdal.GRA_NearestNeighbour, 24 | # only available in GDAL > 2 25 | "max" : getattr(gdal, "GRA_Max", None), 26 | "min" : getattr(gdal, "GRA_Min", None), 27 | } 28 | 29 | def _warpTo(source, target, func, max_error=0.125): 30 | 31 | if func is None: 32 | raise TypeError("Resampling method {:} not available in your GDAL version".format(func)) 33 | 34 | target = np.atleast_2d(target) 35 | if target.ndim < source.ndim: 36 | target = np.broadcast_to( 37 | target, source.shape[:-len(target.shape)]+target.shape, subok=True) 38 | 39 | target = np.ma.array( 40 | target, 41 | mask = target.data==target.fill_value, 42 | dtype = source.dtype, 43 | copy = True, 44 | subok = True) 45 | 46 | target[target.mask] = source.fill_value 47 | target.fill_value = source.fill_value 48 | 49 | out = _toDataset(target, True) 50 | 51 | gdal.Warp(out, _toDataset(source), 52 | resampleAlg=_RESAMPLING[func], 53 | errorThreshold=max_error, 54 | # geoloc=True, 55 | # transformerOptions=["SRC_METHOD=NO_GEOTRANSFORM"] 56 | ) 57 | 58 | # gdal.ReprojectImage( 59 | # _toDataset(source), out, 60 | # None, None, 61 | # _RESAMPLING[func], 62 | # 0.0, max_error) 63 | # print _fromDataset(out) 64 | 65 | return _fromDataset(out) 66 | 67 | 68 | def project(grid, proj, cellsize=None, func="nearest", max_error=0.125): 69 | # type: (GeoArray, _Projection, Optional[float], str, float) -> GeoArray 70 | 71 | bbox = grid.bbox 72 | proj = _Projection(proj) 73 | trans = _Transformer(grid.proj, proj) 74 | uly, ulx = trans(bbox["ymax"], bbox["xmin"]) 75 | lry, lrx = trans(bbox["ymin"], bbox["xmax"]) 76 | ury, urx = trans(bbox["ymax"], bbox["xmax"]) 77 | lly, llx = trans(bbox["ymin"], bbox["xmin"]) 78 | 79 | # Calculate cellsize, i.e. same number of cells along the diagonal. 80 | if cellsize is None: 81 | src_diag = np.sqrt(grid.nrows**2 + grid.ncols**2) 82 | trg_diag = np.sqrt((lly - ury)**2 + (llx - urx)**2) 83 | cellsize = trg_diag/src_diag 84 | 85 | # number of cells 86 | ncols = int(abs(ceil((max(urx, lrx, ulx, llx) - min(urx, lrx, ulx, llx))/cellsize))) 87 | nrows = int(abs(ceil((max(ury, lry, uly, lly) - min(ury, lry, uly, lly))/cellsize))) 88 | 89 | target = array( 90 | data = np.full((grid.nbands, nrows, ncols), grid.fill_value, grid.dtype), 91 | fill_value = grid.fill_value, 92 | dtype = grid.dtype, 93 | yorigin = max(uly, ury, lly, lry), 94 | xorigin = min(ulx, urx, llx, lrx), 95 | origin = "ul", 96 | ycellsize = abs(cellsize) * -1, 97 | xcellsize = cellsize, 98 | # cellsize = cellsize, 99 | proj = proj, 100 | mode = grid.mode, 101 | ) 102 | 103 | out = resample( 104 | source = grid, 105 | target = target, 106 | func = func, 107 | max_error = max_error) 108 | return out.trim() 109 | 110 | 111 | def resample(source, target, func="nearest", max_error=0.125): 112 | return _warpTo( 113 | source=source, 114 | target=target, 115 | func=func, 116 | max_error=max_error) 117 | 118 | 119 | def rescale(source, scaling_factor, func="nearest"): 120 | shape = (list(source.shape[:-2]) + 121 | [int(s / scaling_factor) for s in source.shape[-2:]]) 122 | cellsize = tuple(c * scaling_factor for c in source.cellsize) 123 | scaled_grid = full(shape, source.fill_value, origin=source.origin, 124 | xorigin=source.xorigin, yorigin=source.yorigin, 125 | cellsize=cellsize, dtype=source.dtype) 126 | return resample(source, scaled_grid, func=func) 127 | -------------------------------------------------------------------------------- /geoarray/gdalio.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import warnings 6 | import numpy as np 7 | import gdal, osr 8 | from .gdalspatial import _Projection 9 | from .geotrans import _Geotrans, _Geolocation 10 | 11 | gdal.UseExceptions() 12 | gdal.PushErrorHandler('CPLQuietErrorHandler') 13 | 14 | # should be extended, for available options see: 15 | # http://www.gdal.org/formats_list.html 16 | _DRIVER_DICT = { 17 | ".tif" : "GTiff", 18 | ".asc" : "AAIGrid", 19 | ".img" : "HFA", 20 | ".sdat" : "SAGA", 21 | ".png" : "PNG", # not working properly 22 | } 23 | 24 | # type mapping: 25 | # - there is no boolean data type in GDAL 26 | _TYPEMAP = { 27 | "uint8" : 1, 28 | "int8" : 1, 29 | "uint16" : 2, 30 | "int16" : 3, 31 | "uint32" : 4, 32 | "int32" : 5, 33 | # "int64" : 5, # there is no int64 data type in GDAL, map to int32 and issue a warning 34 | "float32" : 6, 35 | "float64" : 7, 36 | "complex64" : 10, 37 | "complex128" : 11, 38 | 1 : "int8", 39 | 2 : "uint16", 40 | 3 : "int16", 41 | 4 : "uint32", 42 | 5 : "int32", 43 | 6 : "float32", 44 | 7 : "float64", 45 | 10 : "complex64", 46 | 11 : "complex128", 47 | 48 | } 49 | 50 | _COLOR_DICT = { 51 | 1 : "L", 52 | 2 : "P", 53 | 3 : "R", 54 | 4 : "G", 55 | 5 : "B", 56 | 6 : "A", 57 | 7 : "H", 58 | 8 : "S", 59 | 9 : "V", 60 | 10 : "C", 61 | 11 : "M", 62 | 12 : "Y", 63 | 13 : "K", 64 | 14 : "Y", 65 | 15 : "Cb", 66 | 16 : "Cr", 67 | } 68 | 69 | _COLOR_MODE_LIST = ( 70 | "L", "P", "RGB", "RGBA", "CMYK", "HSV", "YCbCr" 71 | ) 72 | 73 | _FILE_MODE_DICT = { 74 | "r" : gdal.GA_ReadOnly, 75 | "v" : gdal.GA_ReadOnly, 76 | "a" : gdal.GA_Update 77 | } 78 | 79 | def _fromFile(fname, mode="r"): 80 | """ 81 | Parameters 82 | ---------- 83 | fname : str # file name 84 | 85 | Returns 86 | ------- 87 | GeoArray 88 | 89 | Purpose 90 | ------- 91 | Create GeoArray from file 92 | 93 | """ 94 | 95 | if mode not in _FILE_MODE_DICT: 96 | raise TypeError("Supported file modes are: {:}".format( 97 | ", ".join(_FILE_MODE_DICT.keys()))) 98 | 99 | 100 | fobj = gdal.OpenShared(fname, _FILE_MODE_DICT[mode]) 101 | if fobj: 102 | return _fromDataset(fobj, mode) 103 | raise IOError("Could not open file: {:}".format(fname)) 104 | 105 | 106 | def _getColorMode(fobj): 107 | tmp = [] 108 | for i in range(fobj.RasterCount): 109 | color = fobj.GetRasterBand(i+1).GetColorInterpretation() 110 | tmp.append(_COLOR_DICT.get(color, "L")) 111 | return ''.join(sorted(set(tmp), key=tmp.index)) 112 | 113 | 114 | def _fromDataset(fobj, mode="r"): 115 | 116 | from .core import GeoArray 117 | 118 | def _parseGeotrans(geotrans): 119 | return { 120 | "yorigin": geotrans[3], 121 | "xorigin": geotrans[0], 122 | "ycellsize": geotrans[5], 123 | "xcellsize": geotrans[1], 124 | "origin": "ul", # is that always true? 125 | "yparam": geotrans[4], 126 | "xparam": geotrans[2]} 127 | 128 | fill_values = tuple( 129 | fobj.GetRasterBand(i+1).GetNoDataValue() for i in range(fobj.RasterCount)) 130 | 131 | if len(set(fill_values)) > 1: 132 | warnings.warn( 133 | "More then on fill value found. Only {:} will be used".format(fill_values[0]), 134 | RuntimeWarning) 135 | 136 | 137 | data = fobj.GetVirtualMemArray() if mode == "v" else fobj.ReadAsArray() 138 | # NOTE: not to robust... 139 | geotrans = _Geotrans(shape=data.shape, **_parseGeotrans(fobj.GetGeoTransform())) 140 | 141 | return GeoArray( 142 | data = data, 143 | fill_value = fill_values[0], 144 | proj = _Projection(fobj.GetProjection()), 145 | mode = mode, 146 | color_mode = _getColorMode(fobj), 147 | fobj = fobj, 148 | geotrans = geotrans) 149 | 150 | 151 | def _toDataset(grid, mem=False): 152 | 153 | # Returns an gdal memory dataset created from the given grid 154 | 155 | if grid._fobj and not mem: 156 | return grid._fobj 157 | 158 | driver = gdal.GetDriverByName("MEM") 159 | 160 | try: 161 | out = driver.Create("", grid.ncols, grid.nrows, grid.nbands, 162 | _TYPEMAP[str(grid.dtype)]) 163 | except KeyError: 164 | raise RuntimeError("Datatype {:} not supported by GDAL".format(grid.dtype)) 165 | 166 | if isinstance(grid.geotrans, _Geotrans): 167 | out.SetGeoTransform(grid.toGdal()) 168 | elif isinstance(grid.geotrans, _Geolocation): 169 | out.SetMetadata(grid.toGdal(), "GEOLOCATION") 170 | 171 | if grid.proj: 172 | out.SetProjection(grid.proj.toWkt()) 173 | 174 | for n in range(grid.nbands): 175 | band = out.GetRasterBand(n+1) 176 | if grid.fill_value is not None: 177 | band.SetNoDataValue(float(grid.fill_value)) 178 | data = grid[n] if grid.ndim > 2 else grid 179 | band.WriteArray(data) 180 | 181 | # if isinstance(grid.geotrans, _Geolocation): 182 | # for i, data in enumerate([grid.yvalues, grid.xvalues]): 183 | # print (n + 1 + i, nbands) 184 | # band = out.GetRasterBand(n + 1 + i) 185 | # if grid.fill_value is not None: 186 | # band.SetNoDataValue(float(grid.fill_value)) 187 | # band.WriteArray(data) 188 | 189 | return out 190 | 191 | 192 | def _toFile(geoarray, fname): 193 | """ 194 | Arguments 195 | --------- 196 | fname : str # file name 197 | 198 | Returns 199 | ------- 200 | None 201 | 202 | Purpose 203 | ------- 204 | Write GeoArray to file. The output dataset type is derived from 205 | the file name extension. See _DRIVER_DICT for implemented formats. 206 | """ 207 | 208 | def _fnameExtension(fname): 209 | return os.path.splitext(fname)[-1].lower() 210 | 211 | def _getDriver(fext): 212 | """ 213 | Guess driver from file name extension 214 | """ 215 | if fext in _DRIVER_DICT: 216 | driver = gdal.GetDriverByName(_DRIVER_DICT[fext]) 217 | metadata = driver.GetMetadata_Dict() 218 | if "YES" == metadata.get("DCAP_CREATE", metadata.get("DCAP_CREATECOPY")): 219 | return driver 220 | raise IOError("Datatype cannot be written") 221 | raise IOError("No driver found for filename extension '{:}'".format(fext)) 222 | 223 | def _getDatatype(driver): 224 | tnames = tuple(driver.GetMetadata_Dict()["DMD_CREATIONDATATYPES"].split(" ")) 225 | types = tuple(gdal.GetDataTypeByName(t) for t in tnames) 226 | tdict = tuple((gdal.GetDataTypeSize(t), t) for t in types) 227 | otype = max(tdict, key=lambda x: x[0])[-1] 228 | return np.dtype(_TYPEMAP[otype]) 229 | 230 | def _copyMetadata(source, target): 231 | # driver.CreateCopy does *not* copy all the metadata, so do this manually 232 | target_domains = set(target.GetMetadataDomainList()) 233 | for domain in source.GetMetadataDomainList(): 234 | if domain not in target_domains: 235 | metadata = source.GetMetadata(domain) 236 | target.SetMetadata(metadata, domain) 237 | 238 | source = _toDataset(geoarray) 239 | driver = _getDriver(_fnameExtension(fname)) 240 | target = driver.CreateCopy(fname, source, 0) 241 | _copyMetadata(source, target) 242 | 243 | 244 | def _writeData(grid): 245 | 246 | fobj = grid.fobj 247 | data = grid.data 248 | if data.ndim == 2: 249 | data = data[None, ...] 250 | 251 | for n in range(fobj.RasterCount) : 252 | fobj.GetRasterBand(n + 1).WriteArray(data[n]) 253 | -------------------------------------------------------------------------------- /geoarray/gdalspatial.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import warnings 5 | import numpy as np 6 | import gdal, osr 7 | 8 | gdal.UseExceptions() 9 | gdal.PushErrorHandler('CPLQuietErrorHandler') 10 | 11 | 12 | class _Projection(object): 13 | def __init__(self, arg): 14 | """ 15 | Arguments: 16 | arg can be: 17 | 1. int : EPSG code 18 | 2. dict : pyproj compatable dictionary 19 | 3. str : WKT string 20 | 4. _Projection 21 | """ 22 | self._srs = osr.SpatialReference() 23 | self._import(arg) 24 | 25 | def _import(self, value): 26 | if isinstance(value, _Projection): 27 | self._srs = value._srs 28 | elif value: 29 | if isinstance(value, int): 30 | method = self._srs.ImportFromEPSG 31 | elif isinstance(value, str): 32 | method = self._srs.ImportFromWkt 33 | elif isinstance(value, dict): 34 | try: 35 | method = self._srs.ImportFromDict 36 | except AttributeError: 37 | method = self._srs.ImportFromProj4 38 | value = "+{:}".format( 39 | " +".join( 40 | ["=".join(map(str, pp)) for pp in value.items()])) 41 | else: 42 | raise RuntimeError("Projection not understood") 43 | 44 | if method(value): 45 | raise RuntimeError("Failed to set projection") 46 | 47 | def __eq__(self, other): 48 | return bool(self._srs.IsSame(_Projection(other)._srs)) 49 | 50 | def toWkt(self): 51 | return self._srs.ExportToPrettyWkt() 52 | 53 | def toProj4(self): 54 | return self._srs.ExportToProj4() 55 | 56 | def toDict(self): 57 | proj = self._srs.ExportToProj4() 58 | out = dict( 59 | filter( 60 | lambda x: len(x) == 2, 61 | [p.replace("+", "").split("=") for p in proj.split(" +")])) 62 | return out 63 | 64 | def __nonzero__(self): 65 | # is a an projection set? 66 | return bool(self.toWkt()) 67 | 68 | def __get__(self, *args, **kwargs): 69 | return self 70 | 71 | def __set__(self, obj, val): 72 | self._import(val) 73 | 74 | def __str__(self): 75 | return self.toWkt() 76 | 77 | def __repr__(self): 78 | return self.toWkt() 79 | 80 | 81 | class _Transformer(object): 82 | def __init__(self, sproj, tproj): 83 | """ 84 | Arguments 85 | --------- 86 | sproj, tproj : Projection 87 | 88 | Purpose 89 | ------- 90 | Encapsulates the osr Cordinate Transformation functionality 91 | """ 92 | self._tx = osr.CoordinateTransformation( 93 | sproj._srs, tproj._srs) 94 | 95 | def __call__(self, y, x): 96 | try: 97 | xt, yt, _ = self._tx.TransformPoint(x, y) 98 | except NotImplementedError: 99 | raise AttributeError("Projections not correct or given!") 100 | return yt, xt 101 | -------------------------------------------------------------------------------- /geoarray/geotrans.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | from .utils import _broadcastTo 6 | from abc import ABCMeta, abstractmethod, abstractproperty 7 | 8 | class _GeoBase(object): 9 | __metaclass__ = ABCMeta 10 | 11 | #@property 12 | #def origin(self): 13 | # return "".join( 14 | # ["l" if self.ycellsize > 0 else "u", 15 | # "l" if self.xcellsize > 0 else "r"]) 16 | 17 | @abstractproperty 18 | def coordinates(self): 19 | pass 20 | 21 | @abstractproperty 22 | def bbox(self): 23 | pass 24 | 25 | @abstractmethod 26 | def _todict(self): 27 | pass 28 | 29 | @abstractmethod 30 | def _replace(self): 31 | pass 32 | 33 | @abstractmethod 34 | def _getitem(self): 35 | pass 36 | 37 | @abstractmethod 38 | def toGdal(self): 39 | raise NotImplementedError 40 | 41 | 42 | class _Geolocation(_GeoBase): 43 | def __init__(self, yvalues, xvalues, shape, origin): 44 | self.yvalues = yvalues 45 | self.xvalues = xvalues 46 | self.shape = shape 47 | self.origin = origin 48 | 49 | @property 50 | def yorigin(self): 51 | bbox = self.bbox 52 | return self.bbox["ymax" if self.origin[0] == "u" else "ymin"] 53 | 54 | @property 55 | def xorigin(self): 56 | return self.bbox["xmax" if self.origin[1] == "r" else "xmin"] 57 | 58 | @property 59 | def coordinates(self): 60 | return self.yvalues, self.xvalues 61 | 62 | @property 63 | def bbox(self): 64 | 65 | ymin, ymax = self.yvalues.min(), self.yvalues.max() 66 | xmin, xmax = self.xvalues.min(), self.xvalues.max() 67 | 68 | ydiff = np.abs(np.diff(self.yvalues, axis=-2)) 69 | xdiff = np.abs(np.diff(self.xvalues, axis=-1)) 70 | if self.origin[0] == "u": 71 | ymin -= ydiff[-1].max() 72 | else: 73 | ymax += ydiff[0].max() 74 | 75 | if self.origin[1] == "l": 76 | xmax += xdiff[:,-1].max() 77 | else: 78 | xmin -= xdiff[:,0].max() 79 | 80 | return {"ymin": ymin, "ymax": ymax, "xmin": xmin, "xmax": xmax} 81 | 82 | 83 | def _replace(self, yvalues=None, xvalues=None, origin=None, shape=None): 84 | return _Geolocation( 85 | yvalues=self.yvalues if yvalues is None else yvalues, 86 | xvalues=self.xvalues if xvalues is None else xvalues, 87 | shape=self.shape if shape is None else shape, 88 | origin=self.origin if origin is None else origin) 89 | 90 | def _todict(self): 91 | return { 92 | "yvalues": self.yvalues, 93 | "xvalues": self.xvalues} 94 | 95 | def _getitem(self, slc): 96 | 97 | yvalues = np.array( 98 | _broadcastTo(self.yvalues, self.shape, (-2, -1))[slc], 99 | copy=False, ndmin=2) 100 | 101 | xvalues = np.array( 102 | _broadcastTo(self.xvalues, self.shape, (-2, -1))[slc], 103 | copy=False, ndmin=2) 104 | 105 | if yvalues.ndim > 2: 106 | yvalues = yvalues[..., 0, :, :] 107 | 108 | if xvalues.ndim > 2: 109 | xvalues = xvalues[..., 0, :, :] 110 | 111 | return self._replace(yvalues=yvalues, xvalues=xvalues) 112 | 113 | 114 | def toGdal(self): 115 | shape = (1,) + self.shape if len(self.shape) < 3 else self.shape 116 | return { 117 | "X_BAND": str(shape[0]), # need to be filled 118 | "Y_BAND": str(shape[0] + 1), # need to be filled 119 | "PIXEL_OFFSET": "0", 120 | "LINE_OFFSET": "0", 121 | "PIXEL_STEP": "1", 122 | "LINE_STEP": "1"} 123 | 124 | 125 | class _Geotrans(_GeoBase): 126 | def __init__(self, yorigin, xorigin, ycellsize, xcellsize, 127 | yparam, xparam, origin, shape): 128 | self.yorigin = yorigin 129 | self.xorigin = xorigin 130 | self.ycellsize = ycellsize 131 | self.xcellsize = xcellsize 132 | self.yparam = yparam 133 | self.xparam = xparam 134 | self.origin = origin 135 | self.shape = shape 136 | self._yvalues = None 137 | self._xvalues = None 138 | 139 | @property 140 | def cellsize(self): 141 | return (self.ycellsize, self.xcellsize) 142 | 143 | @property 144 | def nrows(self): 145 | try: 146 | return self.shape[-2] 147 | except IndexError: 148 | return 1 149 | 150 | @property 151 | def ncols(self): 152 | try: 153 | return self.shape[-1] 154 | except IndexError: 155 | return 1 156 | 157 | @property 158 | def bbox(self): 159 | 160 | corners = np.array(self.getCorners()) 161 | ymin, xmin = np.min(corners, axis=0) 162 | ymax, xmax = np.max(corners, axis=0) 163 | 164 | return {"ymin": ymin, "ymax": ymax, "xmin": xmin, "xmax": xmax} 165 | 166 | 167 | def _calcCoordinate(self, row, col): 168 | 169 | yval = (self.yorigin 170 | + col * self.yparam 171 | + row * self.ycellsize) 172 | xval = (self.xorigin 173 | + col * self.xcellsize 174 | + row * self.xparam) 175 | return yval, xval 176 | 177 | def toGdal(self): 178 | out = (self.xorigin, self.xcellsize, self.xparam, 179 | self.yorigin, self.yparam, self.ycellsize) 180 | return out 181 | 182 | @property 183 | def coordinates(self): 184 | if self._yvalues is None or self._xvalues is None: 185 | xdata, ydata = np.meshgrid( 186 | np.arange(self.ncols, dtype=float), 187 | np.arange(self.nrows, dtype=float)) 188 | self._yvalues, self._xvalues = self._calcCoordinate(ydata, xdata) 189 | return self._yvalues, self._xvalues 190 | 191 | @property 192 | def yvalues(self): 193 | return self.coordinates[0] 194 | 195 | @property 196 | def xvalues(self): 197 | return self.coordinates[1] 198 | 199 | def getCorners(self): 200 | corners = [(0, 0), (self.nrows, 0), 201 | (0, self.ncols), (self.nrows, self.ncols)] 202 | return [self._calcCoordinate(*idx) for idx in corners] 203 | 204 | def getCorner(self, corner=None): 205 | if not corner: 206 | corner = self.origin 207 | 208 | bbox = self.bbox 209 | return ( 210 | bbox["ymax"] if corner[0] == "u" else bbox["ymin"], 211 | bbox["xmax"] if corner[1] == "r" else bbox["xmin"],) 212 | 213 | def _replace(self, yorigin=None, xorigin=None, ycellsize=None, xcellsize=None, 214 | yparam=None, xparam=None, origin=None, shape=None): 215 | 216 | return _Geotrans( 217 | yorigin=self.yorigin if yorigin is None else yorigin, 218 | xorigin=self.xorigin if xorigin is None else xorigin, 219 | ycellsize=self.ycellsize if ycellsize is None else ycellsize, 220 | xcellsize=self.xcellsize if xcellsize is None else xcellsize, 221 | yparam=self.yparam if yparam is None else yparam, 222 | xparam=self.xparam if xparam is None else xparam, 223 | origin=self.origin if origin is None else origin, 224 | shape=self.shape if shape is None else shape) 225 | 226 | def _todict(self): 227 | return { 228 | "yorigin": self.yorigin, 229 | "xorigin": self.xorigin, 230 | "ycellsize": self.ycellsize, 231 | "xcellsize": self.xcellsize, 232 | "yparam": self.yparam, 233 | "xparam": self.xparam, 234 | "origin": self.origin} 235 | 236 | def _getitem(self, slc): 237 | 238 | yvalues = np.array( 239 | _broadcastTo(self.yvalues, self.shape, (-2, -1))[slc], 240 | copy=False, ndmin=2) 241 | 242 | xvalues = np.array( 243 | _broadcastTo(self.xvalues, self.shape, (-2, -1))[slc], 244 | copy=False, ndmin=2) 245 | 246 | nrows, ncols = yvalues.shape[-2:] 247 | ycellsize = np.diff(yvalues, axis=-2).mean() if nrows > 1 else self.ycellsize 248 | xcellsize = np.diff(xvalues, axis=-1).mean() if ncols > 1 else self.xcellsize 249 | 250 | out = self._replace( 251 | yorigin=yvalues.max(), xorigin=xvalues.min(), 252 | ycellsize=ycellsize, xcellsize=xcellsize, 253 | shape=yvalues.shape) 254 | return out 255 | -------------------------------------------------------------------------------- /geoarray/spatial.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | from math import floor, ceil 6 | 7 | 8 | class SpatialMixin(object): 9 | 10 | def trim(self): 11 | """ 12 | Arguments 13 | --------- 14 | None 15 | 16 | Returns 17 | ------- 18 | GeoArray 19 | 20 | Purpose 21 | ------- 22 | Removes rows and columns from the margins of the 23 | grid if they contain only fill values. 24 | """ 25 | 26 | try: 27 | y_idx, x_idx = np.where(self.data != self.fill_value)[-2:] 28 | return self.removeCells( 29 | top=min(y_idx), bottom=self.nrows - max(y_idx) - 1, 30 | left=min(x_idx), right=self.ncols - max(x_idx) - 1) 31 | except ValueError: 32 | return self 33 | 34 | def removeCells(self, top=0, left=0, bottom=0, right=0): 35 | """ 36 | Arguments 37 | --------- 38 | top, left, bottom, right : int 39 | 40 | Returns 41 | ------- 42 | GeoArray 43 | 44 | Purpose 45 | ------- 46 | Remove the number of given cells from the respective 47 | margin of the grid. 48 | """ 49 | 50 | top = int(max(top, 0)) 51 | left = int(max(left, 0)) 52 | bottom = self.nrows - int(max(bottom, 0)) 53 | right = self.ncols - int(max(right, 0)) 54 | 55 | return self[..., top:bottom, left:right] 56 | 57 | def shrink(self, ymin=None, ymax=None, xmin=None, xmax=None): 58 | """ 59 | Arguments 60 | --------- 61 | ymin, ymax, xmin, xmax : scalar 62 | 63 | Returns 64 | ------- 65 | GeoArray 66 | 67 | Purpose 68 | ------- 69 | Shrinks the grid in a way that the given bbox is still 70 | within the grid domain. 71 | 72 | BUG: 73 | ------------ 74 | For bbox with both negative and postive values 75 | """ 76 | bbox = { 77 | "ymin": ymin if ymin is not None else self.bbox["ymin"], 78 | "ymax": ymax if ymax is not None else self.bbox["ymax"], 79 | "xmin": xmin if xmin is not None else self.bbox["xmin"], 80 | "xmax": xmax if xmax is not None else self.bbox["xmax"], 81 | } 82 | 83 | cellsize = [float(abs(cs)) for cs in self.cellsize] 84 | top = floor((self.bbox["ymax"] - bbox["ymax"]) / cellsize[0]) 85 | left = floor((bbox["xmin"] - self.bbox["xmin"]) / cellsize[1]) 86 | bottom = floor((bbox["ymin"] - self.bbox["ymin"]) / cellsize[0]) 87 | right = floor((self.bbox["xmax"] - bbox["xmax"]) / cellsize[1]) 88 | 89 | return self.removeCells( 90 | max(top, 0), max(left, 0), max(bottom, 0), max(right, 0)) 91 | 92 | def addCells(self, top=0, left=0, bottom=0, right=0): 93 | """ 94 | Arguments 95 | --------- 96 | top, left, bottom, right : int 97 | 98 | Returns 99 | ------- 100 | GeoArray 101 | 102 | Purpose 103 | ------- 104 | Add the number of given cells to the respective margin of the grid. 105 | """ 106 | 107 | from .core import GeoArray 108 | 109 | top = int(max(top, 0)) 110 | left = int(max(left, 0)) 111 | bottom = int(max(bottom, 0)) 112 | right = int(max(right, 0)) 113 | 114 | if self.origin[0] == "l": 115 | top, bottom = bottom, top 116 | if self.origin[1] == "r": 117 | left, right = right, left 118 | 119 | shape = list(self.shape) 120 | shape[-2:] = self.nrows + top + bottom, self.ncols + left + right 121 | 122 | try: 123 | data = np.full(shape, self.fill_value, self.dtype) 124 | except TypeError: 125 | # fill_value is set to none 126 | raise AttributeError( 127 | "Valid fill_value needed, actual value is {:}" 128 | .format(self.fill_value)) 129 | 130 | yorigin = self.yorigin + top*self.ycellsize * -1 131 | xorigin = self.xorigin + left*self.xcellsize * -1 132 | 133 | out = GeoArray( 134 | **self._getArgs( 135 | data=data, 136 | geotrans=self.geotrans._replace( 137 | yorigin=yorigin, xorigin=xorigin, shape=shape), 138 | mode="r", fobj=None)) 139 | 140 | # the Ellipsis ensures that the function works 141 | # for arrays with more than two dimensions 142 | out[..., top:top+self.nrows, left:left+self.ncols] = self 143 | return out 144 | 145 | def enlarge(self, ymin=None, ymax=None, xmin=None, xmax=None): 146 | """ 147 | Arguments 148 | --------- 149 | ymin, ymax, xmin, xmax : scalar 150 | 151 | Returns 152 | ------- 153 | None 154 | 155 | Purpose 156 | ------- 157 | Enlarge the grid in a way that the given coordinates will 158 | be part of the grid domain. Added rows/cols are filled with 159 | the grid's fill value. 160 | """ 161 | 162 | bbox = { 163 | "ymin": ymin if ymin is not None else self.bbox["ymin"], 164 | "ymax": ymax if ymax is not None else self.bbox["ymax"], 165 | "xmin": xmin if xmin is not None else self.bbox["xmin"], 166 | "xmax": xmax if xmax is not None else self.bbox["xmax"],} 167 | 168 | cellsize = [float(abs(cs)) for cs in self.cellsize] 169 | 170 | top = ceil((bbox["ymax"] - self.bbox["ymax"]) / cellsize[0]) 171 | left = ceil((self.bbox["xmin"] - bbox["xmin"]) / cellsize[1]) 172 | bottom = ceil((self.bbox["ymin"] - bbox["ymin"]) / cellsize[0]) 173 | right = ceil((bbox["xmax"] - self.bbox["xmax"]) / cellsize[1]) 174 | 175 | return self.addCells( 176 | max(top, 0), max(left, 0), max(bottom, 0), max(right, 0)) 177 | 178 | def coordinatesOf(self, y_idx, x_idx): 179 | """ 180 | Arguments 181 | --------- 182 | y_idx, x_idx : int 183 | 184 | Returns 185 | ------- 186 | (scalar, scalar) 187 | 188 | Purpose 189 | ------- 190 | Return the coordinates of the grid cell definied by the given 191 | row and column index values. The cell corner to which the returned 192 | values belong is definied 193 | by the attribute origin: 194 | "ll": lower-left corner 195 | "lr": lower-right corner 196 | "ul": upper-left corner 197 | "ur": upper-right corner 198 | """ 199 | 200 | if ((y_idx < 0 or x_idx < 0) 201 | or (y_idx >= self.nrows 202 | or x_idx >= self.ncols)): 203 | raise ValueError("Index out of bounds !") 204 | 205 | yorigin, xorigin = self.getCorner("ul") 206 | return ( 207 | yorigin - y_idx * abs(self.cellsize[0]), 208 | xorigin + x_idx * abs(self.cellsize[1])) 209 | 210 | def indexOf(self, ycoor, xcoor): 211 | """ 212 | Arguments 213 | --------- 214 | ycoor, xcoor : scalar 215 | 216 | Returns 217 | ------- 218 | (int, int) 219 | 220 | Purpose 221 | ------- 222 | Find the grid cell into which the given coordinates 223 | fall and return its row/column index values. 224 | """ 225 | 226 | 227 | yorigin, xorigin = self.getCorner("ul") 228 | 229 | yidx = int(floor((yorigin - ycoor) / float(abs(self.ycellsize)))) 230 | xidx = int(floor((xcoor - xorigin) / float(abs(self.xcellsize)))) 231 | 232 | if yidx < 0 or yidx >= self.nrows or xidx < 0 or xidx >= self.ncols: 233 | raise ValueError("Given Coordinates not within the grid domain!") 234 | 235 | return yidx, xidx 236 | 237 | 238 | # def snap(self,target): 239 | # """ 240 | # Arguments 241 | # --------- 242 | # target : GeoArray 243 | 244 | # Returns 245 | # ------- 246 | # None 247 | 248 | # Purpose 249 | # ------- 250 | # Shift the grid origin that it matches the nearest cell origin in target. 251 | 252 | # Restrictions 253 | # ------------ 254 | # The shift will only alter the grid coordinates. No changes to the 255 | # data will be done. In case of large shifts the physical integrety 256 | # of the data might be disturbed! 257 | 258 | # diff = np.array(self.getCorner()) - np.array(target.getCorner(self.origin)) 259 | # dy, dx = abs(diff)%target.cellsize * np.sign(diff) 260 | 261 | # if abs(dy) > self.cellsize[0]/2.: 262 | # dy += self.cellsize[0] 263 | 264 | # if abs(dx) > self.cellsize[1]/2.: 265 | # dx += self.cellsize[1] 266 | 267 | # self.xorigin -= dx 268 | # self.yorigin -= dy 269 | -------------------------------------------------------------------------------- /geoarray/utils.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import numpy as np 6 | 7 | 8 | try: 9 | basestring 10 | except NameError: 11 | basestring = str 12 | 13 | 14 | def _broadcastTo(array, shape, dims): 15 | """ 16 | array, shape: see numpy.broadcast_to 17 | dims: tuple, the dimensions the array dimensions 18 | should end up in the output array 19 | """ 20 | assert len(array.shape) == len(dims) 21 | assert len(set(dims)) == len(dims) # no duplicates 22 | # handle negative indices 23 | dims = [d if d >= 0 else d+len(shape) for d in dims] 24 | # bring array to the desired dimensionality 25 | slc = [slice(None, None, None) if i in dims else None 26 | for i in range(len(shape))] 27 | return np.broadcast_to(array[tuple(slc)], shape) 28 | 29 | 30 | def _broadcastedMeshgrid(*arrays): 31 | 32 | def _toNd(array, n, pos=-1): 33 | """ 34 | expand given 1D array to n dimensions, 35 | the dimensions > 0 can be given by pos 36 | """ 37 | assert array.ndim == 1, "arrays should be 1D" 38 | shape = np.ones(n, dtype=int) 39 | shape[pos] = len(array) 40 | return arr.reshape(shape) 41 | 42 | shape = tuple(len(arr) for arr in arrays) 43 | 44 | out = [] 45 | for i, arr in enumerate(arrays): 46 | tmp = np.broadcast_to( 47 | _toNd(arr, len(shape), pos=i), 48 | shape 49 | ) 50 | # there should be a solution without transposing... 51 | out.append(tmp.T) 52 | return out 53 | 54 | 55 | def _tupelize(arg): 56 | out = arg 57 | if isinstance(out, basestring): 58 | out = (out, ) 59 | if not isinstance(out, tuple): 60 | try: 61 | out = tuple(out) 62 | except TypeError: 63 | out = (out, ) 64 | return out 65 | -------------------------------------------------------------------------------- /geoarray/wrapper.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Author 6 | ------ 7 | David Schaefer 8 | 9 | Purpose 10 | ------- 11 | This module provides initializer function for core.GeoArray 12 | """ 13 | 14 | import numpy as np 15 | from .core import GeoArray 16 | from .gdalio import _fromFile, _fromDataset 17 | from .gdalspatial import _Projection 18 | from .geotrans import _Geotrans, _Geolocation 19 | from .utils import _tupelize 20 | # from typing import Optional, Union, Tuple, Any, Mapping, AnyStr 21 | 22 | 23 | def array(data, # type: Union[np.ndarray, GeoArray] 24 | dtype = None, # type: Optional[Union[AnyStr, np.dtype]] 25 | yorigin = 0, # type: Optional[float] 26 | xorigin = 0, # type: Optional[float] 27 | origin = "ul", # type: Optional[floatAnyStr] 28 | fill_value = None, # type: Optional[float] 29 | cellsize = 1, # type: Optional[float]] 30 | ycellsize = None, # type: Optional[float] 31 | xcellsize = None, # type: Optional[float] 32 | yparam = 0, # type: Optional[float] 33 | xparam = 0, # type: Optional[float] 34 | # geotrans = None, # type: Optional[_Geotrans] 35 | yvalues = None, # type: Optional[np.ndarray] 36 | xvalues = None, # type: Optional[np.ndarray] 37 | proj = None, # type: Mapping[AnyStr, Union[AnyStr, float]] 38 | mode = "r", # type: AnyStr 39 | color_mode = "L", # type: AnyStr 40 | copy = False, # type: bool 41 | fobj = None, # type: Optional[osgeo.gdal.Dataset] 42 | ): # type: (...) -> GeoArray 43 | """ 44 | Arguments 45 | --------- 46 | data : numpy.ndarray # data to wrap 47 | 48 | Optional Arguments 49 | ------------------ 50 | dtype : str/np.dtype # type of the returned grid 51 | yorigin : int/float, default: 0 # y-value of the grid's origin 52 | xorigin : int/float, default: 0 # x-value of the grid's origin 53 | origin : {"ul","ur","ll","lr"}, # position of the origin. One of: 54 | default: "ul" # "ul" : upper left corner 55 | # "ur" : upper right corner 56 | # "ll" : lower left corner 57 | # "lr" : lower right corner 58 | fill_value : inf/float # fill or fill value 59 | cellsize : int/float or 2-tuple of those # cellsize, cellsizes in y and x direction 60 | proj : dict/None # proj4 projection parameters 61 | copy : bool # create a copy of the given data 62 | 63 | Returns 64 | ------- 65 | GeoArray 66 | 67 | Purpose 68 | ------- 69 | Create a GeoArray from data. 70 | """ 71 | 72 | 73 | def _checkGeolocArray(array, diffaxis): 74 | array = np.asarray(array) 75 | diff = np.diff(array, axis=diffaxis) 76 | 77 | assert array.ndim == 2 78 | assert array.shape == data.shape[-2:] 79 | assert len(np.unique(np.sign(diff))) == 1 80 | 81 | return array 82 | 83 | if yvalues is not None and xvalues is not None: 84 | yvalues = _checkGeolocArray(yvalues, 0) 85 | xvalues = _checkGeolocArray(xvalues, 1) 86 | geotrans = _Geolocation(yvalues, xvalues, shape=data.shape, origin=origin) 87 | else: 88 | 89 | cellsize = _tupelize(cellsize) 90 | 91 | if ycellsize is None: 92 | ycellsize = cellsize[0] 93 | if (origin[0] == "u" and ycellsize > 0) or (origin[0] == "l" and ycellsize < 0): 94 | ycellsize *= -1 95 | 96 | if xcellsize is None: 97 | xcellsize = cellsize[-1] 98 | if (origin[1] == "r" and xcellsize > 0) or (origin[0] == "l" and xcellsize < 0): 99 | xcellsize *= -1 100 | 101 | # if geotrans is None: 102 | # NOTE: not to robust... 103 | geotrans = _Geotrans( 104 | yorigin=yorigin, xorigin=xorigin, 105 | ycellsize=ycellsize, xcellsize=xcellsize, 106 | yparam=yparam, xparam=xparam, 107 | origin=origin, shape=data.shape) 108 | 109 | proj = _Projection(proj) 110 | 111 | if isinstance(data, GeoArray): 112 | return GeoArray( 113 | dtype = dtype or data.dtype, 114 | geotrans = data.geotrans, 115 | fill_value = fill_value or data.fill_value, 116 | proj = proj or data.proj, 117 | mode = mode or data.mode, 118 | color_mode = color_mode or data.color_mode, 119 | fobj = data.fobj, 120 | data = data.data) 121 | 122 | return GeoArray( 123 | data = np.array(data, dtype=dtype, copy=copy), 124 | geotrans = geotrans, 125 | fill_value = fill_value, 126 | proj = proj, 127 | mode = mode, 128 | color_mode = color_mode, 129 | fobj = fobj,) 130 | 131 | 132 | def zeros(shape, dtype=np.float64, *args, **kwargs): 133 | """ 134 | Arguments 135 | --------- 136 | shape : tuple # shape of the returned grid 137 | 138 | Optional Arguments 139 | ------------------ 140 | see array 141 | 142 | Returns 143 | ------- 144 | GeoArray 145 | 146 | Purpose 147 | ------- 148 | Return a new GeoArray of given shape and type, filled with zeros. 149 | """ 150 | 151 | return array(data=np.zeros(shape, dtype), *args, **kwargs) 152 | 153 | 154 | def ones(shape, dtype=np.float64, *args, **kwargs): 155 | """ 156 | Arguments 157 | --------- 158 | shape : tuple # shape of the returned grid 159 | 160 | Optional Arguments 161 | ------------------ 162 | see array 163 | 164 | Returns 165 | ------- 166 | GeoArray 167 | 168 | Purpose 169 | ------- 170 | Return a new GeoArray of given shape and type, filled with ones. 171 | """ 172 | 173 | return array(data=np.ones(shape, dtype), *args, **kwargs) 174 | 175 | 176 | def full(shape, value, dtype=np.float64, *args, **kwargs): 177 | """ 178 | Arguments 179 | --------- 180 | shape : tuple # shape of the returned grid 181 | fill_value : scalar # fille value 182 | 183 | Optional Arguments 184 | ------------------ 185 | see array 186 | 187 | Returns 188 | ------- 189 | GeoArray 190 | 191 | Purpose 192 | ------- 193 | Return a new GeoArray of given shape and type, filled with fill_value. 194 | """ 195 | 196 | return array(data=np.full(shape, value, dtype), *args, **kwargs) 197 | 198 | 199 | def empty(shape, dtype=np.float64, *args, **kwargs): 200 | """ 201 | Arguments 202 | ---------- 203 | shape : tuple # shape of the returned grid 204 | 205 | Optional Arguments 206 | ------------------ 207 | see array 208 | 209 | Returns 210 | ------- 211 | GeoArray 212 | 213 | Purpose 214 | ------- 215 | Return a new empty GeoArray of given shape and type 216 | """ 217 | 218 | return array(data=np.empty(shape, dtype), *args, **kwargs) 219 | 220 | 221 | def _likeArgs(arr): 222 | if isinstance(arr, GeoArray): 223 | return arr.header 224 | return {} 225 | 226 | 227 | def zeros_like(arr, dtype=None): 228 | args = _likeArgs(arr) 229 | return zeros(shape=arr.shape, dtype=dtype or arr.dtype, **args) 230 | 231 | 232 | def ones_like(arr, dtype=None): 233 | args = _likeArgs(arr) 234 | return ones(shape=arr.shape, dtype=dtype or arr.dtype, **args) 235 | 236 | 237 | def full_like(arr, value, dtype=None): 238 | args = _likeArgs(arr) 239 | return full(shape=arr.shape, value=value, dtype=dtype or arr.dtype, **args) 240 | 241 | 242 | def fromdataset(ds): 243 | return array(**_fromDataset(ds)) 244 | 245 | 246 | def fromfile(fname, mode="r"): 247 | """ 248 | Arguments 249 | --------- 250 | fname : str # file name 251 | 252 | Returns 253 | ------- 254 | GeoArray 255 | 256 | Purpose 257 | ------- 258 | Create GeoArray from file 259 | 260 | """ 261 | return _fromFile(fname, mode) 262 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from distutils.core import setup 5 | 6 | setup( 7 | name = "geoarray", 8 | version = "1.0", 9 | author = "David Schaefer", 10 | url = "https://github.com/schaefed/geoarray", 11 | py_modules = ["geoarray"], 12 | ) 13 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schaefed/geoarray/37072550fbcd052cf1a1ffd330feae75efca55a8/test/__init__.py -------------------------------------------------------------------------------- /test/test_core.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest, copy, shutil, os 5 | import numpy as np 6 | import geoarray as ga 7 | import gdal 8 | import warnings 9 | import subprocess 10 | import tempfile 11 | from test_utils import createTestFiles, removeTestFiles 12 | 13 | # all tests, run from main directory: 14 | # python -m unittest discover test 15 | 16 | # this test only, run from main directory 17 | # python -m unittest test.test_core 18 | 19 | class Test(unittest.TestCase): 20 | 21 | def setUp(self): 22 | _, self.grids = createTestFiles() 23 | 24 | def tearDown(self): 25 | removeTestFiles() 26 | 27 | def test_setFillValue(self): 28 | rpcvalue = -2222 29 | for base in self.grids: 30 | base.fill_value = rpcvalue 31 | self.assertEqual(base.fill_value, base.dtype.type(rpcvalue)) 32 | 33 | def test_setDataType(self): 34 | rpctype = np.int32 35 | for base in self.grids: 36 | grid = base.astype(rpctype) 37 | self.assertEqual(grid.dtype,rpctype) 38 | 39 | def test_getitem(self): 40 | for base in self.grids: 41 | # simplifies the tests... 42 | base = base[0] if base.ndim > 2 else base 43 | grid = base.copy() 44 | slices = ( 45 | base < 3, 46 | base == 10, 47 | np.where(base>6), 48 | (slice(None,None,None),slice(0,4,3)),(1,1),Ellipsis 49 | ) 50 | 51 | idx = np.arange(12,20) 52 | self.assertTrue(np.all(grid[idx].data == base[ga.array(idx)].data)) 53 | for i,s in enumerate(slices): 54 | slc1 = grid[s] 55 | slc2 = base[s] 56 | self.assertTrue(np.all(slc1.data == slc2.data)) 57 | try: 58 | self.assertTrue(np.all(slc1.mask == slc2.mask)) 59 | except AttributeError: # __getitem__ returned a scalar 60 | pass 61 | 62 | def test_getitemCellsize(self): 63 | 64 | grid = ga.ones((100,100), yorigin=1000, xorigin=1200, cellsize=10, origin="ul") 65 | 66 | self.assertTupleEqual(grid[3:4].cellsize, (-10, 10)) 67 | self.assertTupleEqual(grid[0::2, 0::4].cellsize, (-20, 40)) 68 | 69 | self.assertTupleEqual(grid[0::3, 0::5].cellsize, (-30, 50)) 70 | self.assertTupleEqual(grid[0::1, 0::7].cellsize, (-10, 70)) 71 | # # # needs to be extended... 72 | self.assertTupleEqual(grid[[1,2,5]].cellsize, (-20, 10)) 73 | self.assertTupleEqual(grid[[1,2,4,10]].cellsize, (-30, 10)) 74 | 75 | self.assertTupleEqual(grid[[0,10,5]].cellsize, (-25, 10)) 76 | 77 | grid = ga.ones((100,100), yorigin=1000, xorigin=1200, cellsize=10,origin="ll") 78 | self.assertTupleEqual(grid[3:4].cellsize, (10, 10)) 79 | self.assertTupleEqual(grid[0::2, 0::4].cellsize, (20, 40)) 80 | self.assertTupleEqual(grid[0::3, 0::5].cellsize, (30, 50)) 81 | self.assertTupleEqual(grid[0::1, 0::7].cellsize, (10, 70)) 82 | 83 | grid = ga.ones((100,100), yorigin=1000, xorigin=1200, cellsize=10, origin="lr") 84 | self.assertTupleEqual(grid[3:4].cellsize, (10, -10)) 85 | 86 | grid = ga.ones((100,100), yorigin=1000, xorigin=1200, cellsize=10, origin="ur") 87 | self.assertTupleEqual(grid[3:4].cellsize, (-10, -10)) 88 | 89 | # yvals = np.array(range(1000, 1100, 2) + range(1100, 1250, 3))[::-1] 90 | # xvals = np.array(range(0, 100, 2) + range(100, 250, 3)) 91 | # xvalues, yvalues = np.meshgrid(yvals, xvals) 92 | # grid = ga.ones((100, 100), origin="ul", yvalues=yvalues, xvalues=xvalues) 93 | 94 | 95 | def test_getitemOrigin(self): 96 | grids = ( 97 | ga.ones((100, 100), yorigin=1000, xorigin=1200, origin="ul"), 98 | ga.ones((100, 100), yorigin=1000, xorigin=1200, origin="ll"), 99 | ga.ones((100, 100), yorigin=1000, xorigin=1200, origin="ur"), 100 | ga.ones((100, 100), yorigin=1000, xorigin=1200, origin="lr")) 101 | slices = ( 102 | (slice(3, 4)), 103 | (slice(3, 4), slice(55, 77, None)), 104 | (slice(None, None, 7), slice(55, 77, None)), 105 | (-1, ),) 106 | 107 | expected = ( 108 | ((997, 1200), (997, 1255), (1000, 1255), (901, 1200)), 109 | ((1096, 1200), (1096, 1255), (1001, 1255), (1000, 1200)), 110 | ((997, 1200), (997, 1177), (1000, 1177), (901, 1200)), 111 | ((1096, 1200), (1096, 1177), (1001, 1177), (1000, 1200))) 112 | 113 | for i, grid in enumerate(grids): 114 | for slc, exp in zip(slices, expected[i]): 115 | self.assertTupleEqual( exp, grid[slc].getCorner() ) 116 | break 117 | break 118 | 119 | def test_setitem(self): 120 | for base in self.grids: 121 | # simplifies the tests... 122 | base = base[0] if base.ndim > 2 else base 123 | slices = ( 124 | np.arange(12,20).reshape(1,-1), 125 | base.data < 3, 126 | np.where(base>6), 127 | (slice(None,None,None),slice(0,4,3)), 128 | (1,1), 129 | Ellipsis 130 | ) 131 | value = 11 132 | # grid = copy.deepcopy(base) 133 | for slc in slices: 134 | grid = copy.deepcopy(base) 135 | grid[slc] = value 136 | self.assertTrue(np.all(grid[slc] == value)) 137 | 138 | 139 | def test_bbox(self): 140 | grids = ( 141 | ga.ones((100,100), yorigin=1000, xorigin=1200, origin="ul"), 142 | ga.ones((100,100), yorigin=1000, xorigin=1200, origin="ll"), 143 | ga.ones((100,100), yorigin=1000, xorigin=1200, origin="ur"), 144 | ga.ones((100,100), yorigin=1000, xorigin=1200, origin="lr"), 145 | ) 146 | expected = ( 147 | {'xmin': 1200, 'ymin': 900, 'ymax': 1000, 'xmax': 1300}, 148 | {'xmin': 1200, 'ymin': 1000, 'ymax': 1100, 'xmax': 1300}, 149 | {'xmin': 1100, 'ymin': 900, 'ymax': 1000, 'xmax': 1200}, 150 | {'xmin': 1100, 'ymin': 1000, 'ymax': 1100, 'xmax': 1200}, 151 | ) 152 | 153 | for g, e in zip(grids, expected): 154 | self.assertDictEqual(g.bbox, e) 155 | 156 | # def test_simplewrite(self): 157 | # for infile in FILES: 158 | # outfile = os.path.join(TMPPATH, os.path.split(infile)[1]) 159 | # base = ga.fromfile(infile) 160 | 161 | # base.tofile(outfile) 162 | # checkgrid = ga.fromfile(outfile) 163 | 164 | # self.assertDictEqual( 165 | # base._fobj.GetDriver().GetMetadata_Dict(), 166 | # checkgrid._fobj.GetDriver().GetMetadata_Dict() 167 | # ) 168 | 169 | # def test_tofile(self): 170 | # outfiles = (os.path.join(TMPPATH, "file{:}".format(ext)) for ext in ga._DRIVER_DICT) 171 | 172 | # for base in self.grids: 173 | # for outfile in outfiles: 174 | # if outfile.endswith(".png"): 175 | # # data type conversion is done and precision lost 176 | # continue 177 | # if outfile.endswith(".asc") and base.nbands > 1: 178 | # self.assertRaises(RuntimeError) 179 | # continue 180 | # base.tofile(outfile) 181 | # checkgrid = ga.fromfile(outfile) 182 | # self.assertTrue(np.all(checkgrid == base)) 183 | # self.assertDictEqual(checkgrid.bbox, base.bbox) 184 | 185 | def test_copy(self): 186 | for base in self.grids[1:]: 187 | deep_copy = copy.deepcopy(base) 188 | self.assertDictEqual(base.header, deep_copy.header) 189 | self.assertNotEqual(id(base),id(deep_copy)) 190 | self.assertTrue(np.all(base == deep_copy)) 191 | shallow_copy = copy.copy(base) 192 | self.assertDictEqual(base.header, shallow_copy.header) 193 | self.assertNotEqual(id(base),id(shallow_copy)) 194 | self.assertTrue(np.all(base == shallow_copy)) 195 | 196 | def test_numpyFunctions(self): 197 | # Ignore over/underflow warnings in function calls 198 | warnings.filterwarnings("ignore") 199 | # funcs tuple could be extended 200 | funcs = (np.exp, 201 | np.sin, np.cos, np.tan, np.arcsinh, 202 | np.around, np.rint, np.fix, 203 | np.prod, np.sum, 204 | np.trapz, 205 | np.i0, 206 | np.sinc, 207 | np.arctanh, 208 | np.gradient) 209 | 210 | for base in self.grids: 211 | grid = base.copy() 212 | for f in funcs: 213 | r1 = f(base) 214 | r2 = f(grid) 215 | 216 | try: 217 | np.testing.assert_equal(r1.data,r2.data) 218 | np.testing.assert_equal(r1.mask,r2.mask) 219 | except AttributeError: 220 | np.testing.assert_equal(r1,r2) 221 | 222 | if __name__== "__main__": 223 | unittest.main() 224 | -------------------------------------------------------------------------------- /test/test_gdalio.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import tempfile 6 | import geoarray as ga 7 | import numpy as np 8 | from test_utils import testArray, dtypeInfo 9 | 10 | class Test(unittest.TestCase): 11 | 12 | def test_io(self): 13 | test_array = testArray((340, 270)) 14 | endings = ga._DRIVER_DICT.keys() 15 | for ending in endings: 16 | with tempfile.NamedTemporaryFile(suffix=ending) as tf: 17 | # write and read again 18 | test_array.tofile(tf.name) 19 | check_array = ga.fromfile(tf.name) 20 | 21 | # gdal truncates values smaller/larger than the datatype, numpy wraps around. 22 | # clip array to make things comparable. 23 | dinfo = dtypeInfo(check_array.dtype) 24 | grid = test_array.clip(dinfo["min"], dinfo["max"]) 25 | fill_value = check_array.dtype.type(test_array.fill_value) 26 | 27 | np.testing.assert_almost_equal(check_array, grid) 28 | self.assertDictEqual(check_array.bbox, test_array.bbox) 29 | self.assertEqual(check_array.cellsize, test_array.cellsize) 30 | self.assertEqual(check_array.proj, test_array.proj) 31 | self.assertEqual(check_array.fill_value, fill_value) 32 | self.assertEqual(check_array.color_mode, test_array.color_mode) 33 | 34 | def test_updateio(self): 35 | test_array = testArray((340, 270)) 36 | slices = slice(1, -1, 3) 37 | endings = ga._DRIVER_DICT.keys() 38 | for ending in endings: 39 | if ending != ".tif": 40 | continue 41 | with tempfile.NamedTemporaryFile(suffix=ending) as tf: 42 | test_array.tofile(tf.name) 43 | check_file = ga.fromfile(tf.name, "a") 44 | check_file[slices] = 42 45 | check_file.close() 46 | check_file = ga.fromfile(tf.name, "r") 47 | self.assertTrue((check_file[slices] == 42).all()) 48 | -------------------------------------------------------------------------------- /test/test_methods.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest, copy, shutil, os 5 | import numpy as np 6 | import geoarray as ga 7 | import gdal 8 | import warnings 9 | import subprocess 10 | import tempfile 11 | from test_utils import createTestFiles, removeTestFiles 12 | 13 | # all tests, run from main directory: 14 | # python -m unittest discover test 15 | 16 | # this test only, run from parent directory run 17 | # python -m unittest test.test_methods 18 | 19 | class Test(unittest.TestCase): 20 | 21 | def setUp(self): 22 | self.fnames, self.grids = createTestFiles() 23 | 24 | def tearDown(self): 25 | removeTestFiles() 26 | 27 | # def test_basicMatch(self): 28 | # for base in self.grids: 29 | # grid1, grid2, grid3, grid4 = [base.copy() for _ in xrange(4)] 30 | # grid2.xorigin -= 1 31 | # grid3.cellsize = (grid3.cellsize[0] + 1, grid3.cellsize[0] + 1) 32 | # grid4.proj = {"invalid":"key"} # sets proj to False 33 | # self.assertTrue(base.basicMatch(grid1)) 34 | # self.assertFalse(base.basicMatch(grid2)) 35 | # self.assertFalse(base.basicMatch(grid3)) 36 | # self.assertTrue(base.basicMatch(grid4)) 37 | 38 | def test_addCells(self): 39 | for base in self.grids: 40 | 41 | try: 42 | padgrid = base.addCells(1, 1, 1, 1) 43 | self.assertTrue(np.sum(padgrid[...,1:-1,1:-1] == base)) 44 | except AttributeError: 45 | # input grid has an invalid fill_value, e.g the used test.png 46 | continue 47 | 48 | padgrid = base.addCells(0, 0, 0, 0) 49 | self.assertTrue(np.sum(padgrid[:] == base)) 50 | 51 | padgrid = base.addCells(0, 99, 0, 4000) 52 | self.assertTrue(np.sum(padgrid[...,99:-4000] == base)) 53 | 54 | padgrid = base.addCells(-1000, -4.55, 0, -6765.222) 55 | self.assertTrue(np.all(padgrid == base)) 56 | 57 | def test_enlarge(self): 58 | for i, base in enumerate(self.grids[6:]): 59 | bbox = base.bbox 60 | if base.fill_value is None: 61 | base.fill_value = -9999 62 | cellsize = [abs(cs) for cs in base.cellsize] 63 | newbbox = { 64 | "ymin" : bbox["ymin"] - .7 * cellsize[0], 65 | "xmin" : bbox["xmin"] - 2.5 * cellsize[1], 66 | "ymax" : bbox["ymax"] + 6.1 * cellsize[0], 67 | "xmax" : bbox["xmax"] + .1 * cellsize[1] 68 | } 69 | enlrgrid = base.enlarge(**newbbox) 70 | 71 | self.assertEqual(enlrgrid.nrows, base.nrows + 1 + 7) 72 | self.assertEqual(enlrgrid.ncols, base.ncols + 3 + 1) 73 | 74 | x = np.arange(20).reshape((4,5)) 75 | grid = ga.array( 76 | x, yorigin=100, xorigin=200, 77 | origin="ll", cellsize=20, fill_value=-9) 78 | enlarged = grid.enlarge(xmin=130, xmax=200, ymin=66) 79 | self.assertDictEqual( 80 | enlarged.bbox, 81 | {'xmin': 120, 'ymin': 60, 'ymax': 180, 'xmax': 300}) 82 | 83 | def test_shrink(self): 84 | for base in self.grids: 85 | bbox = base.bbox 86 | cellsize = [abs(cs) for cs in base.cellsize] 87 | newbbox = { 88 | "ymin" : bbox["ymin"] + .7 * cellsize[0], 89 | "xmin" : bbox["xmin"] + 2.5 * cellsize[1], 90 | "ymax" : bbox["ymax"] - 6.1 * cellsize[0], 91 | "xmax" : bbox["xmax"] - .1 * cellsize[1], 92 | } 93 | shrgrid = base.shrink(**newbbox) 94 | self.assertEqual(shrgrid.nrows, base.nrows - 0 - 6) 95 | self.assertEqual(shrgrid.ncols, base.ncols - 2 - 0) 96 | 97 | def test_removeCells(self): 98 | for base in self.grids: 99 | rmgrid = base.removeCells(1,1,1,1) 100 | self.assertEqual(np.sum(rmgrid - base[...,1:-1,1:-1]) , 0) 101 | rmgrid = base.removeCells(0,0,0,0) 102 | self.assertEqual(np.sum(rmgrid - base) , 0) 103 | 104 | def test_trim(self): 105 | for base in self.grids: 106 | trimgrid = base.trim() 107 | self.assertTrue(np.any(trimgrid[0,...] != base.fill_value)) 108 | self.assertTrue(np.any(trimgrid[-1,...] != base.fill_value)) 109 | self.assertTrue(np.any(trimgrid[...,0] != base.fill_value)) 110 | self.assertTrue(np.any(trimgrid[...,-1] != base.fill_value)) 111 | 112 | # def test_snap(self): 113 | # for base in self.grids: 114 | # offsets = ( 115 | # (-75,-30), 116 | # (np.array(base.cellsize) *.9, np.array(base.cellsize) *20), 117 | # (base.yorigin * -1.1, base.xorigin * 1.89), 118 | # ) 119 | 120 | # for yoff,xoff in offsets: 121 | # grid = copy.deepcopy(base) 122 | # grid.yorigin -= yoff 123 | # grid.xorigin -= xoff 124 | # yorg, xorg = grid.getCorner() 125 | # grid.snap(base) 126 | 127 | # xdelta = abs(grid.xorigin - xorg) 128 | # ydelta = abs(grid.yorigin - yorg) 129 | 130 | # # asure the shift to the next cell 131 | # self.assertLessEqual(ydelta, base.cellsize[0]/2) 132 | # self.assertLessEqual(xdelta, base.cellsize[1]/2) 133 | 134 | # # grid origin is shifted to a cell multiple of self.grid.origin 135 | # self.assertEqual((grid.yorigin - grid.yorigin)%grid.cellsize[0], 0) 136 | # self.assertEqual((grid.xorigin - grid.xorigin)%grid.cellsize[1], 0) 137 | 138 | def test_coordinatesOf(self): 139 | for base in self.grids: 140 | offset = np.abs(base.cellsize) 141 | bbox = base.bbox 142 | 143 | idxs = ( 144 | (0,0), 145 | (base.nrows-1, base.ncols-1), 146 | (0,base.ncols-1), 147 | (base.nrows-1,0) 148 | ) 149 | expected = ( 150 | (bbox["ymax"], bbox["xmin"]), 151 | (bbox["ymin"]+offset[0], bbox["xmax"]-offset[1]), 152 | (bbox["ymax"], bbox["xmax"]-offset[1]), 153 | (bbox["ymin"]+offset[0], bbox["xmin"]) 154 | ) 155 | 156 | for idx, e in zip(idxs, expected): 157 | self.assertTupleEqual(base.coordinatesOf(*idx), e) 158 | 159 | def test_indexOf(self): 160 | for base in self.grids: 161 | offset = np.abs(base.cellsize)*.8 162 | bbox = base.bbox 163 | 164 | coodinates = ( 165 | (bbox["ymax"], bbox["xmin"]), 166 | (bbox["ymin"]+offset[0], bbox["xmax"]-offset[1]), 167 | (bbox["ymax"], bbox["xmax"]-offset[1]), 168 | (bbox["ymin"]+offset[0], bbox["xmin"]) 169 | ) 170 | 171 | expected = ( 172 | (0,0), 173 | (base.nrows-1, base.ncols-1), 174 | (0,base.ncols-1), 175 | (base.nrows-1,0) 176 | ) 177 | 178 | for c, e in zip(coodinates, expected): 179 | self.assertTupleEqual(base.indexOf(*c), e) 180 | 181 | def test_project(self): 182 | 183 | def assertGeoArrayEqual(ga1, ga2): 184 | self.assertTrue(np.all(ga1.data == ga1.data)) 185 | self.assertTrue(np.all(ga2.mask == ga2.mask)) 186 | self.assertDictEqual(ga1.bbox, ga2.bbox) 187 | self.assertEqual(ga1.proj, ga2.proj) 188 | 189 | epsgcodes = (32632, 32634) 190 | error = 0 191 | cellsize = 1000 192 | warpcmd = "gdalwarp -r 'near' -tr {cellsize} {cellsize} -et {error} -s_srs '{sproj}' -t_srs 'EPSG:{tproj}' {sfname} {tfname}" 193 | 194 | for fname, base in zip(self.fnames, self.grids): 195 | for epsgcode in epsgcodes: 196 | proj = ga.project( 197 | grid = base, 198 | proj = {"init":"epsg:{:}".format(epsgcode)}, 199 | func = "nearest", 200 | cellsize = cellsize, 201 | max_error = 0) 202 | 203 | with tempfile.NamedTemporaryFile(suffix=".tif") as tf: 204 | subprocess.check_output( 205 | warpcmd.format(error=error, cellsize=cellsize, 206 | sproj=base.proj, tproj=epsgcode, 207 | sfname=fname, tfname=tf.name), 208 | shell=True) 209 | compare = ga.fromfile(tf.name).trim() 210 | try: 211 | assertGeoArrayEqual(proj, compare) 212 | except AssertionError: 213 | if fname.endswith("png"): 214 | # the fill_vaue is not set correctly with the png-driver 215 | compare.fill_value = 0 216 | assertGeoArrayEqual(proj, compare.trim()) 217 | else: 218 | raise 219 | 220 | 221 | if __name__== "__main__": 222 | unittest.main() 223 | -------------------------------------------------------------------------------- /test/test_projection.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest 5 | import warnings 6 | import geoarray as ga 7 | import numpy as np 8 | 9 | class Test(unittest.TestCase): 10 | 11 | def test_correctValue(self): 12 | g = ga.ones((200,300), proj=3857) # WGS 84 / Pseudo-Mercator aka Web-Mercator 13 | self.assertTrue(g.proj) 14 | 15 | def test_incorrectValue(self): 16 | with warnings.catch_warnings(record=True) as w: 17 | warnings.simplefilter("always") 18 | # With some Python versions w is empty, so skip the test... 19 | if w: 20 | ga.ones((200,300), proj=4444) # invalid epsg code 21 | self.assertEqual(str(w[0].message), "Projection not understood") 22 | self.assertEqual(w[0].category, RuntimeWarning) 23 | -------------------------------------------------------------------------------- /test/test_utils.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import geoarray as ga 6 | import os 7 | 8 | PWD = os.path.abspath(os.path.dirname(__file__)) 9 | TMPPATH = os.path.join(PWD, "tmp") 10 | 11 | def testArray(shape): 12 | dinfo = dtypeInfo(np.int32) 13 | data = np.arange(np.prod(shape), dtype=np.float32).reshape(shape) 14 | out = ga.array( 15 | # data = np.random.randint(dinfo["min"], high=dinfo["max"], size=shape, dtype=np.int32), 16 | data = data, 17 | proj = 32633, # WGS 84 / UTM 33N 18 | origin = "ul", 19 | yorigin = 9000000, 20 | xorigin = 170000, 21 | cellsize = 1000, 22 | fill_value = -9999, 23 | color_mode = "L") 24 | return out 25 | 26 | def createDirectory(path): 27 | try: 28 | os.mkdir(path) 29 | except OSError: 30 | pass 31 | 32 | def removeTestFiles(): 33 | try: 34 | shutil.rmtree(TMPPATH) 35 | except: 36 | pass 37 | 38 | def createTestFiles(): 39 | createDirectory(TMPPATH) 40 | arrays = ( 41 | testArray((340, 270)), 42 | testArray((4, 340, 270)) 43 | ) 44 | files, fnames = [], [] 45 | for ending in ga._DRIVER_DICT: 46 | for i, arr in enumerate(arrays): 47 | fname = os.path.join(TMPPATH, "test-{:}{:}".format(i, ending)) 48 | try: 49 | arr.tofile(fname) 50 | except RuntimeError: 51 | continue 52 | files.append(ga.fromfile(fname)) 53 | fnames.append(fname) 54 | return tuple(fnames), tuple(files) 55 | 56 | def dtypeInfo(dtype): 57 | try: 58 | tinfo = np.finfo(dtype) 59 | except ValueError: 60 | tinfo = np.iinfo(dtype) 61 | return {"min": tinfo.min, "max": tinfo.max} 62 | -------------------------------------------------------------------------------- /test/test_wrapper.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import unittest, copy, shutil, os 5 | import numpy as np 6 | import geoarray as ga 7 | import gdal 8 | import warnings 9 | import subprocess 10 | import tempfile 11 | 12 | # all tests, run from main directory: 13 | # python -m unittest discover test 14 | 15 | # this test only, run from parent directory run 16 | # python -m unittest test.test_wrapper 17 | 18 | class Test(unittest.TestCase): 19 | 20 | def test_array(self): 21 | data = np.arange(48).reshape(2, 4, 6) 22 | fill_value = -42 23 | yorigin = -15 24 | xorigin = 72 25 | cellsize = (33.33, 33.33) 26 | grid = ga.array( 27 | data=data, fill_value=fill_value, 28 | yorigin=yorigin, xorigin=xorigin, 29 | ycellsize=cellsize[0], xcellsize=cellsize[1]) 30 | 31 | self.assertEqual(grid.shape, data.shape) 32 | self.assertEqual(grid.fill_value, fill_value) 33 | self.assertEqual(grid.yorigin, yorigin) 34 | self.assertEqual(grid.xorigin, xorigin) 35 | self.assertEqual(grid.cellsize, cellsize) 36 | self.assertTrue(np.all(grid == data)) 37 | 38 | def test_zeros(self): 39 | shape = (2, 4, 6) 40 | grid = ga.zeros(shape) 41 | self.assertEqual(grid.shape, shape) 42 | self.assertTrue(np.all(grid == 0)) 43 | 44 | def test_ones(self): 45 | shape = (2, 4, 6) 46 | grid = ga.ones(shape) 47 | self.assertEqual(grid.shape, shape) 48 | self.assertTrue(np.all(grid == 1)) 49 | 50 | def test_full(self): 51 | shape = (2, 4, 6) 52 | fill_value = 42 53 | grid = ga.full(shape,fill_value) 54 | self.assertEqual(grid.shape, shape) 55 | self.assertTrue(np.all(grid == fill_value)) 56 | 57 | def test_empty(self): 58 | shape = (2, 4, 6) 59 | fill_value = 42 60 | grid = ga.empty(shape,fill_value=fill_value) 61 | self.assertEqual(grid.shape, shape) 62 | 63 | def test_ones_zeros_like(self): 64 | grid = ga.array( 65 | data=np.arange(48).reshape(2, 4, 6), fill_value=-42, 66 | yorigin=-15, xorigin=72, 67 | ycellsize=33.33, xcellsize=33.33) 68 | 69 | cases = [(ga.ones_like, 1), (ga.zeros_like, 0)] 70 | for like_func, value in cases: 71 | test = like_func(grid) 72 | self.assertTupleEqual(test.shape, grid.shape) 73 | self.assertTrue(np.all(test == value)) 74 | self.assertDictEqual(test.header, grid.header) 75 | 76 | def test_full_like(self): 77 | grid = ga.array( 78 | data=np.arange(48).reshape(2, 4, 6), 79 | fill_value=-42, 80 | yorigin=-15, xorigin=72, 81 | ycellsize=33.33, xcellsize=33.33) 82 | 83 | value = -4444 84 | test = ga.full_like(grid, value) 85 | self.assertTupleEqual(test.shape, grid.shape) 86 | self.assertTrue(np.all(test == value)) 87 | self.assertDictEqual(test.header, grid.header) 88 | 89 | def test_geoloc(self): 90 | 91 | data = np.arange(48).reshape(2, 4, 6) 92 | xvals = np.array([4, 8, 8.5, 13, 14, 17]) 93 | yvals = np.array([18, 17, 14, 12]) 94 | xvalues, yvalues = np.meshgrid(xvals, yvals) 95 | 96 | gridul = ga.array( 97 | data=data, fill_value=-42, 98 | yvalues=yvalues, xvalues=xvalues) 99 | 100 | self.assertEqual(gridul.yorigin, yvals[0]) 101 | self.assertEqual(gridul.xorigin, xvals[0]) 102 | 103 | gridlr = ga.array( 104 | data=data, fill_value=-42, origin="lr", 105 | yvalues=yvalues, xvalues=xvalues) 106 | 107 | self.assertEqual(gridlr.yorigin, yvals[-1]) 108 | self.assertEqual(gridlr.xorigin, xvals[-1]) 109 | 110 | 111 | if __name__== "__main__": 112 | unittest.main() 113 | --------------------------------------------------------------------------------