"""An object representing EE Geometries.""" # Using lowercase function naming to match the JavaScript names. # pylint: disable=g-bad-name import collections import json import numbers import apifunction import computedobject import ee_exception import serializer class Geometry(computedobject.ComputedObject): """An Earth Engine geometry.""" _initialized = False def __init__(self, geo_json, opt_proj=None, opt_geodesic=None): """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 opt_geodesic is not specified, it will be used as opt_geodesic. opt_proj: An optional projection specification, either 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). opt_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. Raises: EEException: if the given geometry isn't valid. """ self.initialize() computed = (isinstance(geo_json, computedobject.ComputedObject) and not (isinstance(geo_json, Geometry) and geo_json._type is not None)) # pylint: disable=protected-access options = opt_proj or opt_geodesic if computed: if options: raise ee_exception.EEException( 'Setting the CRS or geodesic on a computed Geometry is not ' 'suported. Use Geometry.transform().') else: super(Geometry, self).__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(Geometry, self).__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 iff 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 opt_proj: self._proj = opt_proj elif 'crs' in geo_json: if (isinstance(geo_json.get('crs'), dict) and geo_json['crs'].get('type') == 'name' and isinstance(geo_json['crs'].get('properties'), dict) and isinstance(geo_json['crs']['properties'].get('name'), basestring)): self._proj = geo_json['crs']['properties']['name'] else: raise ee_exception.EEException('Invalid CRS declaration in GeoJSON: ' + json.dumps(geo_json['crs'])) else: self._proj = None # Whether the geometry has spherical geodesic edges. self._geodesic = opt_geodesic if opt_geodesic is None and 'geodesic' in geo_json: self._geodesic = bool(geo_json['geodesic']) @classmethod def initialize(cls): """Imports API functions to this class.""" if not cls._initialized: apifunction.ApiFunction.importApi(cls, 'Geometry', 'Geometry') cls._initialized = True @classmethod def reset(cls): """Removes imported API functions from this class.""" apifunction.ApiFunction.clearApi(cls) cls._initialized = False def __getitem__(self, key): """Allows access to GeoJSON properties for backward-compatibility.""" return self.toGeoJSON()[key] @staticmethod def Point(lon, lat=None): """Construct a GeoJSON Point. Args: lon: The longitude of the point, or a (lon, lat) list/tuple. lat: The latitude of the point. Returns: A dictionary representing a GeoJSON Point. """ if (lat is None and isinstance(lon, (list, tuple)) and len(lon) == 2): lon, lat = lon return Geometry({ 'type': 'Point', 'coordinates': [lon, lat] }) @staticmethod def MultiPoint(*coordinates): """Create a GeoJSON MultiPoint. Args: *coordinates: The coordinates as either an array of [lon, lat] tuples, or literal pairs of coordinate longitudes and latitudes, such as MultiPoint(1, 2, 3, 4). Returns: A dictionary representing a GeoJSON MultiPoint. """ return Geometry({ 'type': 'MultiPoint', 'coordinates': Geometry._makeGeometry( coordinates[0], 2, coordinates[1:]) }) @staticmethod def Rectangle(xlo, ylo, xhi, yhi): """Construct a rectangular polygon from the given corner points. Args: xlo: The minimum X coordinate (e.g. longitude). ylo: The minimum Y coordinate (e.g. latitude). xhi: The maximum X coordinate (e.g. longitude). yhi: The maximum Y coordinate (e.g. latitude). Returns: A dictionary representing a GeoJSON Polygon. """ return Geometry({ 'type': 'Polygon', 'coordinates': [[[xlo, yhi], [xlo, ylo], [xhi, ylo], [xhi, yhi]]] }) @staticmethod def LineString(*coordinates): """Create a GeoJSON LineString. Args: *coordinates: The coordinates as either an array of [lon, lat] tuples, or literal pairs of coordinate longitudes and latitudes, such as LineString(1, 2, 3, 4). Returns: A dictionary representing a GeoJSON LineString. """ return Geometry({ 'type': 'LineString', 'coordinates': Geometry._makeGeometry( coordinates[0], 2, coordinates[1:]) }) @staticmethod def LinearRing(*coordinates): """Construct a LinearRing from the given coordinates. Args: *coordinates: The coordinates as either an array of [lon, lat] tuples, or literal pairs of coordinate longitudes and latitudes, such as LinearRing(1, 2, 3, 4, 5, 6). Returns: A dictionary representing a GeoJSON LinearRing. """ return Geometry({ 'type': 'LinearRing', 'coordinates': Geometry._makeGeometry( coordinates[0], 2, coordinates[1:]) }) @staticmethod def MultiLineString(*coordinates): """Create a GeoJSON 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: *coordinates: The coordinates as either an array of arrays of [lon, lat] tuples, or literal pairs of coordinate longitudes and latitudes, such as MultiLineString(1, 2, 3, 4, 5, 6). Returns: A dictionary representing a GeoJSON MultiLineString. TODO(user): This actually doesn't accept an array of Geometry.LineString, but it should. """ return Geometry({ 'type': 'MultiLineString', 'coordinates': Geometry._makeGeometry( coordinates[0], 3, coordinates[1:]) }) @staticmethod def Polygon(*coordinates): """Create a GeoJSON Polygon. Create a GeoJSON Polygon from either a list of points, or an array of linear rings. If created from points, only an outer ring can be specified. Args: *coordinates: The polygon coordinates as either a var_args list of numbers, or an array of rings, each of which is an array of points. Returns: A dictionary representing a GeoJSON polygon. TODO(user): This actually doesn't accept an array of Geometry.LinearRings, but it should. """ return Geometry({ 'type': 'Polygon', 'coordinates': Geometry._makeGeometry( coordinates[0], 3, coordinates[1:]) }) @staticmethod def MultiPolygon(*coordinates): """Create a GeoJSON MultiPolygon. If created from points, only one polygon can be specified. Args: *coordinates: The multipolygon coordinates either as a var_args list of numbers of an array of polygons. Returns: A dictionary representing a GeoJSON MultiPolygon. TODO(user): This actually doesn't accept an array of Geometry.Polygon, but it should. """ return Geometry({ 'type': 'MultiPolygon', 'coordinates': Geometry._makeGeometry( coordinates[0], 4, coordinates[1:]) }) def encode(self, opt_encoder=None): # pylint: disable=unused-argument """Returns a GeoJSON-compatible representation of the geometry.""" if not getattr(self, '_type', None): return super(Geometry, self).encode(opt_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 return result def toGeoJSON(self): """Returns a GeoJSON representation of the geometry.""" if self.func: raise ee_exception.EEException( 'Can\'t convert a computed geometry to GeoJSON. ' 'Use getInfo() instead.') return self.encode() def toGeoJSONString(self): """Returns a GeoJSON string representation of the geometry.""" if self.func: raise ee_exception.EEException( 'Can\'t convert a computed geometry to GeoJSON. ' 'Use getInfo() instead.') return json.dumps(self.toGeoJSON()) def serialize(self): """Returns the serialized representation of this object.""" return serializer.toJSON(self) def __str__(self): return 'ee.Geometry(%s)' % serializer.toReadableJSON(self) @staticmethod def _isValidGeometry(geometry): """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 (geometry_type == 'LineString' and nesting == 2) or (geometry_type == 'LinearRing' and nesting == 2) or (geometry_type == 'MultiLineString' and nesting == 3) or (geometry_type == 'Polygon' and nesting == 3) or (geometry_type == 'MultiPolygon' and nesting == 4)) @staticmethod def _isValidCoordinates(shape): """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.Iterable): return -1 shape = list(shape) if isinstance(shape[0], collections.Iterable): count = Geometry._isValidCoordinates(shape[0]) # If more than 1 ring or polygon, they should have the same nesting. for i in xrange(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 xrange(0, len(shape)): if not isinstance(shape[i], numbers.Number): 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): """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 isinstance(coordinates[0], numbers.Number): line = [] if len(coordinates) % 2 != 0: raise ee_exception.EEException('Invalid number of coordinates: %s' % len(coordinates)) for i in xrange(0, len(coordinates), 2): pt = [coordinates[i], coordinates[i + 1]] line.append(pt) coordinates = line return coordinates @staticmethod def _makeGeometry(geometry, nesting, opt_coordinates=()): """Check that the given geometry has the specified level of nesting. If the user passed a list of points to one of the Geometry functions, then geometry will not be used and the coordinates in opt_coordinates will be processed instead. This is to allow calls such as: Polygon(1,2,3,4,5,6) and Polygon([[[1,2],[3,4],[5,6]]]) Args: geometry: The geometry to check. nesting: The expected level of array nesting. opt_coordinates: A list of extra coordinates to decode. Returns: The processed geometry. Raises: EEException: if the nesting level of the arrays isn't supported. """ if nesting < 2 or nesting > 4: raise ee_exception.EEException('Unexpected nesting level.') # Handle a list of points. if isinstance(geometry, numbers.Number) and opt_coordinates: coordinates = [geometry] coordinates.extend(opt_coordinates) geometry = Geometry._coordinatesToLine(coordinates) # Make sure the number of nesting levels is correct. item = geometry count = 0 while isinstance(item, list) or isinstance(item, tuple): item = item[0] count += 1 while count < nesting: geometry = [geometry] count += 1 if Geometry._isValidCoordinates(geometry) != nesting: raise ee_exception.EEException('Invalid geometry.') return geometry @staticmethod def name(): return 'Geometry'