| 1 | # Datasets/Virtual.py - Classes representing virtual datasets (e.g.
|
|---|
| 2 | # ClippedGrid).
|
|---|
| 3 | #
|
|---|
| 4 | # Copyright (C) 2010 Jason J. Roberts
|
|---|
| 5 | #
|
|---|
| 6 | # This program is free software; you can redistribute it and/or
|
|---|
| 7 | # modify it under the terms of the GNU General Public License
|
|---|
| 8 | # as published by the Free Software Foundation; either version 2
|
|---|
| 9 | # of the License, or (at your option) any later version.
|
|---|
| 10 | #
|
|---|
| 11 | # This program is distributed in the hope that it will be useful,
|
|---|
| 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|---|
| 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|---|
| 14 | # GNU General Public License (available in the file LICENSE.TXT)
|
|---|
| 15 | # for more details.
|
|---|
| 16 | #
|
|---|
| 17 | # You should have received a copy of the GNU General Public License
|
|---|
| 18 | # along with this program; if not, write to the Free Software
|
|---|
| 19 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|---|
| 20 |
|
|---|
| 21 | import bisect
|
|---|
| 22 | import datetime
|
|---|
| 23 |
|
|---|
| 24 | from GeoEco.Datasets import DatasetCollection, QueryableAttribute, Grid
|
|---|
| 25 | from GeoEco.DynamicDocString import DynamicDocString
|
|---|
| 26 | from GeoEco.Internationalization import _
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 | class TimeSeriesGridStack(Grid):
|
|---|
| 30 | __doc__ = DynamicDocString()
|
|---|
| 31 |
|
|---|
| 32 | def _GetReportProgress(self):
|
|---|
| 33 | return self._ReportProgress
|
|---|
| 34 |
|
|---|
| 35 | def _SetReportProgress(self, value):
|
|---|
| 36 | # TOOO: Validation
|
|---|
| 37 | self._ReportProgress = value
|
|---|
| 38 |
|
|---|
| 39 | ReportProgress = property(_GetReportProgress, _SetReportProgress, doc=DynamicDocString())
|
|---|
| 40 |
|
|---|
| 41 | def __init__(self, collection, expression=None, reportProgress=True, **options):
|
|---|
| 42 | # TODO: self.__class__.__doc__.Obj.ValidateMethodInvocation()
|
|---|
| 43 |
|
|---|
| 44 | # TODO: What can we validate about the collection without
|
|---|
| 45 | # accessing it?
|
|---|
| 46 |
|
|---|
| 47 | # Validate that the collection has a queryable attribute with
|
|---|
| 48 | # data type DateTimeTypeMetadata.
|
|---|
| 49 |
|
|---|
| 50 | from GeoEco.Types import DateTimeTypeMetadata
|
|---|
| 51 |
|
|---|
| 52 | dateTimeAttrs = collection.GetQueryableAttributesWithDataType(DateTimeTypeMetadata)
|
|---|
| 53 | if len(dateTimeAttrs) <= 0:
|
|---|
| 54 | raise ValueError(_(u'This dataset collection does not have a queryable attribute defined with the data type DateTimeTypeMetadata. In order to build a TimeSeriesGridStack from it, it must have an attribute with that data type.'))
|
|---|
| 55 | if len(dateTimeAttrs) > 1: # Should never happen; CollectibleObject.__init__ prevents it
|
|---|
| 56 | raise ValueError(_(u'This dataset collection has multiple queryable attributes defined with the data type DateTimeTypeMetadata. In order to build a TimeSeriesGridStack from it, only one queryable attribute of that type must be defined.'))
|
|---|
| 57 |
|
|---|
| 58 | # Query the collection for the oldest grid within it.
|
|---|
| 59 |
|
|---|
| 60 | self._CachedOldestGrid = collection.GetOldestDataset(expression, **options)
|
|---|
| 61 | if not issubclass(self._CachedOldestGrid.__class__, Grid):
|
|---|
| 62 | raise TypeError(_(u'The dataset collection %(dn)s does not contain Grid datasets. %(cls)s can only be used with dataset collections that contain Grid datasets.') % {u'dn': collection.DisplayName, u'cls': self.__class__.__name__})
|
|---|
| 63 |
|
|---|
| 64 | # Copy all of the queryable attributes, except the
|
|---|
| 65 | # DateTimeTypeMetadata one, and their values from the oldest
|
|---|
| 66 | # grid. All of the grids returned by the expression should
|
|---|
| 67 | # have the same values for any given queryable attribute
|
|---|
| 68 | # (except the DateTimeTypeMetadata attribute and attributes
|
|---|
| 69 | # derived from it). We do not verify this, however.
|
|---|
| 70 |
|
|---|
| 71 | queryableAttributes = []
|
|---|
| 72 | obj = self._CachedOldestGrid
|
|---|
| 73 | while obj is not None:
|
|---|
| 74 | if obj._QueryableAttributes is not None:
|
|---|
| 75 | for attr in obj._QueryableAttributes:
|
|---|
| 76 | if attr.Name != dateTimeAttrs[0].Name:
|
|---|
| 77 | queryableAttributes.append(QueryableAttribute(attr.Name, attr.DisplayName, attr.DataType, None, attr.DerivedFromAttr, attr.DerivedValueMap, attr.DerivedValueFunc)) # Do not copy attr.DerivedLazyDatasetProps
|
|---|
| 78 | obj = obj.ParentCollection
|
|---|
| 79 |
|
|---|
| 80 | queryableAttributeValues = {}
|
|---|
| 81 | for attr in queryableAttributes:
|
|---|
| 82 | if attr.DerivedFromAttr != dateTimeAttrs[0].Name:
|
|---|
| 83 | queryableAttributeValues[attr.Name] = self._CachedOldestGrid.GetQueryableAttributeValue(attr.Name)
|
|---|
| 84 |
|
|---|
| 85 | # Initialize our properties.
|
|---|
| 86 |
|
|---|
| 87 | self._Collection = collection
|
|---|
| 88 | self._Expression = expression
|
|---|
| 89 | self._ReportProgress = reportProgress
|
|---|
| 90 | self._Options = options
|
|---|
| 91 | self._DateTimeAttrName = dateTimeAttrs[0].Name
|
|---|
| 92 |
|
|---|
| 93 | # Initialize the base class.
|
|---|
| 94 |
|
|---|
| 95 | super(TimeSeriesGridStack, self).__init__(queryableAttributes=tuple(queryableAttributes), queryableAttributeValues=queryableAttributeValues)
|
|---|
| 96 |
|
|---|
| 97 | def _GetDisplayName(self):
|
|---|
| 98 | return self._Collection.DisplayName
|
|---|
| 99 |
|
|---|
| 100 | def _GetLazyPropertyPhysicalValue(self, name):
|
|---|
| 101 |
|
|---|
| 102 | # If the oldest grid does not have a t dimension already, we
|
|---|
| 103 | # are stacking a collection of 2D (yx) or 3D (zyx) grids into
|
|---|
| 104 | # a 3D (tyx) or 4D (tzyx) stack.
|
|---|
| 105 |
|
|---|
| 106 | if 't' not in self._CachedOldestGrid.Dimensions:
|
|---|
| 107 |
|
|---|
| 108 | # Handle properties that are not identical to but are
|
|---|
| 109 | # easily calculated from the values of the oldest grid.
|
|---|
| 110 |
|
|---|
| 111 | if name == 'Dimensions':
|
|---|
| 112 | return 't' + self._CachedOldestGrid.Dimensions
|
|---|
| 113 |
|
|---|
| 114 | if name == 'PhysicalDimensions':
|
|---|
| 115 | return 't' + self._CachedOldestGrid.Dimensions # Transposing of the underlying time slices is done when we fetch each slice, thus they are all properly ordered by the time we receive them.
|
|---|
| 116 |
|
|---|
| 117 | if name == 'PhysicalDimensionsFlipped':
|
|---|
| 118 | return tuple([False] * (len(self._CachedOldestGrid.Dimensions) + 1)) # Flipping of the underlying time slices is done when we fetch each slice, thus they are all properly oriented by the time we receive them.
|
|---|
| 119 |
|
|---|
| 120 | if name == 'CoordDependencies':
|
|---|
| 121 | return tuple([None] + list(self._CachedOldestGrid.CoordDependencies))
|
|---|
| 122 |
|
|---|
| 123 | if name == 'CoordIncrements':
|
|---|
| 124 | return tuple([self._CachedOldestGrid.GetLazyPropertyValue('TIncrement')] + list(self._CachedOldestGrid.CoordIncrements))
|
|---|
| 125 |
|
|---|
| 126 | if name == 'CornerCoords':
|
|---|
| 127 | return tuple([self._CachedOldestGrid.GetQueryableAttributeValue(self._DateTimeAttrName)] + list(self._CachedOldestGrid.GetLazyPropertyValue('CornerCoords')))
|
|---|
| 128 |
|
|---|
| 129 | # Handle the shape. This is more complicated because we
|
|---|
| 130 | # have to determine how many time slices there are.
|
|---|
| 131 |
|
|---|
| 132 | if name == 'Shape':
|
|---|
| 133 |
|
|---|
| 134 | # If the t increment is not None, we do not need to
|
|---|
| 135 | # retrieve the full list of grids to know how many
|
|---|
| 136 | # time slices there are. Instead, we can calculate how
|
|---|
| 137 | # many must appear between the oldest grid and the
|
|---|
| 138 | # newest grid.
|
|---|
| 139 |
|
|---|
| 140 | if self.CoordIncrements[0] is not None:
|
|---|
| 141 | newestGrid = self._Collection.GetNewestDataset(self._Expression, **self._Options)
|
|---|
| 142 | if not issubclass(newestGrid.__class__, Grid):
|
|---|
| 143 | raise TypeError(_(u'The dataset collection %(dn)s does not contain Grid datasets. %(cls)s can only be used with dataset collections that contain Grid datasets.') % {u'dn': self._Collection.DisplayName, u'cls': self.__class__.__name__})
|
|---|
| 144 | newestGridDateTime = newestGrid.GetQueryableAttributeValue(self._DateTimeAttrName)
|
|---|
| 145 |
|
|---|
| 146 | # Estimate the number of time slices between the
|
|---|
| 147 | # oldest grid and newest grid.
|
|---|
| 148 |
|
|---|
| 149 | delta = newestGridDateTime - self._CachedOldestGrid.GetQueryableAttributeValue(self._DateTimeAttrName)
|
|---|
| 150 | delta = delta.days * 86400. + delta.seconds
|
|---|
| 151 |
|
|---|
| 152 | if self.TIncrementUnit == 'year':
|
|---|
| 153 | numTimeSlices = int(1.1 * delta / 86400. / 365.)
|
|---|
| 154 | elif self.TIncrementUnit == 'season':
|
|---|
| 155 | numTimeSlices = int(1.1 * delta / 86400. / 365. * 4.)
|
|---|
| 156 | elif self.TIncrementUnit == 'month':
|
|---|
| 157 | numTimeSlices = int(1.1 * delta / 86400. / 365. * 12.)
|
|---|
| 158 | elif self.TIncrementUnit == 'day':
|
|---|
| 159 | numTimeSlices = int(1.1 * delta / 86400.)
|
|---|
| 160 | elif self.TIncrementUnit == 'hour':
|
|---|
| 161 | numTimeSlices = int(1.1 * delta / 3600.)
|
|---|
| 162 | elif self.TIncrementUnit == 'minute':
|
|---|
| 163 | numTimeSlices = int(1.1 * delta / 60.)
|
|---|
| 164 | elif self.TIncrementUnit == 'second':
|
|---|
| 165 | numTimeSlices = int(1.1 * delta)
|
|---|
| 166 | else:
|
|---|
| 167 | raise NotImplementedError(_(u'Programming error in this tool: the t increment unit \'%(unit)s\' is unknown. Please contact the author of this tool for assistance.') % {u'unit': self.TIncrementUnit})
|
|---|
| 168 |
|
|---|
| 169 | # Get a list of t coordinates starting with the
|
|---|
| 170 | # first time slice.
|
|---|
| 171 |
|
|---|
| 172 | tCornerCoordType = self.GetLazyPropertyValue('TCornerCoordType').lower()
|
|---|
| 173 | if tCornerCoordType == 'min':
|
|---|
| 174 | fixedIncrementOffset = -0.5
|
|---|
| 175 | elif fixedIncrementOffset == 'center':
|
|---|
| 176 | fixedIncrementOffset = 0.0
|
|---|
| 177 | elif fixedIncrementOffset == 'max':
|
|---|
| 178 | fixedIncrementOffset = 0.0
|
|---|
| 179 | else:
|
|---|
| 180 | raise NotImplementedError(_(u'Programming error in this tool: the t corner coordinate type \'%(type)s\' is unknown. Please contact the author of this tool for assistance.') % {u'type': self.GetLazyPropertyValue('TCornerCoordType')})
|
|---|
| 181 |
|
|---|
| 182 | tCoords = self._GetTCoordsList(fixedIncrementOffset, numTimeSlices)
|
|---|
| 183 |
|
|---|
| 184 | # While the time of the newest grid is newer than
|
|---|
| 185 | # the newest t coordinate, double the size of the
|
|---|
| 186 | # list. This should probably never happen.
|
|---|
| 187 |
|
|---|
| 188 | while tCoords[-1] < newestGridDateTime:
|
|---|
| 189 | numTimeSlices *= 2
|
|---|
| 190 | tCoords = self._GetTCoordsList(fixedIncrementOffset, numTimeSlices)
|
|---|
| 191 |
|
|---|
| 192 | # Search the t coordinates backwards for the time
|
|---|
| 193 | # of the newest grid. This tells us the number of
|
|---|
| 194 | # time slices. If we do not find it, something odd
|
|---|
| 195 | # is going on; the parsed datetime is inconsistent
|
|---|
| 196 | # with the definition of the dataset.
|
|---|
| 197 |
|
|---|
| 198 | i = len(tCoords) - 1
|
|---|
| 199 | while i >= 0:
|
|---|
| 200 | if tCoords[i] == newestGridDateTime:
|
|---|
| 201 | break
|
|---|
| 202 | if tCoords[i] < newestGridDateTime:
|
|---|
| 203 | raise ValueError(_(u'Failed to compute a time coordinate that matched the time of the newest grid in this %(cls)s. The datetime of that grid is %(last)s but the two closest time coordinates are %(dt1)s and %(dt2)s.') % {u'cls': self.__class__.__name__, u'last': newestGridDateTime.strftime('%Y-%m-%d %H:%M:%S'), u'dt1': tCoords[i].strftime('%Y-%m-%d %H:%M:%S'), u'dt2': tCoords[i+1].strftime('%Y-%m-%d %H:%M:%S')})
|
|---|
| 204 | i -= 1
|
|---|
| 205 | if i < 0:
|
|---|
| 206 | raise ValueError(_(u'Programming error in this tool: The datetime of the newest grid in this %(cls)s is %(last)s, which comes before the datetime of the first time coordinate, %(dt1)s. Please contact the author of this tool for assistance') % {u'cls': self.__class__.__name__, u'last': newestGridDateTime.strftime('%Y-%m-%d %H:%M:%S'), u'dt1': tCoords[0].strftime('%Y-%m-%d %H:%M:%S')})
|
|---|
| 207 |
|
|---|
| 208 | # Set and return the shape.
|
|---|
| 209 |
|
|---|
| 210 | shape = tuple([i+1] + list(self._CachedOldestGrid.Shape))
|
|---|
| 211 | self.SetLazyPropertyValue('Shape', shape)
|
|---|
| 212 | return shape
|
|---|
| 213 |
|
|---|
| 214 | # The t increment is None. We need to retrieve the
|
|---|
| 215 | # full list of grids to know the number of time
|
|---|
| 216 | # slices. We do not currently support this. TODO:
|
|---|
| 217 | # implement it.
|
|---|
| 218 |
|
|---|
| 219 | raise NotImplementedError(_(u'The dataset collection %(dn)s contains grids that do not have a fixed time increment. The current implementation of %(cls)s does not support grids of this kind.') % {u'dn': self._Collection.DisplayName, u'cls': self.__class__.__name__})
|
|---|
| 220 |
|
|---|
| 221 | # If the contained grids have a t dimension already, we are
|
|---|
| 222 | # concatenating a collection of 3D (tyx) or 4D (tzyx) grids.
|
|---|
| 223 | # The stack will have the same dimensions as an individual
|
|---|
| 224 | # grid in the collection. We do not currently support this.
|
|---|
| 225 | # TODO: implement it.
|
|---|
| 226 |
|
|---|
| 227 | else:
|
|---|
| 228 | raise NotImplementedError(_(u'The dataset collection %(dn)s contains grids that have a time dimension. The current implementation of %(cls)s does not support grids with a time dimension.') % {u'dn': self._Collection.DisplayName, u'cls': self.__class__.__name__})
|
|---|
| 229 |
|
|---|
| 230 | # If we got to here, the caller has requested a lazy property
|
|---|
| 231 | # that is assumed to be the same for all grids in the
|
|---|
| 232 | # collection as well as the stack itself. Return the value
|
|---|
| 233 | # from the oldest grid.
|
|---|
| 234 |
|
|---|
| 235 | return self._CachedOldestGrid.GetLazyPropertyValue(name)
|
|---|
| 236 |
|
|---|
| 237 | def _GetCoords(self, coord, sliceList):
|
|---|
| 238 | return self._CachedOldestGrid._GetCoords(coord, sliceList)
|
|---|
| 239 |
|
|---|
| 240 | def _ReadNumpyArray(self, sliceList):
|
|---|
| 241 |
|
|---|
| 242 | # Get a list of t coordinates for the requested time slices.
|
|---|
| 243 |
|
|---|
| 244 | tCornerCoordType = self.GetLazyPropertyValue('TCornerCoordType').lower()
|
|---|
| 245 | if tCornerCoordType == 'min':
|
|---|
| 246 | tCoords = self.MinCoords.__getitem__(('t', sliceList[0]))
|
|---|
| 247 | elif fixedIncrementOffset == 'center':
|
|---|
| 248 | tCoords = self.CenterCoords.__getitem__(('t', sliceList[0]))
|
|---|
| 249 | elif fixedIncrementOffset == 'max':
|
|---|
| 250 | tCoords = self.MaxCoords.__getitem__(('t', sliceList[0]))
|
|---|
| 251 | else:
|
|---|
| 252 | raise NotImplementedError(_(u'Programming error in this tool: the t corner coordinate type \'%(type)s\' is unknown. Please contact the author of this tool for assistance.') % {u'type': self.GetLazyPropertyValue('TCornerCoordType')})
|
|---|
| 253 |
|
|---|
| 254 | # TODO: handle len(tCoords) == 0
|
|---|
| 255 |
|
|---|
| 256 | # Query the collection for a list of datasets with t
|
|---|
| 257 | # coordinates that fall within the min and max coordinates of
|
|---|
| 258 | # the requested time slices.
|
|---|
| 259 |
|
|---|
| 260 | expression = self._DateTimeAttrName + ' >= ' + tCoords[0].strftime('#%Y-%m-%d %H:%M:%S# AND Year >= %Y') + ' AND ' + self._DateTimeAttrName + ' <= ' + tCoords[-1].strftime('#%Y-%m-%d %H:%M:%S# AND Year <= %Y')
|
|---|
| 261 | if self._Expression is not None:
|
|---|
| 262 | expression += ' AND (' + self._Expression + ')'
|
|---|
| 263 |
|
|---|
| 264 | datasets = self._Collection.QueryDatasets(expression, self._ReportProgress and len(tCoords) > 1, **self._Options)
|
|---|
| 265 |
|
|---|
| 266 | # Most likely, the datasets are already sorted in ascending
|
|---|
| 267 | # time order, but this is not required. Sort them, to be sure.
|
|---|
| 268 |
|
|---|
| 269 | datasets.sort(key=lambda ds: ds.GetQueryableAttributeValue(self._DateTimeAttrName))
|
|---|
| 270 |
|
|---|
| 271 | # Allocate a numpy array to return.
|
|---|
| 272 |
|
|---|
| 273 | import numpy
|
|---|
| 274 | data = numpy.zeros(map(lambda s: s.stop - s.start, sliceList), str(self._CachedOldestGrid.UnscaledDataType))
|
|---|
| 275 |
|
|---|
| 276 | # Fill each time slice by retrieving the data from the
|
|---|
| 277 | # corresponding dataset. If we encounter a time slice for
|
|---|
| 278 | # which there is no dataset with a matching time coordinate,
|
|---|
| 279 | # allocate a slice of NoData and report a warning. If there is
|
|---|
| 280 | # no NoData value, leave the values at zero.
|
|---|
| 281 |
|
|---|
| 282 | t = 0
|
|---|
| 283 | while t < len(tCoords):
|
|---|
| 284 | while len(datasets) > 0 and datasets[0].GetQueryableAttributeValue(self._DateTimeAttrName) < tCoords[t]:
|
|---|
| 285 | if hasattr(datasets[0], '_Close'):
|
|---|
| 286 | datasets[0]._Close()
|
|---|
| 287 | elif datasets[0].ParentCollection is not None and hasattr(datasets[0].ParentCollection, '_Close') and (len(datasets) == 1 or datasets[0].ParentCollection != datasets[1].ParentCollection):
|
|---|
| 288 | datasets[0].ParentCollection._Close()
|
|---|
| 289 | del datasets[0]
|
|---|
| 290 |
|
|---|
| 291 | if len(datasets) > 0 and datasets[0].GetQueryableAttributeValue(self._DateTimeAttrName) == tCoords[t]:
|
|---|
| 292 | data[t] = datasets[0].UnscaledData.__getitem__(tuple(sliceList[1:]))
|
|---|
| 293 | elif self._CachedOldestGrid.UnscaledNoDataValue is not None:
|
|---|
| 294 | self._LogWarning(_(u'There is no data in %(dn)s for the time slice %(ts)s.') % {u'dn': self._Collection.DisplayName, u'ts': tCoords[t].strftime('%Y-%m-%d %H:%M:%S')})
|
|---|
| 295 | data[t] += self._CachedOldestGrid.UnscaledNoDataValue
|
|---|
| 296 | else:
|
|---|
| 297 | self._LogWarning(_(u'There is no data in %(dn)s for the time slice %(ts)s but the datasets in this collection do not have a NoData value defined. The values of this time slice will be set to 0.') % {u'dn': self._Collection.DisplayName, u'ts': tCoords[t].strftime('%Y-%m-%d %H:%M:%S')})
|
|---|
| 298 |
|
|---|
| 299 | t += 1
|
|---|
| 300 |
|
|---|
| 301 | while len(datasets) > 0:
|
|---|
| 302 | if hasattr(datasets[0], '_Close'):
|
|---|
| 303 | datasets[0]._Close()
|
|---|
| 304 | elif datasets[0].ParentCollection is not None and hasattr(datasets[0].ParentCollection, '_Close') and (len(datasets) == 1 or datasets[0].ParentCollection != datasets[1].ParentCollection):
|
|---|
| 305 | datasets[0].ParentCollection._Close()
|
|---|
| 306 | del datasets[0]
|
|---|
| 307 |
|
|---|
| 308 | # Return the populated numpy array.
|
|---|
| 309 |
|
|---|
| 310 | return data, self._CachedOldestGrid.UnscaledNoDataValue
|
|---|
| 311 |
|
|---|
| 312 |
|
|---|
| 313 | class GridSlice(Grid):
|
|---|
| 314 | __doc__ = DynamicDocString()
|
|---|
| 315 |
|
|---|
| 316 | def __init__(self, grid, tIndex=None, zIndex=None, tQAName=u'DateTime', tQADisplayName=_(u'Date'), tQACoordType=u'min', zQAName=u'Depth', zQADisplayName=_(u'Depth'), zQACoordType=u'center'):
|
|---|
| 317 | # TODO: Validation
|
|---|
| 318 |
|
|---|
| 319 | # Perform additional validation.
|
|---|
| 320 |
|
|---|
| 321 | if tIndex is None and zIndex is None:
|
|---|
| 322 | raise ValueError(_(u'Both tIndex and zIndex are None. A value must be provided for at least one of them.'))
|
|---|
| 323 |
|
|---|
| 324 | if tIndex is not None:
|
|---|
| 325 | if 't' not in grid.Dimensions:
|
|---|
| 326 | raise TypeError(_(u'A value was provided for tIndex but %(dn)s does not have a t dimension.') % {u'dn': grid.DisplayName})
|
|---|
| 327 | if tQAName is None:
|
|---|
| 328 | raise TypeError(_(u'If a value is provided for tIndex, a value must also be provided for tQAName.'))
|
|---|
| 329 | if tQADisplayName is None:
|
|---|
| 330 | raise TypeError(_(u'If a value is provided for tIndex, a value must also be provided for tQADisplayName.'))
|
|---|
| 331 | if tQACoordType is None:
|
|---|
| 332 | raise TypeError(_(u'If a value is provided for tIndex, a value must also be provided for tQACoordType.'))
|
|---|
| 333 |
|
|---|
| 334 | if zIndex is not None:
|
|---|
| 335 | if 'z' not in grid.Dimensions:
|
|---|
| 336 | raise TypeError(_(u'A value was provided for zIndex but %(dn)s does not have a z dimension.') % {u'dn': grid.DisplayName})
|
|---|
| 337 | if zQAName is None:
|
|---|
| 338 | raise TypeError(_(u'If a value is provided for zIndex, a value must also be provided for zQAName.'))
|
|---|
| 339 | if zQADisplayName is None:
|
|---|
| 340 | raise TypeError(_(u'If a value is provided for zIndex, a value must also be provided for zQADisplayName.'))
|
|---|
| 341 | if zQACoordType is None:
|
|---|
| 342 | raise TypeError(_(u'If a value is provided for zIndex, a value must also be provided for zQACoordType.'))
|
|---|
| 343 |
|
|---|
| 344 | # Validate the provided indices and make them positive.
|
|---|
| 345 |
|
|---|
| 346 | if tIndex is not None:
|
|---|
| 347 | if tIndex < 0:
|
|---|
| 348 | if tIndex + grid.Shape[0] < 0:
|
|---|
| 349 | raise IndexError(_(u'tIndex is out of range.'))
|
|---|
| 350 | tIndex += grid.Shape[0]
|
|---|
| 351 | elif tIndex >= grid.Shape[0]:
|
|---|
| 352 | raise IndexError(_(u'tIndex is out of range.'))
|
|---|
| 353 |
|
|---|
| 354 | if zIndex is not None:
|
|---|
| 355 | if 'z' not in grid.Dimensions:
|
|---|
| 356 | if zIndex + grid.Shape[grid.Dimensions.index('z')] < 0:
|
|---|
| 357 | raise IndexError(_(u'zIndex is out of range.'))
|
|---|
| 358 | zIndex += grid.Shape[grid.Dimensions.index('z')]
|
|---|
| 359 | elif zIndex >= grid.Shape[grid.Dimensions.index('z')]:
|
|---|
| 360 | raise IndexError(_(u'zIndex is out of range.'))
|
|---|
| 361 |
|
|---|
| 362 | # Initialize our properties.
|
|---|
| 363 |
|
|---|
| 364 | self._Grid = grid
|
|---|
| 365 | self._TIndex = tIndex
|
|---|
| 366 | self._ZIndex = zIndex
|
|---|
| 367 |
|
|---|
| 368 | if self._TIndex is not None:
|
|---|
| 369 | self._TQAName = tQAName
|
|---|
| 370 | self._TQADisplayName = tQADisplayName
|
|---|
| 371 | self._TQACoordType = tQACoordType
|
|---|
| 372 | else:
|
|---|
| 373 | self._TQAName = None
|
|---|
| 374 | self._TQADisplayName = None
|
|---|
| 375 | self._TQACoordType = None
|
|---|
| 376 |
|
|---|
| 377 | if self._ZIndex is not None:
|
|---|
| 378 | self._ZQAName = zQAName
|
|---|
| 379 | self._ZQADisplayName = zQADisplayName
|
|---|
| 380 | self._ZQACoordType = zQACoordType
|
|---|
| 381 | else:
|
|---|
| 382 | self._ZQAName = None
|
|---|
| 383 | self._ZQADisplayName = None
|
|---|
| 384 | self._ZQACoordType = None
|
|---|
| 385 |
|
|---|
| 386 | if self._TIndex is not None and self._ZIndex is not None:
|
|---|
| 387 | self._DisplayName = _(u'%(tdn)s, %(zdn)s slice [%(tIndex)i, %(zIndex)i] of %(dn)s') % {u'tdn': self._TQADisplayName.lower(), u'zdn': self._ZQADisplayName.lower(), u'tIndex': self._TIndex, u'zIndex': self._ZIndex, u'dn': self._Grid.DisplayName}
|
|---|
| 388 | elif self._TIndex is not None:
|
|---|
| 389 | self._DisplayName = _(u'%(tdn)s slice %(tIndex)i of %(dn)s') % {u'tdn': self._TQADisplayName.lower(), u'tIndex': self._TIndex, u'dn': self._Grid.DisplayName}
|
|---|
| 390 | else:
|
|---|
| 391 | self._DisplayName = _(u'%(zdn)s slice %(zIndex)i of %(dn)s') % {u'zdn': self._ZQADisplayName.lower(), u'zIndex': self._ZIndex, u'dn': self._Grid.DisplayName}
|
|---|
| 392 |
|
|---|
| 393 | # For our queryable attributes, use all of those of the grid
|
|---|
| 394 | # plus the ones for the t and/or z dimensions.
|
|---|
| 395 |
|
|---|
| 396 | queryableAttributes = []
|
|---|
| 397 | if grid._QueryableAttributes is not None:
|
|---|
| 398 | queryableAttributes.extend(grid._QueryableAttributes)
|
|---|
| 399 |
|
|---|
| 400 | queryableAttributeValues = {}
|
|---|
| 401 | if grid._QueryableAttributeValues is not None:
|
|---|
| 402 | queryableAttributeValues.update(grid._QueryableAttributeValues)
|
|---|
| 403 |
|
|---|
| 404 | if self._TIndex is not None:
|
|---|
| 405 | queryableAttributes.append(QueryableAttribute(self._TQAName, self._TQADisplayName, DateTimeTypeMetadata()))
|
|---|
| 406 | if self._TQACoordType == 'min':
|
|---|
| 407 | queryableAttributeValues[self._TQAName] = self._Grid.MinCoords['t', self._TIndex]
|
|---|
| 408 | elif self._TQACoordType == 'center':
|
|---|
| 409 | queryableAttributeValues[self._TQAName] = self._Grid.CenterCoords['t', self._TIndex]
|
|---|
| 410 | else:
|
|---|
| 411 | queryableAttributeValues[self._TQAName] = self._Grid.MaxCoords['t', self._TIndex]
|
|---|
| 412 |
|
|---|
| 413 | if self._ZIndex is not None:
|
|---|
| 414 | queryableAttributes.append(QueryableAttribute(self._ZQAName, self._ZQADisplayName, FloatTypeMetadata()))
|
|---|
| 415 | if self._ZQACoordType == 'min':
|
|---|
| 416 | queryableAttributeValues[self._ZQAName] = self._Grid.MinCoords['z', self._ZIndex]
|
|---|
| 417 | elif self._ZQACoordType == 'center':
|
|---|
| 418 | queryableAttributeValues[self._ZQAName] = self._Grid.CenterCoords['z', self._ZIndex]
|
|---|
| 419 | else:
|
|---|
| 420 | queryableAttributeValues[self._ZQAName] = self._Grid.MaxCoords['z', self._ZIndex]
|
|---|
| 421 |
|
|---|
| 422 | # Our goal is to imitate the contained grid except with fewer
|
|---|
| 423 | # dimensions. In order to do this, we have to override the
|
|---|
| 424 | # dimensions and other lazy properties related to it. Obtain
|
|---|
| 425 | # the indices of the remaining dimensions.
|
|---|
| 426 |
|
|---|
| 427 | if 'z' in self._Grid.Dimensions and self._ZIndex is None:
|
|---|
| 428 | self._RemainingDimensionIndices = [1,2,3] # It is a t slice of a tzyx grid, so remaining dimensions are zyx
|
|---|
| 429 | elif 't' in self._Grid.Dimensions and self._TIndex is None:
|
|---|
| 430 | self._RemainingDimensionIndices = [0,2,3] # It is a z slice of a tzyx grid, so remaining dimensions are tyx
|
|---|
| 431 | else:
|
|---|
| 432 | self._RemainingDimensionIndices = range(len(self._Grid.Dimensions))[-2:] # It is either a tz slice of a tzyx grid, a t slice of a tyx grid, or a z slice of a zyx grid, so remaining dimensions are yx
|
|---|
| 433 |
|
|---|
| 434 | # Initialize the base class.
|
|---|
| 435 |
|
|---|
| 436 | super(GridSlice, self).__init__(queryableAttributes=tuple(queryableAttributes), queryableAttributeValues=queryableAttributeValues)
|
|---|
| 437 |
|
|---|
| 438 | def _GetDisplayName(self):
|
|---|
| 439 | return self._DisplayName
|
|---|
| 440 |
|
|---|
| 441 | def _GetLazyPropertyPhysicalValue(self, name):
|
|---|
| 442 |
|
|---|
| 443 | # If the requested property is sequence related to the
|
|---|
| 444 | # dimensions, get the values from the grid we're slicing but
|
|---|
| 445 | # remove the element corresponding to the sliced dimension(s).
|
|---|
| 446 |
|
|---|
| 447 | if name == 'Dimensions':
|
|---|
| 448 | return u''.join([self._Grid.Dimensions[i] for i in self._RemainingDimensionIndices])
|
|---|
| 449 |
|
|---|
| 450 | if name == 'Shape':
|
|---|
| 451 | return tuple([self._Grid.Shape[i] for i in self._RemainingDimensionIndices])
|
|---|
| 452 |
|
|---|
| 453 | if name == 'CoordDependencies':
|
|---|
| 454 | return tuple([self._Grid.CoordDependencies[i] for i in self._RemainingDimensionIndices]) # TODO: to the contained lists need to be modified?
|
|---|
| 455 |
|
|---|
| 456 | if name == 'CoordIncrements':
|
|---|
| 457 | return tuple([self._Grid.CoordIncrements[i] for i in self._RemainingDimensionIndices])
|
|---|
| 458 |
|
|---|
| 459 | if name == 'CornerCoords':
|
|---|
| 460 | return tuple([self._Grid.GetLazyPropertyValue('CornerCoords')[i] for i in self._RemainingDimensionIndices])
|
|---|
| 461 |
|
|---|
| 462 | # If the requested property is PhysicalDimensions or
|
|---|
| 463 | # PhysicalDimensionsFlipped, return values indicating the
|
|---|
| 464 | # dimensions in the ideal order. The contained grid takes care
|
|---|
| 465 | # of reordering, if needed.
|
|---|
| 466 |
|
|---|
| 467 | if name == 'PhysicalDimensions':
|
|---|
| 468 | return self.Dimensions
|
|---|
| 469 |
|
|---|
| 470 | if name == 'PhysicalDimensionsFlipped':
|
|---|
| 471 | return tuple([False] * len(self.Dimensions))
|
|---|
| 472 |
|
|---|
| 473 | # Otherwise just get the unaltered value from the contained
|
|---|
| 474 | # grid.
|
|---|
| 475 |
|
|---|
| 476 | return self._Grid.GetLazyPropertyValue(name)
|
|---|
| 477 |
|
|---|
| 478 | def _GetUnscaledDataAsArray(self, key):
|
|---|
| 479 | return self._Grid._GetUnscaledDataAsArray(self._AddSlicedDimsToKey(key))
|
|---|
| 480 |
|
|---|
| 481 | def _SetUnscaledDataWithArray(self, key, value):
|
|---|
| 482 | return self._Grid._SetUnscaledDataWithArray(self._AddSlicedDimsToKey(key), value)
|
|---|
| 483 |
|
|---|
| 484 | def _AddSlicedDimsToKey(self, key):
|
|---|
| 485 |
|
|---|
| 486 | # Validate the key. Although we are calling the
|
|---|
| 487 | # _ValidateAndFlipKey function, because our
|
|---|
| 488 | # PhysicalDimensionsFlipped contains only False, none of the
|
|---|
| 489 | # key's indices will be flipped.
|
|---|
| 490 |
|
|---|
| 491 | key2 = self._ValidateAndFlipKey(key)
|
|---|
| 492 |
|
|---|
| 493 | # The key does not include the dimensions the sliced
|
|---|
| 494 | # dimensions. Add these to the key as single indices.
|
|---|
| 495 |
|
|---|
| 496 | if self._ZIndex is not None:
|
|---|
| 497 | key2.insert(0, self._ZIndex)
|
|---|
| 498 |
|
|---|
| 499 | if self._TIndex is not None:
|
|---|
| 500 | key2.insert(0, self._TIndex)
|
|---|
| 501 |
|
|---|
| 502 | # Return a tuple.
|
|---|
| 503 |
|
|---|
| 504 | return tuple(key2)
|
|---|
| 505 |
|
|---|
| 506 |
|
|---|
| 507 | class GridSliceCollection(DatasetCollection):
|
|---|
| 508 | __doc__ = DynamicDocString()
|
|---|
| 509 |
|
|---|
| 510 | def __init__(self, grid, tQAName=u'DateTime', tQADisplayName=_(u'Time'), tQACoordType=u'min', zQAName=u'Depth', zQADisplayName=_(u'Depth'), zQACoordType=u'center'):
|
|---|
| 511 | # TODO: Validation
|
|---|
| 512 |
|
|---|
| 513 | # Perform additional validation.
|
|---|
| 514 |
|
|---|
| 515 | if tQAName is not None and tQADisplayName is None:
|
|---|
| 516 | raise TypeError(_(u'If a value is provided for tQAName, a value must also be provided for tQADisplayName.'))
|
|---|
| 517 |
|
|---|
| 518 | if tQAName is not None and tQACoordType is None:
|
|---|
| 519 | raise TypeError(_(u'If a value is provided for tQAName, a value must also be provided for tQACoordType.'))
|
|---|
| 520 |
|
|---|
| 521 | if zQAName is not None and zQADisplayName is None:
|
|---|
| 522 | raise TypeError(_(u'If a value is provided for zQAName, a value must also be provided for zQADisplayName.'))
|
|---|
| 523 |
|
|---|
| 524 | if zQAName is not None and zQACoordType is None:
|
|---|
| 525 | raise TypeError(_(u'If a value is provided for zQAName, a value must also be provided for zQACoordType.'))
|
|---|
| 526 |
|
|---|
| 527 | if not ('t' in grid.Dimensions or 'z' in grid.Dimensions):
|
|---|
| 528 | raise ValueError(_(u'Cannot construct a GridSliceCollection from %(dn)s because it does not have a t dimension or a z dimension.') % {u'dn': grid.DisplayName})
|
|---|
| 529 |
|
|---|
| 530 | if not ('t' in grid.Dimensions and tQAName is not None or 'z' in grid.Dimensions and zQAName is not None):
|
|---|
| 531 | raise ValueError(_(u'Cannot construct a GridSliceCollection from %(dn)s. Although it has a t dimension and/or z dimension, it was not sliced by that (or those) dimensions. It must be sliced by at least one of them.') % {u'dn': grid.DisplayName})
|
|---|
| 532 |
|
|---|
| 533 | # Initialize our properties.
|
|---|
| 534 |
|
|---|
| 535 | self._Grid = grid
|
|---|
| 536 |
|
|---|
| 537 | if 't' in grid.Dimensions and tQAName is not None:
|
|---|
| 538 | self._TQAName = tQAName
|
|---|
| 539 | self._TQADisplayName = tQADisplayName
|
|---|
| 540 | self._TQACoordType = tQACoordType
|
|---|
| 541 | else:
|
|---|
| 542 | self._TQAName = None
|
|---|
| 543 | self._TQADisplayName = None
|
|---|
| 544 | self._TQACoordType = None
|
|---|
| 545 |
|
|---|
| 546 | if 'z' in grid.Dimensions and zQAName is not None:
|
|---|
| 547 | self._ZQAName = zQAName
|
|---|
| 548 | self._ZQADisplayName = zQADisplayName
|
|---|
| 549 | self._ZQACoordType = zQACoordType
|
|---|
| 550 | else:
|
|---|
| 551 | self._ZQAName = None
|
|---|
| 552 | self._ZQADisplayName = None
|
|---|
| 553 | self._ZQACoordType = None
|
|---|
| 554 |
|
|---|
| 555 | if self._TQAName is not None and self._ZQAName is not None:
|
|---|
| 556 | self._DisplayName = _(u'%(tdn)s and %(zdn)s slices of %(dn)s') % {u'tdn': self._TQADisplayName.lower(), u'zdn': self._ZQADisplayName.lower(), u'dn': self._Grid.DisplayName}
|
|---|
| 557 | elif self._TQAName is not None:
|
|---|
| 558 | self._DisplayName = _(u'%(tdn)s slices of %(dn)s') % {u'tdn': self._TQADisplayName.lower(), u'dn': self._Grid.DisplayName}
|
|---|
| 559 | else:
|
|---|
| 560 | self._DisplayName = _(u'%(zdn)s slices of %(dn)s') % {u'zdn': self._ZQADisplayName.lower(), u'dn': self._Grid.DisplayName}
|
|---|
| 561 |
|
|---|
| 562 | # For our queryable attributes, use all of those of the grid
|
|---|
| 563 | # and its parents plus the ones for the t and/or z dimensions.
|
|---|
| 564 |
|
|---|
| 565 | queryableAttributes = []
|
|---|
| 566 | obj = grid
|
|---|
| 567 | while obj is not None:
|
|---|
| 568 | if obj._QueryableAttributes is not None:
|
|---|
| 569 | queryableAttributes.extend(obj._QueryableAttributes)
|
|---|
| 570 | obj = obj.ParentCollection
|
|---|
| 571 |
|
|---|
| 572 | if self._TQAName is not None:
|
|---|
| 573 | queryableAttributes.append(QueryableAttribute(self._TQAName, self._TQADisplayName, DateTimeTypeMetadata()))
|
|---|
| 574 |
|
|---|
| 575 | if self._ZQAName is not None:
|
|---|
| 576 | queryableAttributes.append(QueryableAttribute(self._ZQAName, self._ZQADisplayName, FloatTypeMetadata()))
|
|---|
| 577 |
|
|---|
| 578 | # Initialize the base class.
|
|---|
| 579 |
|
|---|
| 580 | super(GridSliceCollection, self).__init__(parentCollection=grid.ParentCollection, queryableAttributes=tuple(queryableAttributes))
|
|---|
| 581 |
|
|---|
| 582 | def _GetDisplayName(self):
|
|---|
| 583 | return self._DisplayName
|
|---|
| 584 |
|
|---|
| 585 | def _QueryDatasets(self, parsedExpression, progressReporter, options, parentAttrValues):
|
|---|
| 586 | return self._QueryGridSlices(parsedExpression, progressReporter)
|
|---|
| 587 |
|
|---|
| 588 | def _GetOldestDataset(self, parsedExpression, options, parentAttrValues, dateTimeAttrName):
|
|---|
| 589 | datasets = self._QueryGridSlices(parsedExpression, numResults=1)
|
|---|
| 590 | if len(datasets) > 0:
|
|---|
| 591 | return datasets[0]
|
|---|
| 592 | return None
|
|---|
| 593 |
|
|---|
| 594 | def _GetNewestDataset(self, parsedExpression, options, parentAttrValues, dateTimeAttrName):
|
|---|
| 595 | datasets = self._QueryGridSlices(parsedExpression, numResults=1, reverseOrder=True)
|
|---|
| 596 | if len(datasets) > 0:
|
|---|
| 597 | return datasets[0]
|
|---|
| 598 | return None
|
|---|
| 599 |
|
|---|
| 600 | def _QueryGridSlices(self, parsedExpression, progressReporter=None, numResults=None, reverseOrder=False):
|
|---|
| 601 |
|
|---|
| 602 | attrValues = {}
|
|---|
| 603 | for attr in self._QueryableAttributes:
|
|---|
| 604 | attrValues[attr.Name] = self._Grid.GetQueryableAttributeValue(attr.Name)
|
|---|
| 605 |
|
|---|
| 606 | # Iterate through the slices in the appropriate order, z
|
|---|
| 607 | # changing before t.
|
|---|
| 608 |
|
|---|
| 609 | results = []
|
|---|
| 610 |
|
|---|
| 611 | if self._TQAName is not None:
|
|---|
| 612 | maxT = self._Grid.Shape[0]
|
|---|
| 613 | if reverseOrder:
|
|---|
| 614 | t = maxT - 1
|
|---|
| 615 | else:
|
|---|
| 616 | t = 0
|
|---|
| 617 | else:
|
|---|
| 618 | t = None
|
|---|
| 619 |
|
|---|
| 620 | if self._ZQAName is not None:
|
|---|
| 621 | maxZ = self._Grid.Shape[self._Grid.Dimensions.index('z')]
|
|---|
| 622 | if reverseOrder:
|
|---|
| 623 | z = maxZ - 1
|
|---|
| 624 | else:
|
|---|
| 625 | z = 0
|
|---|
| 626 | else:
|
|---|
| 627 | z = None
|
|---|
| 628 |
|
|---|
| 629 | while (numResults is None or len(results) < numResults) and \
|
|---|
| 630 | (self._TQAName is None or (t >= 0 and t < maxT)) and \
|
|---|
| 631 | (self._ZQAName is None or (z >= 0 and z < maxZ)):
|
|---|
| 632 |
|
|---|
| 633 | # Set the t and/or z queryable attribute values for this
|
|---|
| 634 | # slice.
|
|---|
| 635 |
|
|---|
| 636 | if self._TQAName is not None:
|
|---|
| 637 | if self._TQACoordType == 'min':
|
|---|
| 638 | tCoord = self._Grid.MinCoords['t', t]
|
|---|
| 639 | elif self._TQACoordType == 'center':
|
|---|
| 640 | tCoord = self._Grid.CenterCoords['t', t]
|
|---|
| 641 | else:
|
|---|
| 642 | tCoord = self._Grid.MaxCoords['t', t]
|
|---|
| 643 | attrValues[self._TQAName] = tCoord
|
|---|
| 644 | attrValues['Year'] = tCoord.year
|
|---|
| 645 | attrValues['Month'] = tCoord.month
|
|---|
| 646 | attrValues['Day'] = tCoord.day
|
|---|
| 647 | attrValues['Hour'] = tCoord.hour
|
|---|
| 648 | attrValues['Minute'] = tCoord.minute
|
|---|
| 649 | attrValues['Second'] = tCoord.second
|
|---|
| 650 | attrValues['DayOfYear'] = (datetime.datetime(tCoord.year, tCoord.month, tCoord.day) - datetime.datetime(tCoord.year, 1, 1)).days + 1
|
|---|
| 651 |
|
|---|
| 652 | if self._ZQAName is not None:
|
|---|
| 653 | if self._ZQACoordType == 'min':
|
|---|
| 654 | zCoord = self._Grid.MinCoords['z', z]
|
|---|
| 655 | elif self._ZQACoordType == 'center':
|
|---|
| 656 | zCoord = self._Grid.CenterCoords['z', z]
|
|---|
| 657 | else:
|
|---|
| 658 | zCoord = self._Grid.MaxCoords['z', z]
|
|---|
| 659 | attrValues[self._ZQAName] = zCoord
|
|---|
| 660 |
|
|---|
| 661 | # Evaluate the expression for this slice. The only thing
|
|---|
| 662 | # that will be different about this slice compared to
|
|---|
| 663 | # others is the t and/or z queryable attribute values.
|
|---|
| 664 |
|
|---|
| 665 | if parsedExpression is not None:
|
|---|
| 666 | try:
|
|---|
| 667 | result = parsedExpression.eval(attrValues)
|
|---|
| 668 | except Exception, e:
|
|---|
| 669 | continue # TODO: report better message
|
|---|
| 670 | else:
|
|---|
| 671 | result = True
|
|---|
| 672 |
|
|---|
| 673 | if self._TQAName is not None and self._ZQAName is not None:
|
|---|
| 674 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Query result for t=%(t)s (%(tCoord)s), z=%(z)i (%(zCoord)s): %(result)s'), {u'class': self.__class__.__name__, u'id': id(self), u't': t, u'z': z, u'tCoord': str(tCoord), u'zCoord': repr(zCoord), u'result': repr(result)})
|
|---|
| 675 | elif self._TQAName is not None:
|
|---|
| 676 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Query result for t=%(t)s (%(tCoord)s): %(result)s'), {u'class': self.__class__.__name__, u'id': id(self), u't': t, u'tCoord': str(tCoord), u'result': repr(result)}) # TODO: Re-enable this.
|
|---|
| 677 | else:
|
|---|
| 678 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Query result for z=%(z)i (%(zCoord)s): %(result)s'), {u'class': self.__class__.__name__, u'id': id(self), u'z': z, u'zCoord': repr(zCoord), u'result': repr(result)})
|
|---|
| 679 |
|
|---|
| 680 | if result:
|
|---|
| 681 | results.append(GridSlice(self._Grid, tIndex=t, zIndex=z, tQAName=self._TQAName, tQADisplayName=self._TQADisplayName, tQACoordType=self._TQACoordType, zQAName=self._ZQAName, zQADisplayName=self._ZQADisplayName, zQACoordType=self._ZQACoordType))
|
|---|
| 682 | if progressReporter is not None:
|
|---|
| 683 | progressReporter.ReportProgress()
|
|---|
| 684 |
|
|---|
| 685 | # Go on to the next slice.
|
|---|
| 686 |
|
|---|
| 687 | if self._ZQAName is not None:
|
|---|
| 688 | if reverseOrder:
|
|---|
| 689 | z -= 1
|
|---|
| 690 | else:
|
|---|
| 691 | z += 1
|
|---|
| 692 |
|
|---|
| 693 | if self._TQAName is not None and (self._ZQAName is None or z < 0 or z >= maxZ):
|
|---|
| 694 | if reverseOrder:
|
|---|
| 695 | if self._ZQAName is not None:
|
|---|
| 696 | z = maxZ - 1
|
|---|
| 697 | t -= 1
|
|---|
| 698 | else:
|
|---|
| 699 | if self._ZQAName is not None:
|
|---|
| 700 | z = 0
|
|---|
| 701 | t += 1
|
|---|
| 702 |
|
|---|
| 703 | return results
|
|---|
| 704 |
|
|---|
| 705 |
|
|---|
| 706 | class ClippedGrid(Grid):
|
|---|
| 707 | __doc__ = DynamicDocString()
|
|---|
| 708 |
|
|---|
| 709 | def __init__(self, grid, clipBy=u'Cell indices', xMin=None, xMax=None, yMin=None, yMax=None, zMin=None, zMax=None, tMin=None, tMax=None):
|
|---|
| 710 | self.__class__.__doc__.Obj.ValidateMethodInvocation()
|
|---|
| 711 |
|
|---|
| 712 | # Validate the provided indices and convert them to a list of
|
|---|
| 713 | # slices with positive indices.
|
|---|
| 714 |
|
|---|
| 715 | sliceList = [self._GetSlicesForClippedExtent(grid, 'y', clipBy, yMin, yMax), self._GetSlicesForClippedExtent(grid, 'x', clipBy, xMin, xMax)]
|
|---|
| 716 |
|
|---|
| 717 | if 'z' in grid.Dimensions:
|
|---|
| 718 | sliceList = [self._GetSlicesForClippedExtent(grid, 'z', clipBy, zMin, zMax)] + sliceList
|
|---|
| 719 | elif zMin is not None or zMax is not None:
|
|---|
| 720 | raise ValueError(_(u'Values were provided for zMin and/or zMax but %(dn)s does not have a z dimension.') % {u'dn': grid.DisplayName})
|
|---|
| 721 |
|
|---|
| 722 | if 't' in grid.Dimensions:
|
|---|
| 723 | sliceList = [self._GetSlicesForClippedExtent(grid, 't', clipBy, tMin, tMax)] + sliceList
|
|---|
| 724 | elif tMin is not None or tMax is not None:
|
|---|
| 725 | raise ValueError(_(u'Values were provided for tMin and/or tMax but %(dn)s does not have a t dimension.') % {u'dn': grid.DisplayName})
|
|---|
| 726 |
|
|---|
| 727 | # Initialize our properties.
|
|---|
| 728 |
|
|---|
| 729 | self._Grid = grid
|
|---|
| 730 | self._SliceList = sliceList
|
|---|
| 731 |
|
|---|
| 732 | sliceListForDisplayName = []
|
|---|
| 733 | for i in range(len(grid.Dimensions)):
|
|---|
| 734 | if sliceList[i].start > 0:
|
|---|
| 735 | sliceListForDisplayName.append(_(u'%(dim)sMin = %(index)i') % {u'dim': grid.Dimensions[i], u'index': sliceList[i].start})
|
|---|
| 736 | if sliceList[i].stop < grid.Shape[i]:
|
|---|
| 737 | sliceListForDisplayName.append(_(u'%(dim)sMax = %(index)i') % {u'dim': grid.Dimensions[i], u'index': sliceList[i].stop - 1})
|
|---|
| 738 |
|
|---|
| 739 | if len(sliceListForDisplayName) > 0:
|
|---|
| 740 | self._DisplayName = _(u'%(dn)s, clipped to indices %(indices)s') % {u'dn': self._Grid.DisplayName, u'indices': u', '.join(sliceListForDisplayName)}
|
|---|
| 741 | else:
|
|---|
| 742 | self._DisplayName = self._Grid.DisplayName
|
|---|
| 743 |
|
|---|
| 744 | # Initialize the base class.
|
|---|
| 745 |
|
|---|
| 746 | super(ClippedGrid, self).__init__(self._Grid.ParentCollection, queryableAttributes=self._Grid._QueryableAttributes, queryableAttributeValues=self._Grid._QueryableAttributeValues)
|
|---|
| 747 |
|
|---|
| 748 | # Our goal is to imitate the contained grid except with a
|
|---|
| 749 | # smaller extent. In order to do this, we have to override the
|
|---|
| 750 | # lazy properties for the shape and corner coordinates. This
|
|---|
| 751 | # task is complicated because we have to expose the same
|
|---|
| 752 | # queryable attributes and have the same parent collection,
|
|---|
| 753 | # and those could be used to set the shape and corner
|
|---|
| 754 | # coordinates. Thus we can't just wait to be called at
|
|---|
| 755 | # _GetLazyPropertyPhysicalValue to return the shape and corner
|
|---|
| 756 | # coordinates because if a queryable attribute sets them, we
|
|---|
| 757 | # won't ever be called.
|
|---|
| 758 | #
|
|---|
| 759 | # To work around this, set the shape now (we know it already)
|
|---|
| 760 | # and see if the corner coordinates are available from the
|
|---|
| 761 | # contained grid without accessing physical storage (i.e. they
|
|---|
| 762 | # are already cached by the contained grid or are set by a
|
|---|
| 763 | # queryable attribute of it or its parents). If they are, then
|
|---|
| 764 | # set our modified values now. Otherwise, do nothing; we know
|
|---|
| 765 | # that we'll be called at _GetLazyPropertyPhysicalValue when
|
|---|
| 766 | # they are needed, and we can retrieve and modify the values
|
|---|
| 767 | # from the contained grid at that point.
|
|---|
| 768 |
|
|---|
| 769 | self.SetLazyPropertyValue('Shape', tuple(map(lambda s: s.stop - s.start, sliceList)))
|
|---|
| 770 |
|
|---|
| 771 | oldCornerCoords = grid.GetLazyPropertyValue('CornerCoords', allowPhysicalValue=False)
|
|---|
| 772 | if oldCornerCoords is not None:
|
|---|
| 773 | self.SetLazyPropertyValue('CornerCoords', self._GetNewCornerCoords(oldCornerCoords, grid, sliceList))
|
|---|
| 774 |
|
|---|
| 775 | def _GetDisplayName(self):
|
|---|
| 776 | return self._DisplayName
|
|---|
| 777 |
|
|---|
| 778 | def _GetLazyPropertyPhysicalValue(self, name):
|
|---|
| 779 | if name == 'CornerCoords':
|
|---|
| 780 | return self._GetNewCornerCoords(self._Grid.GetLazyPropertyValue('CornerCoords'), self._Grid, self._SliceList)
|
|---|
| 781 | return self._Grid.GetLazyPropertyValue(name)
|
|---|
| 782 |
|
|---|
| 783 | @classmethod
|
|---|
| 784 | def _GetSlicesForClippedExtent(cls, grid, dim, clipBy, start, stop):
|
|---|
| 785 | dimNum = grid.Dimensions.index(dim)
|
|---|
| 786 |
|
|---|
| 787 | if start is not None:
|
|---|
| 788 | if clipBy == u'cell indices':
|
|---|
| 789 | if start < 0:
|
|---|
| 790 | absStart = grid.Shape[dimNum] + start
|
|---|
| 791 | else:
|
|---|
| 792 | absStart = start
|
|---|
| 793 | if absStart < 0 or absStart > grid.Shape[dimNum] - 1:
|
|---|
| 794 | raise IndexError(_(u'%(dim)sMin (%(value)i) is out of range.') % {u'dim': dim, u'value': start})
|
|---|
| 795 | else:
|
|---|
| 796 | absStart = bisect.bisect_left(grid.CenterCoords[dim], start)
|
|---|
| 797 | if absStart > grid.Shape[dimNum] - 1:
|
|---|
| 798 | raise IndexError(_(u'%(dim)sMin (%(value)s) is out of range. It must be less than or equal to %(max)s, the %(dim)s coordinate of the center of the right-most cell.') % {u'dim': dim, u'value': repr(start), u'max': repr(grid.CenterCoords[dim, -1])})
|
|---|
| 799 | else:
|
|---|
| 800 | absStart = 0
|
|---|
| 801 |
|
|---|
| 802 | if stop is not None:
|
|---|
| 803 | if clipBy == u'cell indices':
|
|---|
| 804 | if stop < 0:
|
|---|
| 805 | absStop = grid.Shape[dimNum] + stop
|
|---|
| 806 | else:
|
|---|
| 807 | absStop = stop
|
|---|
| 808 | if absStop < 0 or absStop > grid.Shape[dimNum]:
|
|---|
| 809 | raise IndexError(_(u'%(dim)sMax (%(value)i) is out of range.') % {u'dim': dim, u'value': stop})
|
|---|
| 810 | else:
|
|---|
| 811 | if start is not None and stop < start:
|
|---|
| 812 | raise IndexError(_(u'%(dim)sMin (%(value1)i) is greater than %(dim)sMax (%(value2)i). %(dim)sMax must be greater than %(dim)sMin.') % {u'dim': dim, u'value1': start, u'value2': stop})
|
|---|
| 813 | absStop = bisect.bisect_right(grid.CenterCoords[dim], stop)
|
|---|
| 814 | if absStop == 0:
|
|---|
| 815 | raise IndexError(_(u'%(dim)sMax (%(value)s) is out of range. It must be greater than or equal to %(min)s, the %(dim)s coordinate of the center of the left-most cell.') % {u'dim': dim, u'value': repr(stop), u'min': repr(grid.CenterCoords[dim, 0])})
|
|---|
| 816 | else:
|
|---|
| 817 | absStop = grid.Shape[dimNum]
|
|---|
| 818 |
|
|---|
| 819 | if absStart == absStop:
|
|---|
| 820 | if clipBy == u'cell indices':
|
|---|
| 821 | raise IndexError(_(u'%(dim)sMin and %(dim)sMax are the same (%(value)i). %(dim)sMax must be greater than %(dim)sMin.') % {u'dim': dim, u'value': start})
|
|---|
| 822 | else:
|
|---|
| 823 | raise IndexError(_(u'%(dim)sMin and %(dim)sMax do not enclose the center of at least one cell. The clipped grid must have at least one cell along the %(dim)s axis. For this to happen, %(dim)sMin and %(dim)sMax must enclose the center of at least one cell.') % {u'dim': dim})
|
|---|
| 824 | elif absStart > absStop:
|
|---|
| 825 | raise IndexError(_(u'%(dim)sMin (%(value1)i) is greater than %(dim)sMax (%(value2)i). %(dim)sMax must be greater than %(dim)sMin.') % {u'dim': dim, u'value1': start, u'value2': stop})
|
|---|
| 826 |
|
|---|
| 827 | return slice(absStart, absStop)
|
|---|
| 828 |
|
|---|
| 829 | @classmethod
|
|---|
| 830 | def _GetNewCornerCoords(cls, oldCornerCoords, grid, sliceList): # TODO: Does this need to be a classmethod?
|
|---|
| 831 | newCornerCoords = list(oldCornerCoords)
|
|---|
| 832 |
|
|---|
| 833 | for i in range(len(newCornerCoords)):
|
|---|
| 834 | if newCornerCoords[i] is not None:
|
|---|
| 835 | if grid.Dimensions[i] != 't':
|
|---|
| 836 | newCornerCoords[i] = grid.CenterCoords[grid.Dimensions[i], sliceList[i].start]
|
|---|
| 837 | else:
|
|---|
| 838 | tCornerCoordType = grid.GetLazyPropertyValue('TCornerCoordType')
|
|---|
| 839 | if tCornerCoordType == 'min':
|
|---|
| 840 | newCornerCoords[i] = grid.MinCoords['t', sliceList[i].start]
|
|---|
| 841 | elif tCornerCoordType == 'center':
|
|---|
| 842 | newCornerCoords[i] = grid.CenterCoords['t', sliceList[i].start]
|
|---|
| 843 | else:
|
|---|
| 844 | newCornerCoords[i] = grid.MaxCoords['t', sliceList[i].start]
|
|---|
| 845 |
|
|---|
| 846 | return tuple(newCornerCoords)
|
|---|
| 847 |
|
|---|
| 848 | def _GetCoordsForOffset(self, key, fixedIncrementOffset):
|
|---|
| 849 |
|
|---|
| 850 | # Validate the key.
|
|---|
| 851 |
|
|---|
| 852 | coord, coordNum, slices, sliceDims = self._GetSlicesForCoordsKey(key)
|
|---|
| 853 |
|
|---|
| 854 | # Adjust the slices list, which could be None, or a list of
|
|---|
| 855 | # slices and/or integers.
|
|---|
| 856 |
|
|---|
| 857 | if slices is not None:
|
|---|
| 858 | for i in range(0, len(sliceDims)):
|
|---|
| 859 | dimNum = self.Dimensions.index(sliceDims[i])
|
|---|
| 860 |
|
|---|
| 861 | if isinstance (slices[i], int):
|
|---|
| 862 | slices[i] = self._AdjustCoord(slices[i], dimNum)
|
|---|
| 863 |
|
|---|
| 864 | elif slices[i].start is None and slices[i].stop is None:
|
|---|
| 865 | if slices[i].step is not None and slices[i].step < 0:
|
|---|
| 866 | if self._SliceList[coordNum].start > 0:
|
|---|
| 867 | slices[i] = slice(self._SliceList[coordNum].stop - 1, self._SliceList[coordNum].start - 1, slices[i].step)
|
|---|
| 868 | else:
|
|---|
| 869 | slices[i] = slice(self._SliceList[coordNum].stop - 1, None, slices[i].step)
|
|---|
| 870 | else:
|
|---|
| 871 | slices[i] = slice(self._SliceList[coordNum].start, self._SliceList[coordNum].stop, slices[i].step)
|
|---|
| 872 |
|
|---|
| 873 | elif slices[i].start is not None and slices[i].stop is None:
|
|---|
| 874 | if slices[i].step is not None and slices[i].step < 0:
|
|---|
| 875 | if self._SliceList[coordNum].start > 0:
|
|---|
| 876 | slices[i] = slice(self._AdjustCoord(slices[i].start, dimNum), self._SliceList[coordNum].start - 1, slices[i].step)
|
|---|
| 877 | else:
|
|---|
| 878 | slices[i] = slice(self._AdjustCoord(slices[i].start, dimNum), None, slices[i].step)
|
|---|
| 879 | else:
|
|---|
| 880 | slices[i] = slice(self._AdjustCoord(slices[i].start, dimNum), self._SliceList[coordNum].stop, slices[i].step)
|
|---|
| 881 |
|
|---|
| 882 | elif slices[i].start is None and slices[i].stop is not None:
|
|---|
| 883 | if slices[i].step is not None and slices[i].step < 0:
|
|---|
| 884 | slices[i] = slice(self._SliceList[coordNum].stop - 1, self._AdjustCoord(slices[i].stop, dimNum), slices[i].step)
|
|---|
| 885 | else:
|
|---|
| 886 | slices[i] = slice(self._SliceList[coordNum].start, self._AdjustCoord(slices[i].stop, dimNum), slices[i].step)
|
|---|
| 887 |
|
|---|
| 888 | else:
|
|---|
| 889 | slices[i] = slice(self._AdjustCoord(slices[i].start, dimNum), self._AdjustCoord(slices[i].stop, dimNum), slices[i].step)
|
|---|
| 890 | else:
|
|---|
| 891 | slices = []
|
|---|
| 892 | for i in range(0, len(sliceDims)):
|
|---|
| 893 | dimNum = self.Dimensions.index(sliceDims[i])
|
|---|
| 894 | slices.append(self._SliceList[dimNum])
|
|---|
| 895 |
|
|---|
| 896 | # Get the coordinates from the contained grid.
|
|---|
| 897 |
|
|---|
| 898 | return self._Grid._GetCoordsForOffset(tuple([coord] + slices), fixedIncrementOffset)
|
|---|
| 899 |
|
|---|
| 900 | def _AdjustCoord(self, c, coordNum):
|
|---|
| 901 | if c >= 0:
|
|---|
| 902 | return c + self._SliceList[coordNum].start
|
|---|
| 903 | return c - (self._Grid.Shape[coordNum] - self._SliceList[coordNum].stop)
|
|---|
| 904 |
|
|---|
| 905 | def _ReadNumpyArray(self, sliceList):
|
|---|
| 906 | return self._Grid._ReadNumpyArray(map(lambda s: slice(s[0].start+s[1].start, s[0].stop+s[1].start, s[0].step), zip(sliceList, self._SliceList)))
|
|---|
| 907 |
|
|---|
| 908 | def _WriteNumpyArray(self, sliceList, data):
|
|---|
| 909 | self._Grid._WriteNumpyArray(map(lambda s: slice(s[0].start+s[1].start, s[0].stop+s[1].start, s[0].step), zip(sliceList, self._SliceList)), data)
|
|---|
| 910 |
|
|---|
| 911 |
|
|---|
| 912 | class MaskedGrid(Grid):
|
|---|
| 913 | __doc__ = DynamicDocString()
|
|---|
| 914 |
|
|---|
| 915 | def __init__(self, grid, masks, operators, values, unscaledNoDataValue=None, scaledNoDataValue=None):
|
|---|
| 916 | # TODO: self.__class__.__doc__.Obj.ValidateMethodInvocation()
|
|---|
| 917 |
|
|---|
| 918 | # Initialize our properties.
|
|---|
| 919 |
|
|---|
| 920 | if len(values) < len(operators):
|
|---|
| 921 | values = values + [None] * (len(operators) - len(values))
|
|---|
| 922 |
|
|---|
| 923 | self._Grid = grid
|
|---|
| 924 | self._Masks = masks
|
|---|
| 925 | self._Operators = operators
|
|---|
| 926 | self._Values = values
|
|---|
| 927 | self._UnscaledNoDataValue = unscaledNoDataValue
|
|---|
| 928 | self._ScaledNoDataValue = scaledNoDataValue
|
|---|
| 929 | self._MaskOffsets = None
|
|---|
| 930 |
|
|---|
| 931 | self._DisplayName = _(u'%(dn)s, masked where %(maskExpressions)s') % {u'dn': grid.DisplayName, u'maskExpressions': _(u' or ').join(map(lambda s: self._GetMaskDisplayExpression(*s), zip(masks, operators, values)))}
|
|---|
| 932 |
|
|---|
| 933 | # Initialize the base class.
|
|---|
| 934 |
|
|---|
| 935 | super(MaskedGrid, self).__init__(self._Grid.ParentCollection, queryableAttributes=self._Grid._QueryableAttributes, queryableAttributeValues=self._Grid._QueryableAttributeValues)
|
|---|
| 936 |
|
|---|
| 937 | # Our goal is to imitate the contained grid except with ideal
|
|---|
| 938 | # values for PhysicalDimensions or PhysicalDimensionsFlipped
|
|---|
| 939 | # (the contained grid takes care of reordering, if needed) and
|
|---|
| 940 | # with an UnscaledNoDataValue and ScaledNoDataValue (if the
|
|---|
| 941 | # contained grid does not have any). This task is complicated
|
|---|
| 942 | # because we have to expose the same queryable attributes and
|
|---|
| 943 | # have the same parent collection, and those could be used to
|
|---|
| 944 | # set those lazy properties. Thus we can't just wait to be
|
|---|
| 945 | # called at _GetLazyPropertyPhysicalValue to return the values
|
|---|
| 946 | # because if a queryable attribute sets them, we won't ever be
|
|---|
| 947 | # called.
|
|---|
| 948 | #
|
|---|
| 949 | # To work around this, see if values are available without
|
|---|
| 950 | # accessing physical storage (i.e. they are set by a queryable
|
|---|
| 951 | # attribute). If they are, then set our modified values now.
|
|---|
| 952 | # Otherwise, do nothing; we know that we'll be called at
|
|---|
| 953 | # _GetLazyPropertyPhysicalValue when they are needed.
|
|---|
| 954 |
|
|---|
| 955 | if self.HasLazyPropertyValue('PhysicalDimensions', allowPhysicalValue=False):
|
|---|
| 956 | self.SetLazyPropertyValue('PhysicalDimensions', self._GetLazyPropertyPhysicalValue('PhysicalDimensions'))
|
|---|
| 957 |
|
|---|
| 958 | if self.HasLazyPropertyValue('PhysicalDimensionsFlipped', allowPhysicalValue=False):
|
|---|
| 959 | self.SetLazyPropertyValue('PhysicalDimensionsFlipped', self._GetLazyPropertyPhysicalValue('PhysicalDimensionsFlipped'))
|
|---|
| 960 |
|
|---|
| 961 | if self.HasLazyPropertyValue('UnscaledNoDataValue', allowPhysicalValue=False):
|
|---|
| 962 | self.SetLazyPropertyValue('UnscaledNoDataValue', self._GetLazyPropertyPhysicalValue('UnscaledNoDataValue'))
|
|---|
| 963 |
|
|---|
| 964 | if self.HasLazyPropertyValue('ScaledNoDataValue', allowPhysicalValue=False):
|
|---|
| 965 | self.SetLazyPropertyValue('ScaledNoDataValue', self._GetLazyPropertyPhysicalValue('ScaledNoDataValue'))
|
|---|
| 966 |
|
|---|
| 967 | def _GetDisplayName(self):
|
|---|
| 968 | return self._DisplayName
|
|---|
| 969 |
|
|---|
| 970 | def _GetMaskDisplayExpression(self, mask, op, value):
|
|---|
| 971 | if op in [u'=', u'==', u'!=', u'<>', u'<', u'<=', u'>', u'>']:
|
|---|
| 972 | return mask.DisplayName + u' ' + op + u' ' + repr(value)
|
|---|
| 973 | return mask.DisplayName + u' ' + op
|
|---|
| 974 |
|
|---|
| 975 | def _GetLazyPropertyPhysicalValue(self, name):
|
|---|
| 976 |
|
|---|
| 977 | # If the requested property is PhysicalDimensions or
|
|---|
| 978 | # PhysicalDimensionsFlipped, return values indicating the
|
|---|
| 979 | # dimensions in the ideal order. The contained grid takes care
|
|---|
| 980 | # of reordering, if needed.
|
|---|
| 981 |
|
|---|
| 982 | if name == 'PhysicalDimensions':
|
|---|
| 983 | return self.Dimensions
|
|---|
| 984 |
|
|---|
| 985 | if name == 'PhysicalDimensionsFlipped':
|
|---|
| 986 | return tuple([False] * len(self.Dimensions))
|
|---|
| 987 |
|
|---|
| 988 | # If the requested property is the UnscaledNoDataValue or
|
|---|
| 989 | # ScaledNoDataValue, determine the value we should return.
|
|---|
| 990 |
|
|---|
| 991 | if name == 'UnscaledNoDataValue':
|
|---|
| 992 | value = self._Grid.GetLazyPropertyValue(name)
|
|---|
| 993 | if value is not None:
|
|---|
| 994 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Using the UnscaledNoDataValue of the contained grid (%(value)s).'), {u'class': self.__class__.__name__, u'id': id(self), u'value': repr(value)})
|
|---|
| 995 | return value
|
|---|
| 996 |
|
|---|
| 997 | if self._UnscaledNoDataValue is not None:
|
|---|
| 998 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Using the UnscaledNoDataValue supplied to the MaskedGrid constructor (%(value)s).'), {u'class': self.__class__.__name__, u'id': id(self), u'value': repr(self._UnscaledNoDataValue)})
|
|---|
| 999 | return self._UnscaledNoDataValue
|
|---|
| 1000 |
|
|---|
| 1001 | value = self._GetNoDataValueForDataType(self.UnscaledDataType)
|
|---|
| 1002 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Using the default UnscaledNoDataValue (%(value)s) for UnscaledDataType %(dt)s. The contained grid does not have an UnscaledNoDataValue, nor was one supplied to the MaskedGrid constructor.'), {u'class': self.__class__.__name__, u'id': id(self), u'value': repr(value), u'dt': self.UnscaledDataType})
|
|---|
| 1003 | return value
|
|---|
| 1004 |
|
|---|
| 1005 | if name == 'ScaledNoDataValue':
|
|---|
| 1006 | value = self._Grid.GetLazyPropertyValue(name)
|
|---|
| 1007 | if value is not None:
|
|---|
| 1008 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Using the ScaledNoDataValue of the contained grid (%(value)s).'), {u'class': self.__class__.__name__, u'id': id(self), u'value': repr(value)})
|
|---|
| 1009 | return value
|
|---|
| 1010 |
|
|---|
| 1011 | if self._ScaledNoDataValue is not None:
|
|---|
| 1012 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Using the ScaledNoDataValue supplied to the MaskedGrid constructor (%(value)s).'), {u'class': self.__class__.__name__, u'id': id(self), u'value': repr(self._ScaledNoDataValue)})
|
|---|
| 1013 | return self._ScaledNoDataValue
|
|---|
| 1014 |
|
|---|
| 1015 | value = self._GetNoDataValueForDataType(self.DataType)
|
|---|
| 1016 | if value is not None:
|
|---|
| 1017 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Using the default ScaledNoDataValue (%(value)s) for DataType %(dt)s. The contained grid does not have an ScaledNoDataValue, nor was one supplied to the MaskedGrid constructor.'), {u'class': self.__class__.__name__, u'id': id(self), u'value': repr(value), u'dt': self.DataType})
|
|---|
| 1018 | return value
|
|---|
| 1019 |
|
|---|
| 1020 | # Otherwise just get the unaltered value from the contained
|
|---|
| 1021 | # grid.
|
|---|
| 1022 |
|
|---|
| 1023 | return self._Grid.GetLazyPropertyValue(name)
|
|---|
| 1024 |
|
|---|
| 1025 | @classmethod
|
|---|
| 1026 | def _GetNoDataValueForDataType(cls, dataType):
|
|---|
| 1027 | if dataType == u'int8':
|
|---|
| 1028 | return -128
|
|---|
| 1029 | if dataType == u'uint8':
|
|---|
| 1030 | return 255
|
|---|
| 1031 | if dataType == u'int16':
|
|---|
| 1032 | return -32768
|
|---|
| 1033 | if dataType == u'uint16':
|
|---|
| 1034 | return 65535
|
|---|
| 1035 | if dataType == u'int32':
|
|---|
| 1036 | return -2147483647 # To improve compatibility with ArcGIS, we use -2147483647 rather than -2147483648
|
|---|
| 1037 | if dataType == u'uint32':
|
|---|
| 1038 | return 4294967295L
|
|---|
| 1039 | if dataType == u'float32':
|
|---|
| 1040 | return -3.4028234663852886e+038 # This is the float64 representation of the float32 -3.40282347e+38
|
|---|
| 1041 | if dataType == u'float64':
|
|---|
| 1042 | return -1.7976931348623157e+308
|
|---|
| 1043 | return None
|
|---|
| 1044 |
|
|---|
| 1045 | ## def _GetUnscaledDataAsArray(self, key):
|
|---|
| 1046 | ## return self._GetDataAsArrayAndApplyMasks(key, self._Grid._GetUnscaledDataAsArray, self.UnscaledNoDataValue)
|
|---|
| 1047 | ##
|
|---|
| 1048 | ## def _GetScaledDataAsArray(self, key):
|
|---|
| 1049 | ## return self._GetDataAsArrayAndApplyMasks(key, self._Grid._GetScaledDataAsArray, self.ScaledNoDataValue)
|
|---|
| 1050 |
|
|---|
| 1051 | def _ReadNumpyArray(self, sliceList):
|
|---|
| 1052 |
|
|---|
| 1053 | # If we have not yet validated the masks and computed the
|
|---|
| 1054 | # offsets into them, do it now.
|
|---|
| 1055 |
|
|---|
| 1056 | if self._MaskOffsets is None:
|
|---|
| 1057 | self._ValidateMasksAndSetOffsets()
|
|---|
| 1058 |
|
|---|
| 1059 | # Get the unscaled data from the contained grid.
|
|---|
| 1060 |
|
|---|
| 1061 | data = self._Grid.UnscaledData.__getitem__(tuple(sliceList))
|
|---|
| 1062 |
|
|---|
| 1063 | # Apply each of the masks.
|
|---|
| 1064 |
|
|---|
| 1065 | for i in range(len(self._Masks)):
|
|---|
| 1066 | if self._Operators[i] in [u'=', u'==', u'!=', u'<>', u'<', u'<=', u'>', u'>']:
|
|---|
| 1067 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Masking %(dn2)s where %(dn1)s %(op)s %(value)s.'), {u'class': self.__class__.__name__, u'id': id(self), u'dn2': self._Grid.DisplayName, u'dn1': self._Masks[i].DisplayName, u'op': self._Operators[i], u'value': self._Values[i]})
|
|---|
| 1068 | else:
|
|---|
| 1069 | self._LogDebug(_(u'%(class)s 0x%(id)08X: Masking %(dn2)s where %(dn1)s %(op)s.'), {u'class': self.__class__.__name__, u'id': id(self), u'dn2': self._Grid.DisplayName, u'dn1': self._Masks[i].DisplayName, u'op': self._Operators[i]})
|
|---|
| 1070 |
|
|---|
| 1071 | # Create a list of slices for retrieving the mask.
|
|---|
| 1072 |
|
|---|
| 1073 | maskSliceList = []
|
|---|
| 1074 | for d in range(len(self.Dimensions)):
|
|---|
| 1075 | if self.Dimensions[d] in self._Masks[i].Dimensions:
|
|---|
| 1076 | offset = self._MaskOffsets[i][self._Masks[i].Dimensions.index(self.Dimensions[d])]
|
|---|
| 1077 | maskSliceList.append(slice(sliceList[d].start + offset, sliceList[d].stop + offset))
|
|---|
| 1078 |
|
|---|
| 1079 | # Get the mask.
|
|---|
| 1080 |
|
|---|
| 1081 | maskData = self._Masks[i].Data.__getitem__(tuple(maskSliceList))
|
|---|
| 1082 |
|
|---|
| 1083 | # Perform the requested test on the mask.
|
|---|
| 1084 |
|
|---|
| 1085 | if self._Operators[i] in [u'=', u'==']:
|
|---|
| 1086 | maskResult = maskData == self._Values[i]
|
|---|
| 1087 | elif self._Operators[i] in [u'!=', u'<>']:
|
|---|
| 1088 | maskResult = maskData != self._Values[i]
|
|---|
| 1089 | elif self._Operators[i] == u'<':
|
|---|
| 1090 | maskResult = maskData < self._Values[i]
|
|---|
| 1091 | elif self._Operators[i] == u'<=':
|
|---|
| 1092 | maskResult = maskData <= self._Values[i]
|
|---|
| 1093 | elif self._Operators[i] == u'>':
|
|---|
| 1094 | maskResult = maskData > self._Values[i]
|
|---|
| 1095 | elif self._Operators[i] == u'>=':
|
|---|
| 1096 | maskResult = maskData >= self._Values[i]
|
|---|
| 1097 | else:
|
|---|
| 1098 | raise ValueError(_(u'Unknown mask operator "%(op)s".') % {u'op': self._Operators[i]})
|
|---|
| 1099 |
|
|---|
| 1100 | # Set all cells where the test is True to
|
|---|
| 1101 | # self.UnscaledNoDataValue. Handle the cases where the
|
|---|
| 1102 | # mask has fewer dimensions than the data (for example,
|
|---|
| 1103 | # when a land mask with dimensions yx is used to mask a
|
|---|
| 1104 | # time series of satellite images with dimensions tyx).
|
|---|
| 1105 |
|
|---|
| 1106 | if self.Dimensions == self._Masks[i].Dimensions:
|
|---|
| 1107 | data[maskResult] = self.UnscaledNoDataValue
|
|---|
| 1108 | elif self.Dimensions in [u'zyx', u'tyx'] and self._Masks[i].Dimensions == u'yx' or self.Dimensions == u'tzyx' and self._Masks[i].Dimensions == u'zyx':
|
|---|
| 1109 | data[:, maskResult] = self.UnscaledNoDataValue
|
|---|
| 1110 | elif self.Dimensions == u'tzyx' and self._Masks[i].Dimensions == u'tyx':
|
|---|
| 1111 | data.transpose((1,0,2,3))[:, maskResult] = self.UnscaledNoDataValue
|
|---|
| 1112 | else: # self.Dimensions must be u'tzyx' and self._Masks[i].Dimensions must be u'yx'
|
|---|
| 1113 | data[:, :, maskResult] = self.UnscaledNoDataValue
|
|---|
| 1114 |
|
|---|
| 1115 | # Return the data and self.UnscaledNoDataValue.
|
|---|
| 1116 |
|
|---|
| 1117 | return data, self.UnscaledNoDataValue
|
|---|
| 1118 |
|
|---|
| 1119 | ## def _GetDataAsArrayAndApplyMasks(self, key, getDataFunc, noDataValue):
|
|---|
| 1120 | ##
|
|---|
| 1121 | ## # If we have not yet validated the masks and computed the
|
|---|
| 1122 | ## # offsets into them, do it now.
|
|---|
| 1123 | ##
|
|---|
| 1124 | ## if self._MaskOffsets is None:
|
|---|
| 1125 | ## self._ValidateMasksAndSetOffsets()
|
|---|
| 1126 | ##
|
|---|
| 1127 | ## # Validate the key. Although this function will also flip the
|
|---|
| 1128 | ## # key when necessary, it will never be necessary for us
|
|---|
| 1129 | ## # because all elements of PhysicalDimensionsFlipped are False;
|
|---|
| 1130 | ## # the contained grid and the masks handle their own flipping.
|
|---|
| 1131 | ##
|
|---|
| 1132 | ## flippedKey = self._ValidateAndFlipKey(key)
|
|---|
| 1133 | ##
|
|---|
| 1134 | ## # Convert the flipped key to a list of slices for every
|
|---|
| 1135 | ## # dimension, with positive start and stop attributes, start <=
|
|---|
| 1136 | ## # stop, and step == None.
|
|---|
| 1137 | ##
|
|---|
| 1138 | ## sliceList = self._GetSlicesForFlippedKey(flippedKey)
|
|---|
| 1139 | ##
|
|---|
| 1140 | ## # Get the data from the contained grid using the slice list
|
|---|
| 1141 | ## # rather than the original key. This will ensure that the
|
|---|
| 1142 | ## # returned data has all of the axes. Later, we will apply the
|
|---|
| 1143 | ## # flippedKey to it to obtain the array to return; this will
|
|---|
| 1144 | ## # reduce the number of axes, if the key contains integer
|
|---|
| 1145 | ## # indices.
|
|---|
| 1146 | ##
|
|---|
| 1147 | ## data = getDataFunc(tuple(sliceList))
|
|---|
| 1148 | ##
|
|---|
| 1149 | ## # Apply each of the masks.
|
|---|
| 1150 | ##
|
|---|
| 1151 | ## for i in range(len(self._Masks)):
|
|---|
| 1152 | ## if self._Operators[i] in [u'=', u'==', u'!=', u'<>', u'<', u'<=', u'>', u'>']:
|
|---|
| 1153 | ## self._LogDebug(_(u'%(class)s 0x%(id)08X: Masking %(dn2)s where %(dn1)s %(op)s %(value)s.'), {u'class': self.__class__.__name__, u'id': id(self), u'dn2': self._Grid.DisplayName, u'dn1': self._Masks[i].DisplayName, u'op': self._Operators[i], u'value': self._Values[i]})
|
|---|
| 1154 | ## else:
|
|---|
| 1155 | ## self._LogDebug(_(u'%(class)s 0x%(id)08X: Masking %(dn2)s where %(dn1)s %(op)s.'), {u'class': self.__class__.__name__, u'id': id(self), u'dn2': self._Grid.DisplayName, u'dn1': self._Masks[i].DisplayName, u'op': self._Operators[i]})
|
|---|
| 1156 | ##
|
|---|
| 1157 | ## # Create a list of slices for retrieving the mask.
|
|---|
| 1158 | ##
|
|---|
| 1159 | ## maskSliceList = []
|
|---|
| 1160 | ## for d in range(len(self.Dimensions)):
|
|---|
| 1161 | ## if self.Dimensions[d] in self._Masks[i].Dimensions:
|
|---|
| 1162 | ## offset = self._MaskOffsets[i][self._Masks[i].Dimensions.index(self.Dimensions[d])]
|
|---|
| 1163 | ## maskSliceList.append(slice(sliceList[d].start + offset, sliceList[d].stop + offset))
|
|---|
| 1164 | ##
|
|---|
| 1165 | ## # Get the mask.
|
|---|
| 1166 | ##
|
|---|
| 1167 | ## maskData = self._Masks[i].Data.__getitem__(tuple(maskSliceList))
|
|---|
| 1168 | ##
|
|---|
| 1169 | ## # Perform the requested test on the mask.
|
|---|
| 1170 | ##
|
|---|
| 1171 | ## if self._Operators[i] in [u'=', u'==']:
|
|---|
| 1172 | ## maskResult = maskData == self._Values[i]
|
|---|
| 1173 | ## elif self._Operators[i] in [u'!=', u'<>']:
|
|---|
| 1174 | ## maskResult = maskData != self._Values[i]
|
|---|
| 1175 | ## elif self._Operators[i] == u'<':
|
|---|
| 1176 | ## maskResult = maskData < self._Values[i]
|
|---|
| 1177 | ## elif self._Operators[i] == u'<=':
|
|---|
| 1178 | ## maskResult = maskData <= self._Values[i]
|
|---|
| 1179 | ## elif self._Operators[i] == u'>':
|
|---|
| 1180 | ## maskResult = maskData > self._Values[i]
|
|---|
| 1181 | ## elif self._Operators[i] == u'>=':
|
|---|
| 1182 | ## maskResult = maskData >= self._Values[i]
|
|---|
| 1183 | ## else:
|
|---|
| 1184 | ## raise ValueError(_(u'Unknown mask operator "%(op)s".') % {u'op': self._Operators[i]})
|
|---|
| 1185 | ##
|
|---|
| 1186 | ## # Set all cells where the test is True to noDataValue.
|
|---|
| 1187 | ## # Handle the cases where the mask has fewer dimensions
|
|---|
| 1188 | ## # than the data (for example, when a land mask with
|
|---|
| 1189 | ## # dimensions yx is used to mask a time series of satellite
|
|---|
| 1190 | ## # images with dimensions tyx).
|
|---|
| 1191 | ##
|
|---|
| 1192 | ## if self.Dimensions == self._Masks[i].Dimensions:
|
|---|
| 1193 | ## data[maskResult] = noDataValue
|
|---|
| 1194 | ## elif self.Dimensions in [u'zyx', u'tyx'] and self._Masks[i].Dimensions == u'yx' or self.Dimensions == u'tzyx' and self._Masks[i].Dimensions == u'zyx':
|
|---|
| 1195 | ## data[:, maskResult] = noDataValue
|
|---|
| 1196 | ## elif self.Dimensions == u'tzyx' and self._Masks[i].Dimensions == u'tyx':
|
|---|
| 1197 | ## data.transpose((1,0,2,3))[:, maskResult] = noDataValue
|
|---|
| 1198 | ## else: # self.Dimensions must be u'tzyx' and self._Masks[i].Dimensions must be u'yx'
|
|---|
| 1199 | ## data[:, :, maskResult] = noDataValue
|
|---|
| 1200 | ##
|
|---|
| 1201 | ## # The data has the shape described by the key but we cannot
|
|---|
| 1202 | ## # use the key to index into it because the key refers to an
|
|---|
| 1203 | ## # array with the full shape, not this reduced shape. Adjust
|
|---|
| 1204 | ## # the key indices to the reduced shape.
|
|---|
| 1205 | ##
|
|---|
| 1206 | ## flippedKey = self._AdjustFlippedKeyIndicesToReducedShape(flippedKey)
|
|---|
| 1207 | ##
|
|---|
| 1208 | ## # Return the data.
|
|---|
| 1209 | ##
|
|---|
| 1210 | ## if len(flippedKey) == 1:
|
|---|
| 1211 | ## return data.__getitem__(flippedKey[0])
|
|---|
| 1212 | ##
|
|---|
| 1213 | ## return data.__getitem__(tuple(flippedKey))
|
|---|
| 1214 |
|
|---|
| 1215 | def _ValidateMasksAndSetOffsets(self):
|
|---|
| 1216 | maskOffsets = []
|
|---|
| 1217 |
|
|---|
| 1218 | for mask in self._Masks:
|
|---|
| 1219 |
|
|---|
| 1220 | # Validate that the mask dimensions are a subset of the
|
|---|
| 1221 | # contained grid's dimensions.
|
|---|
| 1222 |
|
|---|
| 1223 | if len(self.Dimensions) < len(mask.Dimensions) or not (mask.Dimensions in [u'yx', u'tzyx'] or mask.Dimensions == u'zyx' and self.Dimensions in [u'zyx', 'tzyx'] or mask.Dimensions == u'tyx' and self.Dimensions in [u'tyx', 'tzyx']):
|
|---|
| 1224 | raise ValueError(_(u'%(dn1)s has dimensions (%(dim1)s) that are incompatible with the dimensions of %(dn2)s (%(dim2)s), so it cannot be used as a mask.') % {u'dn1': mask.DisplayName, u'dn2': self._Grid.DisplayName, u'dim1': mask.Dimensions, u'dim2': self._Grid.Dimensions})
|
|---|
| 1225 |
|
|---|
| 1226 | # Validate that the mask uses the same coordinate system
|
|---|
| 1227 | # as the contained grid.
|
|---|
| 1228 |
|
|---|
| 1229 | if not self.GetSpatialReference('obj').IsSame(mask.GetSpatialReference('obj')):
|
|---|
| 1230 | raise ValueError(_(u'%(dn1)s has a different coordinate system than %(dn2)s, so it cannot be used as a mask.') % {u'dn1': mask.DisplayName, u'dn2': self._Grid.DisplayName})
|
|---|
| 1231 |
|
|---|
| 1232 | # If the contained grid has any dimensions for which the
|
|---|
| 1233 | # coordinates depend on the coordinates of other
|
|---|
| 1234 | # dimensions (e.g. the value of z depends on t, y, and x,
|
|---|
| 1235 | # as is the case with certain ROMS datasets), then we
|
|---|
| 1236 | # require that the shape of the mask exactly match that of
|
|---|
| 1237 | # the contained grid (for the mask's dimensions). We do
|
|---|
| 1238 | # not verify the coordinates of the mask as this could be
|
|---|
| 1239 | # a very time consuming operation.
|
|---|
| 1240 |
|
|---|
| 1241 | if self.CoordDependencies != tuple([None] * len(self.Dimensions)):
|
|---|
| 1242 | for i in range(len(mask.Dimensions)):
|
|---|
| 1243 | if mask.Shape[i] != self.Shape[self.Dimensions.index[mask.Dimensions[i]]]:
|
|---|
| 1244 | raise ValueError(_(u'Because %(dn2)s has dimensions for which the coordinates depend on other dimensions, the MaskedGrid class can only apply masks that have dimensions with the same length as it. %(dn1)s cannot be used as a mask because the %(dim)s dimension has a different length (the mask has length %(len1)i but the grid has length %(len2)i).') % {u'dn1': mask.DisplayName, u'dn2': self._Grid.DisplayName, u'dim': mask.Dimensions[i], u'len1': mask.Shape[i], u'len2': self.Shape[self.Dimensions.index[mask.Dimensions[i]]]})
|
|---|
| 1245 |
|
|---|
| 1246 | maskOffsets.append([0] * len(mask.Dimensions))
|
|---|
| 1247 |
|
|---|
| 1248 | # Otherwise (there are no coordinate dependencies), verify
|
|---|
| 1249 | # that the mask encloses the grid and has the same
|
|---|
| 1250 | # coordinates.
|
|---|
| 1251 |
|
|---|
| 1252 | else:
|
|---|
| 1253 | offsets = []
|
|---|
| 1254 |
|
|---|
| 1255 | for i in range(len(mask.Dimensions)):
|
|---|
| 1256 | gridCoords = self.CenterCoords[mask.Dimensions[i]]
|
|---|
| 1257 | maskCoords = mask.CenterCoords[mask.Dimensions[i]]
|
|---|
| 1258 |
|
|---|
| 1259 | if gridCoords[0] < maskCoords[0] or gridCoords[-1] > maskCoords[-1]:
|
|---|
| 1260 | raise ValueError(_(u'%(dn1)s does not completely enclose %(dn2)s so it cannot be used as a mask.') % {u'dn1': mask.DisplayName, u'dn2': self._Grid.DisplayName})
|
|---|
| 1261 |
|
|---|
| 1262 | try:
|
|---|
| 1263 | offset = maskCoords.searchsorted(gridCoords[0])
|
|---|
| 1264 | if len(maskCoords) - offset < len(gridCoords):
|
|---|
| 1265 | raise ValueError
|
|---|
| 1266 | if (maskCoords[offset:offset+len(gridCoords)] != gridCoords).any():
|
|---|
| 1267 | raise ValueError
|
|---|
| 1268 | except:
|
|---|
| 1269 | raise ValueError(_(u'The %(dim)s coordinates of %(dn1)s do not line up with the %(dim)s coordinates of %(dn2)s, so it cannot be used as a mask.') % {u'dn1': mask.DisplayName, u'dn2': self._Grid.DisplayName, u'dim': mask.Dimensions[i]})
|
|---|
| 1270 |
|
|---|
| 1271 | offsets.append(offset)
|
|---|
| 1272 |
|
|---|
| 1273 | maskOffsets.append(offsets)
|
|---|
| 1274 |
|
|---|
| 1275 | self._MaskOffsets = maskOffsets
|
|---|
| 1276 |
|
|---|
| 1277 |
|
|---|
| 1278 | class RotatedGlobalGrid(Grid):
|
|---|
| 1279 | __doc__ = DynamicDocString()
|
|---|
| 1280 |
|
|---|
| 1281 | def __init__(self, grid, rotationOffset, rotationUnits=u'Map units'):
|
|---|
| 1282 | self.__class__.__doc__.Obj.ValidateMethodInvocation()
|
|---|
| 1283 |
|
|---|
| 1284 | # TODO: Validate that the grid is global?
|
|---|
| 1285 |
|
|---|
| 1286 | # Validate that the grid has a constant x increment.
|
|---|
| 1287 |
|
|---|
| 1288 | if grid.CoordDependencies[-1] is not None:
|
|---|
| 1289 | raise ValueError(_(u'The provided grid, %(dn)s, does not have a constant x increment. The current implementation of RotatedGlobalGrid only supports grids with constant x increments.') % {u'dn': grid.DisplayName})
|
|---|
| 1290 |
|
|---|
| 1291 | # Initialize our properties.
|
|---|
| 1292 |
|
|---|
| 1293 | self._Grid = grid
|
|---|
| 1294 | self._XRotationType = rotationUnits
|
|---|
| 1295 |
|
|---|
| 1296 | if self._XRotationType == u'cells':
|
|---|
| 1297 | self._XRotationInMapUnits = None
|
|---|
| 1298 | self._XRotationInCells = int(round(rotationOffset))
|
|---|
| 1299 | self._DisplayName = _(u'%(dn)s, rotated about the planetary axis by %(cells)i cells') % {u'dn': self._Grid.DisplayName, u'cells': self._XRotationInCells}
|
|---|
| 1300 | else:
|
|---|
| 1301 | self._XRotationInMapUnits = rotationOffset
|
|---|
| 1302 | self._XRotationInCells = None
|
|---|
| 1303 | self._DisplayName = _(u'%(dn)s, rotated about the planetary axis by %(mapUnits)g map units') % {u'dn': self._Grid.DisplayName, u'mapUnits': self._XRotationInMapUnits}
|
|---|
| 1304 |
|
|---|
| 1305 | # Initialize the base class.
|
|---|
| 1306 |
|
|---|
| 1307 | super(RotatedGlobalGrid, self).__init__(self._Grid.ParentCollection, queryableAttributes=self._Grid._QueryableAttributes, queryableAttributeValues=self._Grid._QueryableAttributeValues)
|
|---|
| 1308 |
|
|---|
| 1309 | # Our goal is to imitate the contained grid except with
|
|---|
| 1310 | # different x coordinates. In order to do this, we have to
|
|---|
| 1311 | # override the lazy property for the corner coordinates. This
|
|---|
| 1312 | # task is complicated because we have to expose the same
|
|---|
| 1313 | # queryable attributes and have the same parent collection,
|
|---|
| 1314 | # and those could be used to set the corner coordinates. Thus
|
|---|
| 1315 | # we can't just wait to be called at
|
|---|
| 1316 | # _GetLazyPropertyPhysicalValue to return the corner
|
|---|
| 1317 | # coordinates because if a queryable attribute sets them, we
|
|---|
| 1318 | # won't ever be called.
|
|---|
| 1319 | #
|
|---|
| 1320 | # To work around this, see if the corner coordinates are
|
|---|
| 1321 | # available from the contained grid without accessing physical
|
|---|
| 1322 | # storage (i.e. they are already cached by the contained grid
|
|---|
| 1323 | # or are set by a queryable attribute of it or its parents).
|
|---|
| 1324 | # If they are, then set our modified values now. Otherwise, do
|
|---|
| 1325 | # nothing; we know that we'll be called at
|
|---|
| 1326 | # _GetLazyPropertyPhysicalValue when they are needed, and we
|
|---|
| 1327 | # can retrieve and modify the values from the contained grid
|
|---|
| 1328 | # at that point.
|
|---|
| 1329 |
|
|---|
| 1330 | oldCornerCoords = grid.GetLazyPropertyValue('CornerCoords', allowPhysicalValue=False)
|
|---|
| 1331 | if oldCornerCoords is not None:
|
|---|
| 1332 | self.SetLazyPropertyValue('CornerCoords', self._GetNewCornerCoords(oldCornerCoords))
|
|---|
| 1333 |
|
|---|
| 1334 | def _GetDisplayName(self):
|
|---|
| 1335 | return self._DisplayName
|
|---|
| 1336 |
|
|---|
| 1337 | def _GetLazyPropertyPhysicalValue(self, name):
|
|---|
| 1338 | if name == 'CornerCoords':
|
|---|
| 1339 | return self._GetNewCornerCoords(self._Grid.GetLazyPropertyValue('CornerCoords'))
|
|---|
| 1340 | return self._Grid.GetLazyPropertyValue(name)
|
|---|
| 1341 |
|
|---|
| 1342 | def _GetNewCornerCoords(self, oldCornerCoords):
|
|---|
| 1343 |
|
|---|
| 1344 | # Calculate the new x corner coordinate.
|
|---|
| 1345 |
|
|---|
| 1346 | if self._XRotationType == u'cells':
|
|---|
| 1347 | self._XRotationInMapUnits = self._XRotationInCells * self._Grid.CoordIncrements[-1]
|
|---|
| 1348 | else:
|
|---|
| 1349 | self._XRotationInCells = int(round(self._XRotationInMapUnits / self._Grid.CoordIncrements[-1]))
|
|---|
| 1350 |
|
|---|
| 1351 | newCornerCoords = list(oldCornerCoords[:-1]) + [oldCornerCoords[-1] + self._XRotationInMapUnits]
|
|---|
| 1352 |
|
|---|
| 1353 | # Set the display name to show both the numbers of cells and
|
|---|
| 1354 | # of map units.
|
|---|
| 1355 |
|
|---|
| 1356 | self._DisplayName = _(u'%(dn)s, rotated about the planetary axis by %(cells)i cells (%(mapUnits)g map units)') % {u'dn': self._Grid.DisplayName, u'cells': self._XRotationInCells, u'mapUnits': self._XRotationInMapUnits}
|
|---|
| 1357 |
|
|---|
| 1358 | # Return the new corner coordinates.
|
|---|
| 1359 |
|
|---|
| 1360 | return tuple(newCornerCoords)
|
|---|
| 1361 |
|
|---|
| 1362 | def _ReadNumpyArray(self, sliceList):
|
|---|
| 1363 |
|
|---|
| 1364 | # If we have not calculated the number of cells of rotation,
|
|---|
| 1365 | # retrieve the CornerCoordinates lazy property to force the
|
|---|
| 1366 | # calculation.
|
|---|
| 1367 |
|
|---|
| 1368 | if self._XRotationInCells is None:
|
|---|
| 1369 | cornerCoords = self.GetLazyPropertyValue('CornerCoords')
|
|---|
| 1370 |
|
|---|
| 1371 | # Convert the indices of the requested slice to account for
|
|---|
| 1372 | # the rotation.
|
|---|
| 1373 |
|
|---|
| 1374 | xShape = self.Shape[-1]
|
|---|
| 1375 | rotationInCells = self._XRotationInCells % xShape
|
|---|
| 1376 | xStart = (sliceList[-1].start + rotationInCells) % xShape
|
|---|
| 1377 | xStop = (sliceList[-1].stop - 1 + rotationInCells) % xShape + 1
|
|---|
| 1378 |
|
|---|
| 1379 | # If the stop index is greater than the start index, it means
|
|---|
| 1380 | # the requested slice does not straddle the left and right
|
|---|
| 1381 | # edges of the contained grid, and we can retrieve the data
|
|---|
| 1382 | # with a single request.
|
|---|
| 1383 |
|
|---|
| 1384 | if xStop > xStart:
|
|---|
| 1385 | data = self._Grid.UnscaledData.__getitem__(tuple(list(sliceList[:-1]) + [slice(xStart, xStop)]))
|
|---|
| 1386 |
|
|---|
| 1387 | # Otherwise (the requested slice straddles), retrieve the two
|
|---|
| 1388 | # slabs of data and concatenate them together.
|
|---|
| 1389 |
|
|---|
| 1390 | else:
|
|---|
| 1391 | import numpy
|
|---|
| 1392 | data = numpy.concatenate((self._Grid.UnscaledData.__getitem__(tuple(list(sliceList[:-1]) + [slice(xStart, xShape)])),
|
|---|
| 1393 | self._Grid.UnscaledData.__getitem__(tuple(list(sliceList[:-1]) + [slice(0, xStop)]))),
|
|---|
| 1394 | axis=len(self.Dimensions) - 1)
|
|---|
| 1395 |
|
|---|
| 1396 | # Return the data and self.UnscaledNoDataValue.
|
|---|
| 1397 |
|
|---|
| 1398 | return data, self.UnscaledNoDataValue
|
|---|
| 1399 |
|
|---|
| 1400 |
|
|---|
| 1401 | ###############################################################################
|
|---|
| 1402 | # Metadata: module
|
|---|
| 1403 | ###############################################################################
|
|---|
| 1404 |
|
|---|
| 1405 | from GeoEco.Metadata import *
|
|---|
| 1406 | from GeoEco.Types import *
|
|---|
| 1407 |
|
|---|
| 1408 | AddModuleMetadata(shortDescription=_(u'Classes representing virtual datasets.')) # TODO: Better description
|
|---|
| 1409 |
|
|---|
| 1410 | ###############################################################################
|
|---|
| 1411 | # Metadata: TimeSeriesGridStack class
|
|---|
| 1412 | ###############################################################################
|
|---|
| 1413 |
|
|---|
| 1414 | AddClassMetadata(TimeSeriesGridStack,
|
|---|
| 1415 | shortDescription=_(u'TODO: Add description.'))
|
|---|
| 1416 |
|
|---|
| 1417 | # TODO: Add metadata
|
|---|
| 1418 |
|
|---|
| 1419 | ###############################################################################
|
|---|
| 1420 | # Metadata: GridSlice class
|
|---|
| 1421 | ###############################################################################
|
|---|
| 1422 |
|
|---|
| 1423 | AddClassMetadata(GridSlice,
|
|---|
| 1424 | shortDescription=_(u'TODO: Add description.'))
|
|---|
| 1425 |
|
|---|
| 1426 | # TODO: Add metadata
|
|---|
| 1427 |
|
|---|
| 1428 | ###############################################################################
|
|---|
| 1429 | # Metadata: GridSliceCollection class
|
|---|
| 1430 | ###############################################################################
|
|---|
| 1431 |
|
|---|
| 1432 | AddClassMetadata(GridSliceCollection,
|
|---|
| 1433 | shortDescription=_(u'TODO: Add description.'))
|
|---|
| 1434 |
|
|---|
| 1435 | # TODO: Add metadata
|
|---|
| 1436 |
|
|---|
| 1437 | ###############################################################################
|
|---|
| 1438 | # Metadata: ClippedGrid class
|
|---|
| 1439 | ###############################################################################
|
|---|
| 1440 |
|
|---|
| 1441 | AddClassMetadata(ClippedGrid,
|
|---|
| 1442 | shortDescription=_(u'TODO: Add description.'))
|
|---|
| 1443 |
|
|---|
| 1444 | # Constructor
|
|---|
| 1445 |
|
|---|
| 1446 | _ClipMaxParameterDescription = _(
|
|---|
| 1447 | u"""%(extent)s %(dim)s coordinate or index.
|
|---|
| 1448 |
|
|---|
| 1449 | The Clip By parameter determines whether the value is a coordinate or
|
|---|
| 1450 | an index. If a coordinate is provided, it may be an integer or a
|
|---|
| 1451 | floating point number. If an index is provided, it must be an
|
|---|
| 1452 | non-negative integer.
|
|---|
| 1453 |
|
|---|
| 1454 | If a value is not provided, the clipped grid will extend to the full
|
|---|
| 1455 | extent in the %(dir)s %(dim)s direction.""")
|
|---|
| 1456 |
|
|---|
| 1457 | AddMethodMetadata(ClippedGrid.__init__,
|
|---|
| 1458 | shortDescription=_(u'Constructs a new ClippedGrid instance.'),
|
|---|
| 1459 | isExposedToPythonCallers=True)
|
|---|
| 1460 |
|
|---|
| 1461 | AddArgumentMetadata(ClippedGrid.__init__, u'self',
|
|---|
| 1462 | typeMetadata=ClassInstanceTypeMetadata(cls=ClippedGrid),
|
|---|
| 1463 | description=_(u'ClippedGrid instance.'))
|
|---|
| 1464 |
|
|---|
| 1465 | AddArgumentMetadata(ClippedGrid.__init__, u'grid',
|
|---|
| 1466 | typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
|
|---|
| 1467 | description=_(u"""Grid to clip."""))
|
|---|
| 1468 |
|
|---|
| 1469 | AddArgumentMetadata(ClippedGrid.__init__, u'clipBy',
|
|---|
| 1470 | typeMetadata=UnicodeStringTypeMetadata(allowedValues=[u'Map coordinates', u'Cell indices'], makeLowercase=True),
|
|---|
| 1471 | description=_(
|
|---|
| 1472 | u"""Specifies the type of extent values that will be used to clip the
|
|---|
| 1473 | grid, one of:
|
|---|
| 1474 |
|
|---|
| 1475 | * Coordinates - the values are floating-point numbers representing
|
|---|
| 1476 | coordinates in space and time. Typically this means that x and y
|
|---|
| 1477 | coordinates will be in degrees (if a geographic coordinate system is
|
|---|
| 1478 | used) or a linear unit such as meters (if a projected coordinate
|
|---|
| 1479 | system is used), the z coordinate will be in a linear unit such as
|
|---|
| 1480 | meters, and the time coordinate will be a date and time string in a
|
|---|
| 1481 | standard format such as 'YYYY-MM-DD HH:MM:SS' or a Python datetime
|
|---|
| 1482 | instance.
|
|---|
| 1483 |
|
|---|
| 1484 | * Indices - the values are integer indices of cells in the grid,
|
|---|
| 1485 | starting at 0 and ending at the maximum index value. For example, if
|
|---|
| 1486 | a 2D grid has 360 columns and 180 rows, the x and y indices will
|
|---|
| 1487 | range from 0 to 359 and 0 to 179, respectively.
|
|---|
| 1488 |
|
|---|
| 1489 | If 'Indices' is specified but floating point numbers are provided for
|
|---|
| 1490 | the extent values, the decimal portions of the numbers will be
|
|---|
| 1491 | truncated (e.g. the value 1.7 will be truncated to 1).
|
|---|
| 1492 |
|
|---|
| 1493 | If 'Coordinates' is specified and a coordinate value falls within a
|
|---|
| 1494 | cell, rather than exactly on a boundary between two cells, the cell
|
|---|
| 1495 | will be included in the clipped grid (it will not be clipped out).
|
|---|
| 1496 | Because computers cannot represent all floating-point numbers at full
|
|---|
| 1497 | precision, the resulting rounding errors can sometimes produce
|
|---|
| 1498 | unexpected results. For example, consider a grid with a cell size of
|
|---|
| 1499 | 0.1. We expect that the cells centered at 0.5 and 1.5 would meet at
|
|---|
| 1500 | coordinate value 0.1, but 0.1 cannot be fully represented by the
|
|---|
| 1501 | computer using standard 64-bit floating-point numbers. The computer
|
|---|
| 1502 | rounds it to 0.10000000000000001. Therefore, if you were to clip the
|
|---|
| 1503 | grid a maximum coordinate of 0.10000000000000001, you would expect
|
|---|
| 1504 | that the cell centered at 1.5 would be included in the resulting grid,
|
|---|
| 1505 | because 0.10000000000000001 falls within that cell. But the computer
|
|---|
| 1506 | actually considers the boundary between the two cells to be at
|
|---|
| 1507 | 0.10000000000000001, not 0.1, so that cell would be clipped out."""), # TODO: Add discussion of how small indices correspond to small coordinate values
|
|---|
| 1508 | arcGISDisplayName=_(u'Clip by'))
|
|---|
| 1509 |
|
|---|
| 1510 | AddArgumentMetadata(ClippedGrid.__init__, u'xMin',
|
|---|
| 1511 | typeMetadata=FloatTypeMetadata(canBeNone=True),
|
|---|
| 1512 | description=_ClipMaxParameterDescription % {u'extent': _(u'Minimum'), u'dim': u'x', u'dir': _(u'negative')},
|
|---|
| 1513 | arcGISDisplayName=_(u'Minimum x extent'))
|
|---|
| 1514 |
|
|---|
| 1515 | AddArgumentMetadata(ClippedGrid.__init__, u'xMax',
|
|---|
| 1516 | typeMetadata=FloatTypeMetadata(canBeNone=True),
|
|---|
| 1517 | description=_ClipMaxParameterDescription % {u'extent': _(u'Maximum'), u'dim': u'x', u'dir': _(u'positive')},
|
|---|
| 1518 | arcGISDisplayName=_(u'Maximum x extent'))
|
|---|
| 1519 |
|
|---|
| 1520 | AddArgumentMetadata(ClippedGrid.__init__, u'yMin',
|
|---|
| 1521 | typeMetadata=FloatTypeMetadata(canBeNone=True),
|
|---|
| 1522 | description=_ClipMaxParameterDescription % {u'extent': _(u'Minimum'), u'dim': u'y', u'dir': _(u'negative')},
|
|---|
| 1523 | arcGISDisplayName=_(u'Minimum y extent'))
|
|---|
| 1524 |
|
|---|
| 1525 | AddArgumentMetadata(ClippedGrid.__init__, u'yMax',
|
|---|
| 1526 | typeMetadata=FloatTypeMetadata(canBeNone=True),
|
|---|
| 1527 | description=_ClipMaxParameterDescription % {u'extent': _(u'Maximum'), u'dim': u'y', u'dir': _(u'positive')},
|
|---|
| 1528 | arcGISDisplayName=_(u'Maximum y extent'))
|
|---|
| 1529 |
|
|---|
| 1530 | AddArgumentMetadata(ClippedGrid.__init__, u'zMin',
|
|---|
| 1531 | typeMetadata=FloatTypeMetadata(canBeNone=True),
|
|---|
| 1532 | description=_ClipMaxParameterDescription % {u'extent': _(u'Minimum'), u'dim': u'z', u'dir': _(u'negative')}, # TODO: What about the sign of z? Does z get smaller or larger with increasing depth?
|
|---|
| 1533 | arcGISDisplayName=_(u'Minimum z extent'))
|
|---|
| 1534 |
|
|---|
| 1535 | AddArgumentMetadata(ClippedGrid.__init__, u'zMax',
|
|---|
| 1536 | typeMetadata=FloatTypeMetadata(canBeNone=True),
|
|---|
| 1537 | description=_ClipMaxParameterDescription % {u'extent': _(u'Maximum'), u'dim': u'z', u'dir': _(u'positive')},
|
|---|
| 1538 | arcGISDisplayName=_(u'Maximum z extent'))
|
|---|
| 1539 |
|
|---|
| 1540 | AddArgumentMetadata(ClippedGrid.__init__, u'tMin',
|
|---|
| 1541 | typeMetadata=AnyObjectTypeMetadata(canBeNone=True),
|
|---|
| 1542 | description=_(
|
|---|
| 1543 | u"""Minimum t coordinate or index.
|
|---|
| 1544 |
|
|---|
| 1545 | The Clip By parameter determines whether the value is a coordinate or
|
|---|
| 1546 | an index. If a coordinate is provided, it may be a date and time
|
|---|
| 1547 | string in a standard format such as 'YYYY-MM-DD HH:MM:SS' or a Python
|
|---|
| 1548 | datetime instance. If an index is provided, it must be an non-negative
|
|---|
| 1549 | integer.
|
|---|
| 1550 |
|
|---|
| 1551 | If a value is not provided, the clipped grid will extend to the full
|
|---|
| 1552 | extent in the negative t direction."""),
|
|---|
| 1553 | arcGISDisplayName=_(u'Minimum t extent'))
|
|---|
| 1554 |
|
|---|
| 1555 | AddArgumentMetadata(ClippedGrid.__init__, u'tMax',
|
|---|
| 1556 | typeMetadata=AnyObjectTypeMetadata(canBeNone=True),
|
|---|
| 1557 | description=_(
|
|---|
| 1558 | u"""Maximum t coordinate or index.
|
|---|
| 1559 |
|
|---|
| 1560 | The Clip By parameter determines whether the value is a coordinate or
|
|---|
| 1561 | an index. If a coordinate is provided, it may be a date and time
|
|---|
| 1562 | string in a standard format such as 'YYYY-MM-DD HH:MM:SS' or a Python
|
|---|
| 1563 | datetime instance. If an index is provided, it must be an non-negative
|
|---|
| 1564 | integer.
|
|---|
| 1565 |
|
|---|
| 1566 | If a value is not provided, the clipped grid will extend to the full
|
|---|
| 1567 | extent in the positive t direction."""),
|
|---|
| 1568 | arcGISDisplayName=_(u'Maximum t extent'))
|
|---|
| 1569 |
|
|---|
| 1570 | AddResultMetadata(ClippedGrid.__init__, u'clippedGrid',
|
|---|
| 1571 | typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
|
|---|
| 1572 | description=_(u"""Clipped grid."""))
|
|---|
| 1573 |
|
|---|
| 1574 | ###############################################################################
|
|---|
| 1575 | # Metadata: MaskedGrid class
|
|---|
| 1576 | ###############################################################################
|
|---|
| 1577 |
|
|---|
| 1578 | AddClassMetadata(MaskedGrid,
|
|---|
| 1579 | shortDescription=_(u'TODO: Add description.'))
|
|---|
| 1580 |
|
|---|
| 1581 | # TODO: Add metadata
|
|---|
| 1582 |
|
|---|
| 1583 | ###############################################################################
|
|---|
| 1584 | # Metadata: RotatedGlobalGrid class
|
|---|
| 1585 | ###############################################################################
|
|---|
| 1586 |
|
|---|
| 1587 | AddClassMetadata(RotatedGlobalGrid,
|
|---|
| 1588 | shortDescription=_(u'TODO: Add description.'))
|
|---|
| 1589 |
|
|---|
| 1590 | # Constructor
|
|---|
| 1591 |
|
|---|
| 1592 | AddMethodMetadata(RotatedGlobalGrid.__init__,
|
|---|
| 1593 | shortDescription=_(u'Constructs a new RotatedGlobalGrid instance.'),
|
|---|
| 1594 | isExposedToPythonCallers=True)
|
|---|
| 1595 |
|
|---|
| 1596 | AddArgumentMetadata(RotatedGlobalGrid.__init__, u'self',
|
|---|
| 1597 | typeMetadata=ClassInstanceTypeMetadata(cls=RotatedGlobalGrid),
|
|---|
| 1598 | description=_(u'RotatedGlobalGrid instance.'))
|
|---|
| 1599 |
|
|---|
| 1600 | AddArgumentMetadata(RotatedGlobalGrid.__init__, u'grid',
|
|---|
| 1601 | typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
|
|---|
| 1602 | description=_(
|
|---|
| 1603 | u"""Grid to rotate. The grid must have global longitudinal
|
|---|
| 1604 | extent."""))
|
|---|
| 1605 |
|
|---|
| 1606 | AddArgumentMetadata(RotatedGlobalGrid.__init__, u'rotationOffset',
|
|---|
| 1607 | typeMetadata=FloatTypeMetadata(),
|
|---|
| 1608 | description=_(
|
|---|
| 1609 | u"""Quantity to rotate the grid by, in the units specified by the
|
|---|
| 1610 | Rotation Units parameter.
|
|---|
| 1611 |
|
|---|
| 1612 | Use this parameter to center the grid on a different longitude.
|
|---|
| 1613 | Positive values move the center of the grid to the right (east);
|
|---|
| 1614 | negative values move the center to the west. For example, if the
|
|---|
| 1615 | Rotation Units parameter is 'Cells', the value 10 will cause the
|
|---|
| 1616 | center of the grid to shift 10 cells to the right. Effectively, the 10
|
|---|
| 1617 | left-most cells to be stripped from the left edge and moved to the
|
|---|
| 1618 | right edge."""))
|
|---|
| 1619 |
|
|---|
| 1620 | AddArgumentMetadata(RotatedGlobalGrid.__init__, u'rotationUnits',
|
|---|
| 1621 | typeMetadata=UnicodeStringTypeMetadata(allowedValues=[u'Map units', u'Cells'], makeLowercase=True),
|
|---|
| 1622 | description=_(
|
|---|
| 1623 | u"""Specifies the type of values that will be used to rotate the grid,
|
|---|
| 1624 | one of:
|
|---|
| 1625 |
|
|---|
| 1626 | * Map units - the unit of rotation will be the same as the linear unit
|
|---|
| 1627 | of the map, typically degrees for data in geographic coordinate
|
|---|
| 1628 | systems and meters for data in projected coordinate systems. Because
|
|---|
| 1629 | the rotation must be in whole cells, the rotation quantity will be
|
|---|
| 1630 | converted to grid cells and rounded to the nearest cell.
|
|---|
| 1631 |
|
|---|
| 1632 | * Cells - the unit of rotation will be in grid cells. Because the
|
|---|
| 1633 | rotation must be in whole cells, the rotation quantity will be
|
|---|
| 1634 | rounded to the neareast cell.
|
|---|
| 1635 | """))
|
|---|
| 1636 |
|
|---|
| 1637 | AddResultMetadata(RotatedGlobalGrid.__init__, u'rotatedGrid',
|
|---|
| 1638 | typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
|
|---|
| 1639 | description=_(u"""Rotated grid."""))
|
|---|
| 1640 |
|
|---|
| 1641 | ###############################################################################
|
|---|
| 1642 | # Names exported by this module
|
|---|
| 1643 | ###############################################################################
|
|---|
| 1644 |
|
|---|
| 1645 | __all__ = ['TimeSeriesGridStack',
|
|---|
| 1646 | 'GridSlice',
|
|---|
| 1647 | 'GridSliceCollection',
|
|---|
| 1648 | 'ClippedGrid',
|
|---|
| 1649 | 'MaskedGrid',
|
|---|
| 1650 | 'RotatedGlobalGrid']
|
|---|