root/MGET/Branches/Jason/PythonPackage/src/GeoEco/Datasets/Virtual.py @ 534

Revision 534, 83.9 KB (checked in by jjr8, 3 years ago)

Continuing work on the Datasets infrastructure.

Line 
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
21import bisect
22import datetime
23
24from GeoEco.Datasets import DatasetCollection, QueryableAttribute, Grid
25from GeoEco.DynamicDocString import DynamicDocString
26from GeoEco.Internationalization import _
27
28
29class 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
313class 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
507class 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
706class 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
912class 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
1278class 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
1405from GeoEco.Metadata import *
1406from GeoEco.Types import *
1407
1408AddModuleMetadata(shortDescription=_(u'Classes representing virtual datasets.'))    # TODO: Better description
1409
1410###############################################################################
1411# Metadata: TimeSeriesGridStack class
1412###############################################################################
1413
1414AddClassMetadata(TimeSeriesGridStack,
1415    shortDescription=_(u'TODO: Add description.'))
1416
1417# TODO: Add metadata
1418
1419###############################################################################
1420# Metadata: GridSlice class
1421###############################################################################
1422
1423AddClassMetadata(GridSlice,
1424    shortDescription=_(u'TODO: Add description.'))
1425
1426# TODO: Add metadata
1427
1428###############################################################################
1429# Metadata: GridSliceCollection class
1430###############################################################################
1431
1432AddClassMetadata(GridSliceCollection,
1433    shortDescription=_(u'TODO: Add description.'))
1434
1435# TODO: Add metadata
1436
1437###############################################################################
1438# Metadata: ClippedGrid class
1439###############################################################################
1440
1441AddClassMetadata(ClippedGrid,
1442    shortDescription=_(u'TODO: Add description.'))
1443
1444# Constructor
1445
1446_ClipMaxParameterDescription = _(
1447u"""%(extent)s %(dim)s coordinate or index.
1448
1449The Clip By parameter determines whether the value is a coordinate or
1450an index. If a coordinate is provided, it may be an integer or a
1451floating point number. If an index is provided, it must be an
1452non-negative integer.
1453
1454If a value is not provided, the clipped grid will extend to the full
1455extent in the %(dir)s %(dim)s direction.""")
1456
1457AddMethodMetadata(ClippedGrid.__init__,
1458    shortDescription=_(u'Constructs a new ClippedGrid instance.'),
1459    isExposedToPythonCallers=True)
1460
1461AddArgumentMetadata(ClippedGrid.__init__, u'self',
1462    typeMetadata=ClassInstanceTypeMetadata(cls=ClippedGrid),
1463    description=_(u'ClippedGrid instance.'))
1464
1465AddArgumentMetadata(ClippedGrid.__init__, u'grid',
1466    typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
1467    description=_(u"""Grid to clip."""))
1468
1469AddArgumentMetadata(ClippedGrid.__init__, u'clipBy',
1470    typeMetadata=UnicodeStringTypeMetadata(allowedValues=[u'Map coordinates', u'Cell indices'], makeLowercase=True),
1471    description=_(
1472u"""Specifies the type of extent values that will be used to clip the
1473grid, 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
1489If 'Indices' is specified but floating point numbers are provided for
1490the extent values, the decimal portions of the numbers will be
1491truncated (e.g. the value 1.7 will be truncated to 1).
1492
1493If 'Coordinates' is specified and a coordinate value falls within a
1494cell, rather than exactly on a boundary between two cells, the cell
1495will be included in the clipped grid (it will not be clipped out).
1496Because computers cannot represent all floating-point numbers at full
1497precision, the resulting rounding errors can sometimes produce
1498unexpected results. For example, consider a grid with a cell size of
14990.1. We expect that the cells centered at 0.5 and 1.5 would meet at
1500coordinate value 0.1, but 0.1 cannot be fully represented by the
1501computer using standard 64-bit floating-point numbers. The computer
1502rounds it to 0.10000000000000001. Therefore, if you were to clip the
1503grid a maximum coordinate of 0.10000000000000001, you would expect
1504that the cell centered at 1.5 would be included in the resulting grid,
1505because 0.10000000000000001 falls within that cell. But the computer
1506actually considers the boundary between the two cells to be at
15070.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
1510AddArgumentMetadata(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
1515AddArgumentMetadata(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
1520AddArgumentMetadata(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
1525AddArgumentMetadata(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
1530AddArgumentMetadata(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
1535AddArgumentMetadata(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
1540AddArgumentMetadata(ClippedGrid.__init__, u'tMin',
1541    typeMetadata=AnyObjectTypeMetadata(canBeNone=True),
1542    description=_(
1543u"""Minimum t coordinate or index.
1544
1545The Clip By parameter determines whether the value is a coordinate or
1546an index. If a coordinate is provided, it may be a date and time
1547string in a standard format such as 'YYYY-MM-DD HH:MM:SS' or a Python
1548datetime instance. If an index is provided, it must be an non-negative
1549integer.
1550
1551If a value is not provided, the clipped grid will extend to the full
1552extent in the negative t direction."""),
1553    arcGISDisplayName=_(u'Minimum t extent'))
1554
1555AddArgumentMetadata(ClippedGrid.__init__, u'tMax',
1556    typeMetadata=AnyObjectTypeMetadata(canBeNone=True),
1557    description=_(
1558u"""Maximum t coordinate or index.
1559
1560The Clip By parameter determines whether the value is a coordinate or
1561an index. If a coordinate is provided, it may be a date and time
1562string in a standard format such as 'YYYY-MM-DD HH:MM:SS' or a Python
1563datetime instance. If an index is provided, it must be an non-negative
1564integer.
1565
1566If a value is not provided, the clipped grid will extend to the full
1567extent in the positive t direction."""),
1568    arcGISDisplayName=_(u'Maximum t extent'))
1569
1570AddResultMetadata(ClippedGrid.__init__, u'clippedGrid',
1571    typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
1572    description=_(u"""Clipped grid."""))
1573
1574###############################################################################
1575# Metadata: MaskedGrid class
1576###############################################################################
1577
1578AddClassMetadata(MaskedGrid,
1579    shortDescription=_(u'TODO: Add description.'))
1580
1581# TODO: Add metadata
1582
1583###############################################################################
1584# Metadata: RotatedGlobalGrid class
1585###############################################################################
1586
1587AddClassMetadata(RotatedGlobalGrid,
1588    shortDescription=_(u'TODO: Add description.'))
1589
1590# Constructor
1591
1592AddMethodMetadata(RotatedGlobalGrid.__init__,
1593    shortDescription=_(u'Constructs a new RotatedGlobalGrid instance.'),
1594    isExposedToPythonCallers=True)
1595
1596AddArgumentMetadata(RotatedGlobalGrid.__init__, u'self',
1597    typeMetadata=ClassInstanceTypeMetadata(cls=RotatedGlobalGrid),
1598    description=_(u'RotatedGlobalGrid instance.'))
1599
1600AddArgumentMetadata(RotatedGlobalGrid.__init__, u'grid',
1601    typeMetadata=ClassInstanceTypeMetadata(cls=Grid),
1602    description=_(
1603u"""Grid to rotate. The grid must have global longitudinal
1604extent."""))
1605
1606AddArgumentMetadata(RotatedGlobalGrid.__init__, u'rotationOffset',
1607    typeMetadata=FloatTypeMetadata(),
1608    description=_(
1609u"""Quantity to rotate the grid by, in the units specified by the
1610Rotation Units parameter.
1611
1612Use this parameter to center the grid on a different longitude.
1613Positive values move the center of the grid to the right (east);
1614negative values move the center to the west. For example, if the
1615Rotation Units parameter is 'Cells', the value 10 will cause the
1616center of the grid to shift 10 cells to the right. Effectively, the 10
1617left-most cells to be stripped from the left edge and moved to the
1618right edge."""))
1619
1620AddArgumentMetadata(RotatedGlobalGrid.__init__, u'rotationUnits',
1621    typeMetadata=UnicodeStringTypeMetadata(allowedValues=[u'Map units', u'Cells'], makeLowercase=True),
1622    description=_(
1623u"""Specifies the type of values that will be used to rotate the grid,
1624one 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
1637AddResultMetadata(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']
Note: See TracBrowser for help on using the browser.