"""An object representing EE Geometries.""" from __future__ import annotations import collections.abc import json import math from typing import Any, Dict, List, Optional, Sequence, Tuple, Union from ee import _arg_types from ee import _utils from ee import apifunction from ee import computedobject from ee import ee_exception from ee import ee_list from ee import ee_number from ee import ee_string from ee import ee_types from ee import featurecollection from ee import projection from ee import serializer # A sentinel value used to detect unspecified function parameters. _UNSPECIFIED = object() class Geometry(computedobject.ComputedObject): """An Earth Engine geometry.""" _initialized = False # Tell pytype to not complain about dynamic attributes. _HAS_DYNAMIC_ATTRIBUTES = True @_utils.accept_opt_prefix('opt_proj', 'opt_geodesic', 'opt_evenOdd') def __init__( self, geo_json: Union[Dict[str, Any], computedobject.ComputedObject, Geometry], proj: Optional[Any] = None, geodesic: Optional[bool] = None, evenOdd: Optional[bool] = None, # pylint: disable=g-bad-name ): """Creates a geometry. Args: geo_json: The GeoJSON object describing the geometry or a computed object to be reinterpred as a Geometry. Supports CRS specifications as per the GeoJSON spec, but only allows named (rather than "linked" CRSs). If this includes a 'geodesic' field, and geodesic is not specified, it will be used as geodesic. proj: An optional projection specification, either as an ee.Projection, as a CRS ID code or as a WKT string. If specified, overrides any CRS found in the geo_json parameter. If unspecified and the geo_json does not declare a CRS, defaults to "EPSG:4326" (x=longitude, y=latitude). geodesic: Whether line segments should be interpreted as spherical geodesics. If false, indicates that line segments should be interpreted as planar lines in the specified CRS. If absent, defaults to true if the CRS is geographic (including the default EPSG:4326), or to false if the CRS is projected. evenOdd: If true, polygon interiors will be determined by the even/odd rule, where a point is inside if it crosses an odd number of edges to reach a point at infinity. Otherwise polygons use the left-inside rule, where interiors are on the left side of the shell's edges when walking the vertices in the given order. If unspecified, defaults to True. Raises: EEException: if the given geometry isn't valid. """ self.initialize() # pylint: disable-next=protected-access computed = isinstance(geo_json, computedobject.ComputedObject) and not ( isinstance(geo_json, Geometry) and geo_json._type is not None ) options = proj or geodesic or evenOdd if computed: if options: raise ee_exception.EEException( 'Setting the CRS or geodesic on a computed Geometry is not ' 'supported. Use Geometry.transform().') else: super().__init__(geo_json.func, geo_json.args, geo_json.varName) return # Below here we're working with a GeoJSON literal. if isinstance(geo_json, Geometry): geo_json = geo_json.encode() if not Geometry._isValidGeometry(geo_json): raise ee_exception.EEException('Invalid GeoJSON geometry.') super().__init__(None, None) # The type of the geometry. self._type = geo_json['type'] # The coordinates of the geometry, up to 4 nested levels with numbers at # the last level. None if and only if type is GeometryCollection. self._coordinates = geo_json.get('coordinates') # The subgeometries, None unless type is GeometryCollection. self._geometries = geo_json.get('geometries') # The projection code (WKT or identifier) of the geometry. if proj: self._proj = proj elif 'crs' in geo_json: self._proj = self._get_name_from_crs(geo_json.get('crs')) else: self._proj = None # Whether the geometry has spherical geodesic edges. self._geodesic = geodesic if geodesic is None and 'geodesic' in geo_json: self._geodesic = bool(geo_json['geodesic']) # Whether polygon interiors use the even/odd rule. self._evenOdd = evenOdd # pylint: disable=g-bad-name if evenOdd is None and 'evenOdd' in geo_json: self._evenOdd = bool(geo_json['evenOdd']) # Build a proxy for this object that is an invocation of a server-side # constructor. This is used during Cloud API encoding, but can't be # constructed at that time: due to id()-based caching in Serializer, # building transient objects during encoding isn't safe. ctor_args = {} if self._type == 'GeometryCollection': ctor_name = 'MultiGeometry' ctor_args['geometries'] = [Geometry(g) for g in self._geometries] else: ctor_name = self._type ctor_args['coordinates'] = self._coordinates if self._proj is not None: if isinstance(self._proj, str): ctor_args['crs'] = apifunction.ApiFunction.lookup('Projection').call( self._proj) else: ctor_args['crs'] = self._proj if self._geodesic is not None: ctor_args['geodesic'] = self._geodesic if self._evenOdd is not None: ctor_args['evenOdd'] = self._evenOdd self._computed_equivalent = apifunction.ApiFunction.lookup( 'GeometryConstructors.' + ctor_name).apply(ctor_args) def _get_name_from_crs(self, crs: Dict[str, Any]) -> str: """Returns projection name from a CRS.""" if isinstance(crs, dict) and crs.get('type') == 'name': properties = crs.get('properties') if isinstance(properties, dict): name = properties.get('name') if isinstance(name, str): return name raise ee_exception.EEException( 'Invalid CRS declaration in GeoJSON: ' + json.dumps(crs) ) @classmethod def initialize(cls) -> None: """Imports API functions to this class.""" if not cls._initialized: apifunction.ApiFunction.importApi(cls, cls.name(), cls.name()) cls._initialized = True @classmethod def reset(cls) -> None: """Removes imported API functions from this class.""" apifunction.ApiFunction.clearApi(cls) cls._initialized = False def __getitem__(self, key: str) -> Any: """Allows access to GeoJSON properties for backward-compatibility.""" return self.toGeoJSON()[key] @staticmethod # pylint: disable-next=keyword-arg-before-vararg def Point( coords=_UNSPECIFIED, proj=_UNSPECIFIED, *args, **kwargs ) -> Geometry: """Constructs an ee.Geometry describing a point. Args: coords: A list of two [x,y] coordinates in the given projection. proj: The projection of this geometry, or EPSG:4326 if unspecified. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating EPSG:4326 points, e.g., ee.Geometry.Point(lng, lat). **kwargs: Keyword args that accept "lon" and "lat" for backward- compatibility. Returns: An ee.Geometry describing a point. """ init = Geometry._parseArgs( 'Point', 1, Geometry._GetSpecifiedArgs((coords, proj) + args, ('lon', 'lat'), **kwargs)) if not isinstance(init, computedobject.ComputedObject): xy = init['coordinates'] if not isinstance(xy, (list, tuple)) or len(xy) != 2: raise ee_exception.EEException( 'The Geometry.Point constructor requires 2 coordinates.') return Geometry(init) @staticmethod # pylint: disable-next=keyword-arg-before-vararg def MultiPoint(coords=_UNSPECIFIED, proj=_UNSPECIFIED, *args) -> Geometry: """Constructs an ee.Geometry describing a MultiPoint. Args: coords: A list of points, each in the GeoJSON 'coordinates' format of a Point, or a list of the x,y coordinates in the given projection, or an ee.Geometry describing a point. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating EPSG:4326 MultiPoints given an even number of arguments, e.g., ee.Geometry.MultiPoint(aLng, aLat, bLng, bLat, ...). Returns: An ee.Geometry describing a MultiPoint. """ all_args = Geometry._GetSpecifiedArgs((coords, proj) + args) return Geometry(Geometry._parseArgs('MultiPoint', 2, all_args)) # pylint: disable=keyword-arg-before-vararg @staticmethod def Rectangle( coords=_UNSPECIFIED, proj=_UNSPECIFIED, geodesic=_UNSPECIFIED, evenOdd=_UNSPECIFIED, # pylint: disable=g-bad-name *args, **kwargs, ) -> Geometry: """Constructs an ee.Geometry describing a rectangular polygon. Args: coords: The minimum and maximum corners of the rectangle, as a list of two points each in the format of GeoJSON 'Point' coordinates, or a list of two ee.Geometry objects describing a point, or a list of four numbers in the order xMin, yMin, xMax, yMax. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. geodesic: If false, edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. The default is the geodesic state of the inputs, or true if the inputs are numbers. evenOdd: If true, polygon interiors will be determined by the even/odd rule, where a point is inside if it crosses an odd number of edges to reach a point at infinity. Otherwise polygons use the left-inside rule, where interiors are on the left side of the shell's edges when walking the vertices in the given order. If unspecified, defaults to True. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating EPSG:4326 Polygons given exactly four coordinates, e.g., ee.Geometry.Rectangle(minLng, minLat, maxLng, maxLat). **kwargs: Keyword args that accept "xlo", "ylo", "xhi" and "yhi" for backward-compatibility. Returns: An ee.Geometry describing a rectangular polygon. """ init = Geometry._parseArgs( 'Rectangle', 2, Geometry._GetSpecifiedArgs( (coords, proj, geodesic, evenOdd) + args, ('xlo', 'ylo', 'xhi', 'yhi'), **kwargs)) if not isinstance(init, computedobject.ComputedObject): # GeoJSON does not have a Rectangle type, so expand to a Polygon. xy = init['coordinates'] if not isinstance(xy, (list, tuple)) or len(xy) != 2: raise ee_exception.EEException( 'The Geometry.Rectangle constructor requires 2 points or 4 ' 'coordinates.') x1 = xy[0][0] y1 = xy[0][1] x2 = xy[1][0] y2 = xy[1][1] init['coordinates'] = [[[x1, y2], [x1, y1], [x2, y1], [x2, y2]]] init['type'] = 'Polygon' return Geometry(init) @staticmethod def BBox( west: Union[float, computedobject.ComputedObject], south: Union[float, computedobject.ComputedObject], east: Union[float, computedobject.ComputedObject], north: Union[float, computedobject.ComputedObject], ) -> Geometry: """Constructs a rectangle ee.Geometry from lines of latitude and longitude. If (east - west) ≥ 360° then the longitude range will be normalized to -180° to +180°; otherwise they will be treated as designating points on a circle (e.g., east may be numerically less than west). Args: west: The westernmost enclosed longitude. Will be adjusted to lie in the range -180° to 180°. south: The southernmost enclosed latitude. If less than -90° (south pole), will be treated as -90°. east: The easternmost enclosed longitude. north: The northernmost enclosed latitude. If greater than +90° (north pole), will be treated as +90°. Returns: An ee.Geometry describing a planar WGS84 rectangle. """ # Not using Geometry._parseArgs because that assumes the args should go # directly into a coordinates field. if Geometry._hasServerValue((west, south, east, north)): # Some arguments cannot be handled in the client, so make a server call. return (apifunction.ApiFunction.lookup('GeometryConstructors.BBox') .apply(dict(west=west, south=south, east=east, north=north))) # Else proceed with client-side implementation. # Reject NaN and positive (west) or negative (east) infinities before they # become bad JSON. The other two infinities are acceptable because we # support the general idea of an around-the-globe latitude band. By writing # them negated, we also reject NaN. if not west < math.inf: raise ee_exception.EEException( 'Geometry.BBox: west must not be {}'.format(west)) if not east > -math.inf: raise ee_exception.EEException( 'Geometry.BBox: east must not be {}'.format(east)) # Reject cases which, if we clamped them instead, would move a box whose # bounds lie entirely "past" a pole to being at the pole. By writing them # negated, we also reject NaN. if not south <= 90: raise ee_exception.EEException( 'Geometry.BBox: south must be at most +90°, but was {}°'.format( south)) if not north >= -90: raise ee_exception.EEException( 'Geometry.BBox: north must be at least -90°, but was {}°'.format( north)) # On the other hand, allow a box whose extent lies past the pole, but # canonicalize it to being exactly the pole. south = max(south, -90) north = min(north, 90) if east - west >= 360: # We conclude from seeing more than 360 degrees that the user intends to # specify the entire globe (or a band of latitudes, at least). # Canonicalize to standard global form. west = -180 east = 180 else: # Not the entire globe. Canonicalize coordinate ranges. west = Geometry._canonicalize_longitude(west) east = Geometry._canonicalize_longitude(east) if east < west: east += 360 # GeoJSON does not have a Rectangle type, so expand to a Polygon. return Geometry( geo_json={ 'coordinates': [[ [west, north], [west, south], [east, south], [east, north], ]], 'type': 'Polygon', }, geodesic=False, ) @staticmethod def _canonicalize_longitude(longitude: float) -> float: # Note that Python specifies "The modulo operator always yields a result # with the same sign as its second operand"; therefore no special handling # of negative arguments is needed. longitude = longitude % 360 if longitude > 180: longitude -= 360 return longitude # pylint: disable=keyword-arg-before-vararg @staticmethod def LineString( coords=_UNSPECIFIED, proj=_UNSPECIFIED, geodesic=_UNSPECIFIED, maxError=_UNSPECIFIED, # pylint: disable=g-bad-name *args, ) -> Geometry: """Constructs an ee.Geometry describing a LineString. Args: coords: A list of at least two points. May be a list of coordinates in the GeoJSON 'LineString' format, a list of at least two ee.Geometry objects describing a point, or a list of at least four numbers defining the [x,y] coordinates of at least two points. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. geodesic: If false, edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. The default is the geodesic state of the inputs, or true if the inputs are numbers. maxError: Max error when input geometry must be reprojected to an explicitly requested result projection or geodesic state. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating geodesic EPSG:4326 LineStrings given an even number of arguments, e.g., ee.Geometry.LineString(aLng, aLat, bLng, bLat, ...). Returns: An ee.Geometry describing a LineString. """ all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError) + args) return Geometry(Geometry._parseArgs('LineString', 2, all_args)) # pylint: disable=keyword-arg-before-vararg @staticmethod def LinearRing( coords=_UNSPECIFIED, proj=_UNSPECIFIED, geodesic=_UNSPECIFIED, maxError=_UNSPECIFIED, # pylint: disable=g-bad-name *args, ) -> Geometry: """Constructs an ee.Geometry describing a LinearRing. If the last point is not equal to the first, a duplicate of the first point will be added at the end. Args: coords: A list of points in the ring. May be a list of coordinates in the GeoJSON 'LinearRing' format, a list of at least three ee.Geometry objects describing a point, or a list of at least six numbers defining the [x,y] coordinates of at least three points. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. geodesic: If false, edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. The default is the geodesic state of the inputs, or true if the inputs are numbers. maxError: Max error when input geometry must be reprojected to an explicitly requested result projection or geodesic state. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating geodesic EPSG:4326 LinearRings given an even number of arguments, e.g., ee.Geometry.LinearRing(aLng, aLat, bLng, bLat, ...). Returns: A dictionary representing a GeoJSON LinearRing. """ all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError) + args) return Geometry(Geometry._parseArgs('LinearRing', 2, all_args)) # pylint: disable=keyword-arg-before-vararg @staticmethod def MultiLineString( coords=_UNSPECIFIED, proj=_UNSPECIFIED, geodesic=_UNSPECIFIED, maxError=_UNSPECIFIED, # pylint: disable=g-bad-name *args, ) -> Geometry: """Constructs an ee.Geometry describing a MultiLineString. Create a GeoJSON MultiLineString from either a list of points, or an array of lines (each an array of Points). If a list of points is specified, only a single line is created. Args: coords: A list of linestrings. May be a list of coordinates in the GeoJSON 'MultiLineString' format, a list of at least two ee.Geometry objects describing a LineString, or a list of numbers defining a single linestring. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. geodesic: If false, edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. The default is the geodesic state of the inputs, or true if the inputs are numbers. maxError: Max error when input geometry must be reprojected to an explicitly requested result projection or geodesic state. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating geodesic EPSG:4326 MultiLineStrings with a single LineString, given an even number of arguments, e.g., ee.Geometry.MultiLineString(aLng, aLat, bLng, bLat, ...). Returns: An ee.Geometry describing a MultiLineString. """ all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError) + args) return Geometry(Geometry._parseArgs('MultiLineString', 3, all_args)) # pylint: disable=keyword-arg-before-vararg @staticmethod def Polygon( coords=_UNSPECIFIED, proj=_UNSPECIFIED, geodesic=_UNSPECIFIED, maxError=_UNSPECIFIED, # pylint: disable=g-bad-name evenOdd=_UNSPECIFIED, # pylint: disable=g-bad-name *args, ) -> Geometry: """Constructs an ee.Geometry describing a polygon. Args: coords: A list of rings defining the boundaries of the polygon. May be a list of coordinates in the GeoJSON 'Polygon' format, a list of ee.Geometry describing a LinearRing, or a list of numbers defining a single polygon boundary. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. geodesic: If false, edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. The default is the geodesic state of the inputs, or true if the inputs are numbers. maxError: Max error when input geometry must be reprojected to an explicitly requested result projection or geodesic state. evenOdd: If true, polygon interiors will be determined by the even/odd rule, where a point is inside if it crosses an odd number of edges to reach a point at infinity. Otherwise polygons use the left-inside rule, where interiors are on the left side of the shell's edges when walking the vertices in the given order. If unspecified, defaults to True. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating geodesic EPSG:4326 Polygons with a single LinearRing given an even number of arguments, e.g., ee.Geometry.Polygon(aLng, aLat, bLng, bLat, ..., aLng, aLat). Returns: An ee.Geometry describing a polygon. """ all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError, evenOdd) + args) return Geometry(Geometry._parseArgs('Polygon', 3, all_args)) # pylint: disable=keyword-arg-before-vararg @staticmethod def MultiPolygon( coords=_UNSPECIFIED, proj=_UNSPECIFIED, geodesic=_UNSPECIFIED, maxError=_UNSPECIFIED, # pylint: disable=g-bad-name evenOdd=_UNSPECIFIED, # pylint: disable=g-bad-name *args, ) -> Geometry: """Constructs an ee.Geometry describing a MultiPolygon. If created from points, only one polygon can be specified. Args: coords: A list of polygons. May be a list of coordinates in the GeoJSON 'MultiPolygon' format, a list of ee.Geometry objects describing a Polygon, or a list of numbers defining a single polygon boundary. proj: The projection of this geometry. If unspecified, the default is the projection of the input ee.Geometry, or EPSG:4326 if there are no ee.Geometry inputs. geodesic: If false, edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. The default is the geodesic state of the inputs, or true if the inputs are numbers. maxError: Max error when input geometry must be reprojected to an explicitly requested result projection or geodesic state. evenOdd: If true, polygon interiors will be determined by the even/odd rule, where a point is inside if it crosses an odd number of edges to reach a point at infinity. Otherwise polygons use the left-inside rule, where interiors are on the left side of the shell's edges when walking the vertices in the given order. If unspecified, defaults to True. *args: For convenience, varargs may be used when all arguments are numbers. This allows creating geodesic EPSG:4326 MultiPolygons with a single Polygon with a single LinearRing given an even number of arguments, e.g., ee.Geometry.MultiPolygon(aLng, aLat, bLng, bLat, ..., aLng, aLat). Returns: An ee.Geometry describing a MultiPolygon. """ all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError, evenOdd) + args) return Geometry(Geometry._parseArgs('MultiPolygon', 4, all_args)) @_utils.accept_opt_prefix('opt_encoder') def encode(self, encoder: Optional[Any] = None) -> Dict[str, Any]: """Returns a GeoJSON-compatible representation of the geometry.""" if not getattr(self, '_type', None): return super().encode(encoder) result = {'type': self._type} if self._type == 'GeometryCollection': result['geometries'] = self._geometries else: result['coordinates'] = self._coordinates if self._proj is not None: result['crs'] = {'type': 'name', 'properties': {'name': self._proj}} if self._geodesic is not None: result['geodesic'] = self._geodesic if self._evenOdd is not None: result['evenOdd'] = self._evenOdd return result def encode_cloud_value(self, encoder: Any) -> Any: """Returns a server-side invocation of the appropriate constructor.""" if not getattr(self, '_type', None): return super().encode_cloud_value(encoder) return self._computed_equivalent.encode_cloud_value(encoder) def toGeoJSON(self) -> Dict[str, Any]: """Returns a GeoJSON representation of the geometry.""" if self.func: raise ee_exception.EEException( 'Cannot convert a computed geometry to GeoJSON. ' 'Wrap a getInfo() call in json.dumps instead.' ) return self.encode() def toGeoJSONString(self) -> str: """Returns a GeoJSON string representation of the geometry.""" if self.func: raise ee_exception.EEException( 'Cannot convert a computed geometry to GeoJSON. ' 'Wrap a getInfo() call in json.dumps instead.' ) return json.dumps(self.toGeoJSON()) def serialize(self, for_cloud_api=True): """Returns the serialized representation of this object.""" return serializer.toJSON(self, for_cloud_api=for_cloud_api) def __str__(self) -> str: return 'ee.Geometry(%s)' % serializer.toReadableJSON(self) def __repr__(self) -> str: return self.__str__() @staticmethod def _isValidGeometry(geometry: Dict[str, Any]) -> bool: """Check if a geometry looks valid. Args: geometry: The geometry to check. Returns: True if the geometry looks valid. """ if not isinstance(geometry, dict): return False geometry_type = geometry.get('type') if geometry_type == 'GeometryCollection': geometries = geometry.get('geometries') if not isinstance(geometries, (list, tuple)): return False for sub_geometry in geometries: if not Geometry._isValidGeometry(sub_geometry): return False return True else: coords = geometry.get('coordinates') nesting = Geometry._isValidCoordinates(coords) return ((geometry_type == 'Point' and nesting == 1) or (geometry_type == 'MultiPoint' and (nesting == 2 or not coords)) or (geometry_type == 'LineString' and nesting == 2) or (geometry_type == 'LinearRing' and nesting == 2) or (geometry_type == 'MultiLineString' and (nesting == 3 or not coords)) or (geometry_type == 'Polygon' and nesting == 3) or (geometry_type == 'MultiPolygon' and (nesting == 4 or not coords))) @staticmethod def _isValidCoordinates(shape: Union[Sequence[float], Geometry]) -> int: """Validate the coordinates of a geometry. Args: shape: The coordinates to validate. Returns: The number of nested arrays or -1 on error. """ if not isinstance(shape, collections.abc.Iterable): return -1 if (shape and isinstance(shape[0], collections.abc.Iterable) and not isinstance(shape[0], str)): count = Geometry._isValidCoordinates(shape[0]) # If more than 1 ring or polygon, they should have the same nesting. for i in range(1, len(shape)): if Geometry._isValidCoordinates(shape[i]) != count: return -1 return count + 1 else: # Make sure the pts are all numbers. for i in shape: if not isinstance(i, (float, int)): return -1 # Test that we have an even number of pts. if len(shape) % 2 == 0: return 1 else: return -1 @staticmethod def _coordinatesToLine(coordinates: Sequence[float]) -> Any: """Create a line from a list of points. Args: coordinates: The points to convert. Must be list of numbers of even length, in the format [x1, y1, x2, y2, ...] Returns: An array of pairs of points. """ if not (coordinates and isinstance(coordinates[0], (float, int))): return coordinates if len(coordinates) == 2: return coordinates if len(coordinates) % 2 != 0: raise ee_exception.EEException( 'Invalid number of coordinates: %s' % len(coordinates)) line = [] for i in range(0, len(coordinates), 2): pt = [coordinates[i], coordinates[i + 1]] line.append(pt) return line @staticmethod def _parseArgs(ctor_name: str, depth: int, args: Any) -> Dict[str, Any]: """Parses arguments into a GeoJSON dictionary or a ComputedObject. Args: ctor_name: The name of the constructor to use. depth: The nesting depth at which points are found. args: The array of values to test. Returns: If the arguments are simple, a GeoJSON object describing the geometry. Otherwise a ComputedObject calling the appropriate constructor. """ result = {} keys = ['coordinates', 'crs', 'geodesic'] if ctor_name != 'Rectangle': # The constructor for Rectangle does not accept maxError. keys.append('maxError') keys.append('evenOdd') if all(ee_types.isNumber(i) for i in args): # All numbers, so convert them to a true array. result['coordinates'] = args else: # Parse parameters by position. if len(args) > len(keys): raise ee_exception.EEException( 'Geometry constructor given extra arguments.') for key, arg in zip(keys, args): if arg is not None: result[key] = arg # Standardize the coordinates and test if they are simple enough for # client-side initialization. if (Geometry._hasServerValue(result['coordinates']) or result.get('crs') is not None or result.get('geodesic') is not None or result.get('maxError') is not None): # Some arguments cannot be handled in the client, so make a server call. # Note we don't declare a default evenOdd value, so the server can infer # a default based on the projection. server_name = 'GeometryConstructors.' + ctor_name return apifunction.ApiFunction.lookup(server_name).apply(result) else: # Everything can be handled here, so check the depth and init this object. result['type'] = ctor_name result['coordinates'] = Geometry._fixDepth(depth, result['coordinates']) # Enable evenOdd by default for any kind of polygon. if ('evenOdd' not in result and ctor_name in ['Polygon', 'Rectangle', 'MultiPolygon']): result['evenOdd'] = True return result @staticmethod def _hasServerValue(coordinates: Any) -> bool: """Returns whether any of the coordinates are computed values or geometries. Computed items must be resolved by the server (evaluated in the case of computed values, and processed to a single projection and geodesic state in the case of geometries. Args: coordinates: A nested list of ... of number coordinates. Returns: Whether all coordinates are lists or numbers. """ if isinstance(coordinates, (list, tuple)): return any(Geometry._hasServerValue(i) for i in coordinates) else: return isinstance(coordinates, computedobject.ComputedObject) @staticmethod def _fixDepth(depth: int, coords: Any) -> Any: """Fixes the depth of the given coordinates. Checks that each element has the expected depth as all other elements at that depth. Args: depth: The desired depth. coords: The coordinates to fix. Returns: The fixed coordinates, with the deepest elements at the requested depth. Raises: EEException: if the depth is invalid and could not be fixed. """ if depth < 1 or depth > 4: raise ee_exception.EEException('Unexpected nesting level.') # Handle a list of numbers. if all(isinstance(i, (float, int)) for i in coords): coords = Geometry._coordinatesToLine(coords) # Make sure the number of nesting levels is correct. item = coords count = 0 while isinstance(item, (list, tuple)): item = item[0] if item else None count += 1 while count < depth: coords = [coords] count += 1 if Geometry._isValidCoordinates(coords) != depth: raise ee_exception.EEException('Invalid geometry.') # Empty arrays should not be wrapped. item = coords while isinstance(item, (list, tuple)) and len(item) == 1: item = item[0] if isinstance(item, (list, tuple)) and not item: return [] return coords @staticmethod def _GetSpecifiedArgs( args, keywords: Tuple[str, ...] = (), **kwargs ) -> List[Any]: """Returns args, filtering out _UNSPECIFIED and checking for keywords.""" if keywords: args = list(args) for i, keyword in enumerate(keywords): if keyword in kwargs: assert args[i] is _UNSPECIFIED args[i] = kwargs[keyword] return [i for i in args if i != _UNSPECIFIED] @staticmethod def name() -> str: return 'Geometry' def area( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> ee_number.Number: """Returns the area of the geometry. Returns the area of the geometry. Area of points and line strings is 0 and the area of multi geometries is the sum of the areas of their components (intersecting areas are counted multiple times). Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: If specified, the result will be in the units of the coordinate system of this projection. Otherwise it will be in square meters. """ return apifunction.ApiFunction.call_( self.name() + '.area', self, maxError, proj ) def bounds( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the bounding rectangle of the geometry. Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: If specified, the result will be in this projection. Otherwise it will be in EPSG:4326. """ return apifunction.ApiFunction.call_( self.name() + '.bounds', self, maxError, proj ) def buffer( self, distance: _arg_types.Number, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the input buffered by a given distance. If the distance is positive, the geometry is expanded, and if the distance is negative, the geometry is contracted. Args: distance: The distance of the buffering, which may be negative. If no projection is specified, the unit is meters. Otherwise the unit is in the coordinate system of the projection. maxError: The maximum amount of error tolerated when approximating the buffering circle and performing any necessary reprojection. If unspecified, defaults to 1% of the distance. proj: If specified, the buffering will be performed in this projection and the distance will be interpreted as units of the coordinate system of this projection. Otherwise the distance is interpereted as meters and the buffering is performed in a spherical coordinate system. """ return apifunction.ApiFunction.call_( self.name() + '.buffer', self, distance, maxError, proj ) def centroid( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns a point at the center of the highest-dimension components. Lower-dimensional components are ignored, so the centroid of a geometry containing two polygons, three lines and a point is equivalent to the centroid of a geometry containing just the two polygons. Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: If specified, the result will be in this projection. Otherwise it will be in EPSG:4326. """ return apifunction.ApiFunction.call_( self.name() + '.centroid', self, maxError, proj ) def containedIn( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> computedobject.ComputedObject: """Returns true if and only if one geometry is contained in the other. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. Returns: An ee.Boolean. """ return apifunction.ApiFunction.call_( self.name() + '.containedIn', self, right, maxError, proj ) def contains( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> computedobject.ComputedObject: """Returns true if and only if one geometry contains the other. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. Returns: An ee.Boolean. """ return apifunction.ApiFunction.call_( self.name() + '.contains', self, right, maxError, proj ) def convexHull( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the convex hull of the given geometry. The convex hull of a single point is the point itself, the convex hull of collinear points is a line, and the convex hull of everything else is a polygon. Note that a degenerate polygon with all vertices on the same line will result in a line segment. Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. """ return apifunction.ApiFunction.call_( self.name() + '.convexHull', self, maxError, proj ) def coordinates(self) -> ee_list.List: """Returns a GeoJSON-style list of the geometry's coordinates.""" return apifunction.ApiFunction.call_(self.name() + '.coordinates', self) def coveringGrid( self, proj: _arg_types.Projection, scale: Optional[_arg_types.Number] = None, ) -> featurecollection.FeatureCollection: """Returns a collection of features that cover this geometry. Each feature is a rectangle in the grid defined by the given projection. Args: proj: The projection in which to construct the grid. A feature is generated for each grid cell that intersects 'geometry', where cell corners are at integer-valued positions in the projection. If the projection is scaled in meters, the points will be on a grid of that size at the point of true scale. scale: Overrides the scale of the projection, if provided. May be required if the projection isn't already scaled. """ return apifunction.ApiFunction.call_( self.name() + '.coveringGrid', self, proj, scale ) def cutLines( self, distances: _arg_types.List, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns geometries cut into pieces along the given distances. Converts LineString, MultiLineString, and LinearRing geometries into a MultiLineString by cutting them into parts no longer than the given distance along their length. All other geometry types will be converted to an empty MultiLineString. Args: distances: Distances along each LineString to cut the line into separate pieces, measured in units of the given proj, or meters if proj is unspecified. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: Projection of the result and distance measurements, or EPSG:4326 if unspecified. """ return apifunction.ApiFunction.call_( self.name() + '.cutLines', self, distances, maxError, proj ) def difference( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the result of subtracting the 'right' geometry from the geometry. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. """ return apifunction.ApiFunction.call_( self.name() + '.difference', self, right, maxError, proj ) def disjoint( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> computedobject.ComputedObject: """Returns true if and only if the geometries are disjoint. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. Returns: An ee.Boolean. """ return apifunction.ApiFunction.call_( self.name() + '.disjoint', self, right, maxError, proj ) def dissolve( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the union of the geometry. This leaves single geometries untouched, and unions multi geometries. Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: If specified, the union will be performed in this projection. Otherwise it will be performed in a spherical coordinate system. """ return apifunction.ApiFunction.call_( self.name() + '.dissolve', self, maxError, proj ) def distance( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> ee_number.Number: """Returns the minimum distance between two geometries. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. Returns: An ee.Float. """ return apifunction.ApiFunction.call_( self.name() + '.distance', self, right, maxError, proj ) def edgesAreGeodesics(self) -> computedobject.ComputedObject: """Returns true if the edges are geodesics for a spherical earth. Returns true if the geometry edges, if any, are geodesics along a spherical model of the earth; if false, any edges are straight lines in the projection. """ return apifunction.ApiFunction.call_( self.name() + '.edgesAreGeodesics', self ) @staticmethod def fromS2CellId( cellId: _arg_types.Integer, # pylint: disable=invalid-name ) -> Geometry: """Returns the Polygon corresponding to an S2 cell id. Args: cellId: The S2 cell id as 64 bit integer. """ return apifunction.ApiFunction.call_('Geometry.fromS2CellId', cellId) @staticmethod def fromS2CellToken( cellToken: _arg_types.String, # pylint: disable=invalid-name ) -> Geometry: """Returns the Polygon corresponding to an S2 cell id as a hex string. Args: cellToken: The S2 cell id as a hex string. Trailing zeros are required, e.g. the top level face containing Antarctica is 0xb000000000000000. """ return apifunction.ApiFunction.call_('Geometry.fromS2CellToken', cellToken) def geodesic(self) -> computedobject.ComputedObject: """Returns false if edges are straight in the projection. If true, edges are curved to follow the shortest path on the surface of the Earth. """ return apifunction.ApiFunction.call_(self.name() + '.geodesic', self) def geometries(self) -> ee_list.List: """Returns the list of geometries in a GeometryCollection. For single geometries, returns a singleton list of the geometry . """ return apifunction.ApiFunction.call_(self.name() + '.geometries', self) def intersection( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the intersection of the two geometries. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. """ return apifunction.ApiFunction.call_( self.name() + '.intersection', self, right, maxError, proj ) def intersects( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> computedobject.ComputedObject: """Returns true if and only if the geometries intersect. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. Returns: An ee.Boolean. """ return apifunction.ApiFunction.call_( self.name() + '.intersects', self, right, maxError, proj ) def isUnbounded(self) -> computedobject.ComputedObject: """Returns whether the geometry is unbounded.""" return apifunction.ApiFunction.call_(self.name() + '.isUnbounded', self) def length( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> ee_number.Number: """Returns the length of the linear parts of the geometry. Polygonal parts are ignored. The length of multi geometries is the sum of the lengths of their components. Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: If specified, the result will be in the units of the coordinate system of this projection. Otherwise it will be in meters. """ return apifunction.ApiFunction.call_( self.name() + '.length', self, maxError, proj ) def perimeter( self, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> ee_number.Number: """Returns the perimeter length of the polygonal parts of the geometry. The perimeter of multi geometries is the sum of the perimeters of their components. Args: maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: If specified, the result will be in the units of the coordinate system of this projection. Otherwise it will be in meters. """ return apifunction.ApiFunction.call_( self.name() + '.perimeter', self, maxError, proj ) def projection(self) -> projection.Projection: """Returns the projection of the geometry.""" return apifunction.ApiFunction.call_(self.name() + '.projection', self) @staticmethod # pylint: disable-next=invalid-name def s2Cell(cellId: _arg_types.Integer) -> Geometry: """Returns the Polygon corresponding to an S2 cell id. Args: cellId: The S2 cell id as 64 bit integer. """ return apifunction.ApiFunction.call_('Geometry.s2Cell', cellId) def simplify( self, maxError: _arg_types.ErrorMargin, # pylint: disable=invalid-name proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns a simplified geometry to within a given error margin. Note that this does not respect the error margin requested by the consumer of this algorithm, unless maxError is explicitly specified to be null. This overrides the default Earth Engine policy for propagating error margins, so regardless of the geometry accuracy requested from the output, the inputs will be requested with the error margin specified in the arguments to this algorithm. This results in consistent rendering at all zoom levels of a rendered vector map, but at lower zoom levels (i.e. zoomed out), the geometry won't be simplified, which may harm performance. Args: maxError: The maximum amount of error by which the result may differ from the input. proj: If specified, the result will be in this projection. Otherwise it will be in the same projection as the input. If the error margin is in projected units, the margin will be interpreted as units of this projection. """ return apifunction.ApiFunction.call_( self.name() + '.simplify', self, maxError, proj ) def symmetricDifference( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the symmetric difference between two geometries. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. """ return apifunction.ApiFunction.call_( self.name() + '.symmetricDifference', self, right, maxError, proj ) def transform( self, proj: Optional[_arg_types.Projection] = None, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, ) -> Geometry: """Returns the geometry Transformed to a specific projection. Args: proj: The target projection. Defaults to EPSG:4326. If this has a geographic CRS, the edges of the geometry will be interpreted as geodesics. Otherwise they will be interpreted as straight lines in the projection. maxError: The maximum projection error. """ return apifunction.ApiFunction.call_( self.name() + '.transform', self, proj, maxError ) def type(self) -> ee_string.String: """Returns the GeoJSON type of the geometry.""" return apifunction.ApiFunction.call_(self.name() + '.type', self) def union( self, right: _arg_types.Geometry, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> Geometry: """Returns the union of the two geometries. Args: right: The geometry used as the right operand of the operation. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. """ return apifunction.ApiFunction.call_( self.name() + '.union', self, right, maxError, proj ) def withinDistance( self, right: _arg_types.Geometry, distance: _arg_types.Number, # pylint: disable-next=invalid-name maxError: Optional[_arg_types.ErrorMargin] = None, proj: Optional[_arg_types.Projection] = None, ) -> computedobject.ComputedObject: """Returns true if the geometries are within a specified distance. Args: right: The geometry used as the right operand of the operation. distance: The distance threshold. If a projection is specified, the distance is in units of that projected coordinate system, otherwise it is in meters. maxError: The maximum amount of error tolerated when performing any necessary reprojection. proj: The projection in which to perform the operation. If not specified, the operation will be performed in a spherical coordinate system, and linear distances will be in meters on the sphere. Returns: An ee.Boolean. """ return apifunction.ApiFunction.call_( self.name() + '.withinDistance', self, right, distance, maxError, proj )