#!/usr/bin/env python """Collection filters. Example usage: Filter('time', low, high) .bounds(ring) .eq('time', value) .lt('time', value) """ # Using lowercase function naming to match the JavaScript names. # pylint: disable=g-bad-name from . import apifunction from . import computedobject from . import ee_exception # A map from the deprecated old-style comparison operator names to API # function names, implicitly prefixed with "Filter.". Negative operators # (those starting with "not_") are not included. _FUNCTION_NAMES = { 'equals': 'equals', 'less_than': 'lessThan', 'greater_than': 'greaterThan', 'contains': 'stringContains', 'starts_with': 'stringStartsWith', 'ends_with': 'stringEndsWith', } class Filter(computedobject.ComputedObject): """An object to represent collection filters.""" _initialized = False def __init__(self, opt_filter=None): """Construct a filter. This constructor accepts the following args: 1) Another filter. 2) An array of filters (which are implicitly ANDed together). 3) A ComputedObject returning a filter. Users shouldn't be making these; they're produced by the generator functions below. Args: opt_filter: Optional filter to add. """ self.initialize() if isinstance(opt_filter, (list, tuple)): if not opt_filter: raise ee_exception.EEException('Empty list specified for ee.Filter().') elif len(opt_filter) == 1: opt_filter = opt_filter[0] else: self._filter = tuple(opt_filter) super(Filter, self).__init__( apifunction.ApiFunction.lookup('Filter.and'), {'filters': self._filter}) return if isinstance(opt_filter, computedobject.ComputedObject): super(Filter, self).__init__( opt_filter.func, opt_filter.args, opt_filter.varName) self._filter = (opt_filter,) elif opt_filter is None: # A silly call with no arguments left for backward-compatibility. # Encoding such a filter is expected to fail, but it can be composed # by calling the various methods that end up in _append(). super(Filter, self).__init__(None, None) self._filter = () else: raise ee_exception.EEException( 'Invalid argument specified for ee.Filter(): %s' % opt_filter) @classmethod def initialize(cls): """Imports API functions to this class.""" if not cls._initialized: apifunction.ApiFunction.importApi(cls, 'Filter', 'Filter') cls._initialized = True @classmethod def reset(cls): """Removes imported API functions from this class.""" apifunction.ApiFunction.clearApi(cls) cls._initialized = False def predicateCount(self): """Return the number of predicates that have been added to this filter. Returns: The number of predicates that have been added to this filter. This does not count nested predicates. """ return len(self._filter) def _append(self, new_filter): """Append a predicate to this filter. These are implicitly ANDed. Args: new_filter: The filter to append to this one. Possible types are: 1) another fully constructed Filter, 2) a JSON representation of a filter, 3) an array of 1 or 2. Returns: A new filter that is the combination of both. """ if new_filter is not None: prev = list(self._filter) if isinstance(new_filter, Filter): prev.extend(new_filter._filter) # pylint: disable=protected-access elif isinstance(new_filter, list): prev.extend(new_filter) else: prev.append(new_filter) return Filter(prev) def Not(self): """Returns the opposite of this filter. Returns: The negated filter, which will match iff this filter doesn't. """ return apifunction.ApiFunction.call_('Filter.not', self) @staticmethod def metadata_(name, operator, value): """Filter on metadata. This is deprecated. Args: name: The property name to filter on. operator: The type of comparison. One of: "equals", "less_than", "greater_than", "contains", "begins_with", "ends_with", or any of these prefixed with "not_". value: The value to compare against. Returns: The new filter. Deprecated. Use ee.Filter.eq(), ee.Filter.gte(), etc.' """ operator = operator.lower() # Check for negated filters. negated = False if operator.startswith('not_'): negated = True operator = operator[4:] # Convert the operator to a function. if operator not in _FUNCTION_NAMES: raise ee_exception.EEException( 'Unknown filtering operator: %s' % operator) func_name = 'Filter.' + _FUNCTION_NAMES[operator] new_filter = apifunction.ApiFunction.call_(func_name, name, value) return new_filter.Not() if negated else new_filter @staticmethod def eq(name, value): """Filter to metadata equal to the given value.""" return apifunction.ApiFunction.call_('Filter.equals', name, value) @staticmethod def neq(name, value): """Filter to metadata not equal to the given value.""" return Filter.eq(name, value).Not() @staticmethod def lt(name, value): """Filter to metadata less than the given value.""" return apifunction.ApiFunction.call_('Filter.lessThan', name, value) @staticmethod def gte(name, value): """Filter on metadata greater than or equal to the given value.""" return Filter.lt(name, value).Not() @staticmethod def gt(name, value): """Filter on metadata greater than the given value.""" return apifunction.ApiFunction.call_('Filter.greaterThan', name, value) @staticmethod def lte(name, value): """Filter on metadata less than or equal to the given value.""" return Filter.gt(name, value).Not() @staticmethod def And(*args): """Combine two or more filters using boolean AND.""" if len(args) == 1 and isinstance(args[0], (list, tuple)): args = args[0] return apifunction.ApiFunction.call_('Filter.and', args) @staticmethod def Or(*args): """Combine two or more filters using boolean OR.""" if len(args) == 1 and isinstance(args[0], (list, tuple)): args = args[0] return apifunction.ApiFunction.call_('Filter.or', args) @staticmethod def date(start, opt_end=None): """Filter images by date. The start and end may be a Date, numbers (interpreted as milliseconds since 1970-01-01T00:00:00Z), or strings (such as '1996-01-01T08:00'). Based on 'system:time_start'. Args: start: The inclusive start date. opt_end: The optional exclusive end date, If not specified, a 1-millisecond range starting at 'start' is created. Returns: The modified filter. """ date_range = apifunction.ApiFunction.call_('DateRange', start, opt_end) return apifunction.ApiFunction.apply_('Filter.dateRangeContains', { 'leftValue': date_range, 'rightField': 'system:time_start' }) @staticmethod def inList(opt_leftField=None, opt_rightValue=None, opt_rightField=None, opt_leftValue=None): """Filter on metadata contained in a list. Args: opt_leftField: A selector for the left operand. Should not be specified if leftValue is specified. opt_rightValue: The value of the right operand. Should not be specified if rightField is specified. opt_rightField: A selector for the right operand. Should not be specified if rightValue is specified. opt_leftValue: The value of the left operand. Should not be specified if leftField is specified. Returns: The constructed filter. """ # Implement this in terms of listContains, with the arguments switched. # In listContains the list is on the left side, while in inList it's on # the right. return apifunction.ApiFunction.apply_('Filter.listContains', { 'leftField': opt_rightField, 'rightValue': opt_leftValue, 'rightField': opt_leftField, 'leftValue': opt_rightValue }) @staticmethod def geometry(geometry, opt_errorMargin=None): """Filter on intersection with geometry. Items in the collection with a footprint that fails to intersect the given geometry will be excluded. Args: geometry: The geometry to filter to either as a GeoJSON geometry, or a FeatureCollection, from which a geometry will be extracted. opt_errorMargin: An optional error margin. If a number, interpreted as sphere surface meters. Returns: The constructed filter. """ # Invoke geometry promotion then manually promote to a Feature. args = { 'leftField': '.all', 'rightValue': apifunction.ApiFunction.call_('Feature', geometry) } if opt_errorMargin is not None: args['maxError'] = opt_errorMargin return apifunction.ApiFunction.apply_('Filter.intersects', args) @staticmethod def bounds(geometry, opt_errorMargin=None): """Filter on intersection with geometry. Items in the collection with a footprint that fails to intersect the given geometry will be excluded. This is an alias for geometry(). Caution: providing a large or complex collection as input can result in poor performance. Collating the geometry of collections does not scale well, use the smallest collection (or geometry) that is required to achieve the desired outcome. Args: geometry: The geometry to filter to either as a GeoJSON geometry, or a FeatureCollection, from which a geometry will be extracted. opt_errorMargin: An optional error margin. If a number, interpreted as sphere surface meters. Returns: The constructed filter. """ return Filter.geometry(geometry, opt_errorMargin) @staticmethod def name(): return 'Filter'