geometry.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  1. #!/usr/bin/env python
  2. # coding=utf-8
  3. """An object representing EE Geometries."""
  4. # Using lowercase function naming to match the JavaScript names.
  5. # pylint: disable=g-bad-name
  6. # pylint: disable=g-bad-import-order
  7. import collections.abc
  8. import json
  9. import numbers
  10. from . import apifunction
  11. from . import computedobject
  12. from . import ee_exception
  13. from . import ee_types
  14. from . import serializer
  15. # A sentinel value used to detect unspecified function parameters.
  16. _UNSPECIFIED = object()
  17. class Geometry(computedobject.ComputedObject):
  18. """An Earth Engine geometry."""
  19. _initialized = False
  20. # Tell pytype to not complain about dynamic attributes.
  21. _HAS_DYNAMIC_ATTRIBUTES = True
  22. def __init__(self,
  23. geo_json,
  24. opt_proj=None,
  25. opt_geodesic=None,
  26. opt_evenOdd=None):
  27. """Creates a geometry.
  28. Args:
  29. geo_json: The GeoJSON object describing the geometry or a
  30. computed object to be reinterpred as a Geometry. Supports
  31. CRS specifications as per the GeoJSON spec, but only allows named
  32. (rather than "linked" CRSs). If this includes a 'geodesic' field,
  33. and opt_geodesic is not specified, it will be used as opt_geodesic.
  34. opt_proj: An optional projection specification, either as an
  35. ee.Projection, as a CRS ID code or as a WKT string. If specified,
  36. overrides any CRS found in the geo_json parameter. If unspecified and
  37. the geo_json does not declare a CRS, defaults to "EPSG:4326"
  38. (x=longitude, y=latitude).
  39. opt_geodesic: Whether line segments should be interpreted as spherical
  40. geodesics. If false, indicates that line segments should be
  41. interpreted as planar lines in the specified CRS. If absent,
  42. defaults to true if the CRS is geographic (including the default
  43. EPSG:4326), or to false if the CRS is projected.
  44. opt_evenOdd: If true, polygon interiors will be determined by the even/odd
  45. rule, where a point is inside if it crosses an odd number of edges to
  46. reach a point at infinity. Otherwise polygons use the left-inside
  47. rule, where interiors are on the left side of the shell's edges when
  48. walking the vertices in the given order. If unspecified, defaults to
  49. True.
  50. Raises:
  51. EEException: if the given geometry isn't valid.
  52. """
  53. self.initialize()
  54. computed = (
  55. isinstance(geo_json, computedobject.ComputedObject) and
  56. not (isinstance(geo_json, Geometry) and geo_json._type is not None)) # pylint: disable=protected-access
  57. options = opt_proj or opt_geodesic or opt_evenOdd
  58. if computed:
  59. if options:
  60. raise ee_exception.EEException(
  61. 'Setting the CRS or geodesic on a computed Geometry is not '
  62. 'supported. Use Geometry.transform().')
  63. else:
  64. super(Geometry, self).__init__(geo_json.func, geo_json.args,
  65. geo_json.varName)
  66. return
  67. # Below here we're working with a GeoJSON literal.
  68. if isinstance(geo_json, Geometry):
  69. geo_json = geo_json.encode()
  70. if not Geometry._isValidGeometry(geo_json):
  71. raise ee_exception.EEException('Invalid GeoJSON geometry.')
  72. super(Geometry, self).__init__(None, None)
  73. # The type of the geometry.
  74. self._type = geo_json['type']
  75. # The coordinates of the geometry, up to 4 nested levels with numbers at
  76. # the last level. None iff type is GeometryCollection.
  77. self._coordinates = geo_json.get('coordinates')
  78. # The subgeometries, None unless type is GeometryCollection.
  79. self._geometries = geo_json.get('geometries')
  80. # The projection code (WKT or identifier) of the geometry.
  81. if opt_proj:
  82. self._proj = opt_proj
  83. elif 'crs' in geo_json:
  84. if (isinstance(geo_json.get('crs'), dict) and
  85. geo_json['crs'].get('type') == 'name' and
  86. isinstance(geo_json['crs'].get('properties'), dict) and
  87. isinstance(geo_json['crs']['properties'].get('name'), str)):
  88. self._proj = geo_json['crs']['properties']['name']
  89. else:
  90. raise ee_exception.EEException('Invalid CRS declaration in GeoJSON: ' +
  91. json.dumps(geo_json['crs']))
  92. else:
  93. self._proj = None
  94. # Whether the geometry has spherical geodesic edges.
  95. self._geodesic = opt_geodesic
  96. if opt_geodesic is None and 'geodesic' in geo_json:
  97. self._geodesic = bool(geo_json['geodesic'])
  98. # Whether polygon interiors use the even/odd rule.
  99. self._evenOdd = opt_evenOdd
  100. if opt_evenOdd is None and 'evenOdd' in geo_json:
  101. self._evenOdd = bool(geo_json['evenOdd'])
  102. # Build a proxy for this object that is an invocation of a server-side
  103. # constructor. This is used during Cloud API encoding, but can't be
  104. # constructed at that time: due to id()-based caching in Serializer,
  105. # building transient objects during encoding isn't safe.
  106. ctor_args = {}
  107. if self._type == 'GeometryCollection':
  108. ctor_name = 'MultiGeometry'
  109. ctor_args['geometries'] = [Geometry(g) for g in self._geometries]
  110. else:
  111. ctor_name = self._type
  112. ctor_args['coordinates'] = self._coordinates
  113. if self._proj is not None:
  114. if isinstance(self._proj, str):
  115. ctor_args['crs'] = apifunction.ApiFunction.lookup('Projection').call(
  116. self._proj)
  117. else:
  118. ctor_args['crs'] = self._proj
  119. if self._geodesic is not None:
  120. ctor_args['geodesic'] = self._geodesic
  121. if self._evenOdd is not None:
  122. ctor_args['evenOdd'] = self._evenOdd
  123. self._computed_equivalent = apifunction.ApiFunction.lookup(
  124. 'GeometryConstructors.' + ctor_name).apply(ctor_args)
  125. @classmethod
  126. def initialize(cls):
  127. """Imports API functions to this class."""
  128. if not cls._initialized:
  129. apifunction.ApiFunction.importApi(cls, 'Geometry', 'Geometry')
  130. cls._initialized = True
  131. @classmethod
  132. def reset(cls):
  133. """Removes imported API functions from this class."""
  134. apifunction.ApiFunction.clearApi(cls)
  135. cls._initialized = False
  136. def __getitem__(self, key):
  137. """Allows access to GeoJSON properties for backward-compatibility."""
  138. return self.toGeoJSON()[key]
  139. @staticmethod
  140. def Point(coords=_UNSPECIFIED, proj=_UNSPECIFIED, *args, **kwargs):
  141. """Constructs an ee.Geometry describing a point.
  142. Args:
  143. coords: A list of two [x,y] coordinates in the given projection.
  144. proj: The projection of this geometry, or EPSG:4326 if unspecified.
  145. *args: For convenience, varargs may be used when all arguments are
  146. numbers. This allows creating EPSG:4326 points, e.g.
  147. ee.Geometry.Point(lng, lat).
  148. **kwargs: Keyword args that accept "lon" and "lat" for backward-
  149. compatibility.
  150. Returns:
  151. An ee.Geometry describing a point.
  152. """
  153. init = Geometry._parseArgs(
  154. 'Point', 1,
  155. Geometry._GetSpecifiedArgs((coords, proj) + args, ('lon', 'lat'),
  156. **kwargs))
  157. if not isinstance(init, computedobject.ComputedObject):
  158. xy = init['coordinates']
  159. if not isinstance(xy, (list, tuple)) or len(xy) != 2:
  160. raise ee_exception.EEException(
  161. 'The Geometry.Point constructor requires 2 coordinates.')
  162. return Geometry(init)
  163. @staticmethod
  164. def MultiPoint(coords=_UNSPECIFIED, proj=_UNSPECIFIED, *args):
  165. """Constructs an ee.Geometry describing a MultiPoint.
  166. Args:
  167. coords: A list of points, each in the GeoJSON 'coordinates' format of a
  168. Point, or a list of the x,y coordinates in the given projection, or
  169. an ee.Geometry describing a point.
  170. proj: The projection of this geometry. If unspecified, the default is
  171. the projection of the input ee.Geometry, or EPSG:4326 if there are
  172. no ee.Geometry inputs.
  173. *args: For convenience, varargs may be used when all arguments are
  174. numbers. This allows creating EPSG:4326 MultiPoints given an even
  175. number of arguments, e.g.
  176. ee.Geometry.MultiPoint(aLng, aLat, bLng, bLat, ...).
  177. Returns:
  178. An ee.Geometry describing a MultiPoint.
  179. """
  180. all_args = Geometry._GetSpecifiedArgs((coords, proj) + args)
  181. return Geometry(Geometry._parseArgs('MultiPoint', 2, all_args))
  182. # pylint: disable=keyword-arg-before-vararg
  183. @staticmethod
  184. def Rectangle(coords=_UNSPECIFIED,
  185. proj=_UNSPECIFIED,
  186. geodesic=_UNSPECIFIED,
  187. evenOdd=_UNSPECIFIED,
  188. *args,
  189. **kwargs):
  190. """Constructs an ee.Geometry describing a rectangular polygon.
  191. Args:
  192. coords: The minimum and maximum corners of the rectangle, as a list of
  193. two points each in the format of GeoJSON 'Point' coordinates, or a
  194. list of two ee.Geometry objects describing a point, or a list of four
  195. numbers in the order xMin, yMin, xMax, yMax.
  196. proj: The projection of this geometry. If unspecified, the default is the
  197. projection of the input ee.Geometry, or EPSG:4326 if there are no
  198. ee.Geometry inputs.
  199. geodesic: If false, edges are straight in the projection. If true, edges
  200. are curved to follow the shortest path on the surface of the Earth.
  201. The default is the geodesic state of the inputs, or true if the
  202. inputs are numbers.
  203. evenOdd: If true, polygon interiors will be determined by the even/odd
  204. rule, where a point is inside if it crosses an odd number of edges to
  205. reach a point at infinity. Otherwise polygons use the left-inside
  206. rule, where interiors are on the left side of the shell's edges when
  207. walking the vertices in the given order. If unspecified, defaults to
  208. True.
  209. *args: For convenience, varargs may be used when all arguments are
  210. numbers. This allows creating EPSG:4326 Polygons given exactly four
  211. coordinates, e.g.
  212. ee.Geometry.Rectangle(minLng, minLat, maxLng, maxLat).
  213. **kwargs: Keyword args that accept "xlo", "ylo", "xhi" and "yhi" for
  214. backward-compatibility.
  215. Returns:
  216. An ee.Geometry describing a rectangular polygon.
  217. """
  218. init = Geometry._parseArgs(
  219. 'Rectangle', 2,
  220. Geometry._GetSpecifiedArgs(
  221. (coords, proj, geodesic, evenOdd) + args,
  222. ('xlo', 'ylo', 'xhi', 'yhi'), **kwargs))
  223. if not isinstance(init, computedobject.ComputedObject):
  224. # GeoJSON does not have a Rectangle type, so expand to a Polygon.
  225. xy = init['coordinates']
  226. if not isinstance(xy, (list, tuple)) or len(xy) != 2:
  227. raise ee_exception.EEException(
  228. 'The Geometry.Rectangle constructor requires 2 points or 4 '
  229. 'coordinates.')
  230. x1 = xy[0][0]
  231. y1 = xy[0][1]
  232. x2 = xy[1][0]
  233. y2 = xy[1][1]
  234. init['coordinates'] = [[[x1, y2], [x1, y1], [x2, y1], [x2, y2]]]
  235. init['type'] = 'Polygon'
  236. return Geometry(init)
  237. @staticmethod
  238. def BBox(west, south, east, north):
  239. """Constructs a rectangle ee.Geometry from lines of latitude and longitude.
  240. If (east - west) ≥ 360° then the longitude range will be normalized to -180°
  241. to +180°; otherwise they will be treated as designating points on a circle
  242. (e.g. east may be numerically less than west).
  243. Args:
  244. west: The westernmost enclosed longitude. Will be adjusted to lie in the
  245. range -180° to 180°.
  246. south: The southernmost enclosed latitude. If less than -90° (south pole),
  247. will be treated as -90°.
  248. east: The easternmost enclosed longitude.
  249. north: The northernmost enclosed longitude. If greater than +90° (north
  250. pole), will be treated as +90°.
  251. Returns:
  252. An ee.Geometry describing a planar WGS84 rectangle.
  253. """
  254. # Not using Geometry._parseArgs because that assumes the args should go
  255. # directly into a coordinates field.
  256. if Geometry._hasServerValue((west, south, east, north)):
  257. # Some arguments cannot be handled in the client, so make a server call.
  258. return (apifunction.ApiFunction.lookup('GeometryConstructors.BBox')
  259. .apply(dict(west=west, south=south, east=east, north=north)))
  260. # Else proceed with client-side implementation.
  261. # Reject NaN and positive (west) or negative (east) infinities before they
  262. # become bad JSON. The other two infinities are acceptable because we
  263. # support the general idea of an around-the-globe latitude band. By writing
  264. # them negated, we also reject NaN.
  265. if not west < float('inf'):
  266. raise ee_exception.EEException(
  267. 'Geometry.BBox: west must not be {}'.format(west))
  268. if not east > float('-inf'):
  269. raise ee_exception.EEException(
  270. 'Geometry.BBox: east must not be {}'.format(east))
  271. # Reject cases which, if we clamped them instead, would move a box whose
  272. # bounds lie entirely "past" a pole to being at the pole. By writing them
  273. # negated, we also reject NaN.
  274. if not south <= 90:
  275. raise ee_exception.EEException(
  276. 'Geometry.BBox: south must be at most +90°, but was {}°'.format(
  277. south))
  278. if not north >= -90:
  279. raise ee_exception.EEException(
  280. 'Geometry.BBox: north must be at least -90°, but was {}°'.format(
  281. north))
  282. # On the other hand, allow a box whose extent lies past the pole, but
  283. # canonicalize it to being exactly the pole.
  284. south = max(south, -90)
  285. north = min(north, 90)
  286. if east - west >= 360:
  287. # We conclude from seeing more than 360 degrees that the user intends to
  288. # specify the entire globe (or a band of latitudes, at least).
  289. # Canonicalize to standard global form.
  290. west = -180
  291. east = 180
  292. else:
  293. # Not the entire globe. Canonicalize coordinate ranges.
  294. west = Geometry._canonicalize_longitude(west)
  295. east = Geometry._canonicalize_longitude(east)
  296. if east < west:
  297. east += 360
  298. # GeoJSON does not have a Rectangle type, so expand to a Polygon.
  299. return Geometry(
  300. geo_json={
  301. 'coordinates': [[[west, north],
  302. [west, south],
  303. [east, south],
  304. [east, north]]],
  305. 'type': 'Polygon',
  306. },
  307. opt_geodesic=False)
  308. @staticmethod
  309. def _canonicalize_longitude(longitude):
  310. # Note that Python specifies "The modulo operator always yields a result
  311. # with the same sign as its second operand"; therefore no special handling
  312. # of negative arguments is needed.
  313. longitude = longitude % 360
  314. if longitude > 180:
  315. longitude -= 360
  316. return longitude
  317. # pylint: disable=keyword-arg-before-vararg
  318. @staticmethod
  319. def LineString(coords=_UNSPECIFIED,
  320. proj=_UNSPECIFIED,
  321. geodesic=_UNSPECIFIED,
  322. maxError=_UNSPECIFIED,
  323. *args):
  324. """Constructs an ee.Geometry describing a LineString.
  325. Args:
  326. coords: A list of at least two points. May be a list of coordinates in
  327. the GeoJSON 'LineString' format, a list of at least two ee.Geometry
  328. objects describing a point, or a list of at least four numbers
  329. defining the [x,y] coordinates of at least two points.
  330. proj: The projection of this geometry. If unspecified, the default is the
  331. projection of the input ee.Geometry, or EPSG:4326 if there are no
  332. ee.Geometry inputs.
  333. geodesic: If false, edges are straight in the projection. If true, edges
  334. are curved to follow the shortest path on the surface of the Earth.
  335. The default is the geodesic state of the inputs, or true if the
  336. inputs are numbers.
  337. maxError: Max error when input geometry must be reprojected to an
  338. explicitly requested result projection or geodesic state.
  339. *args: For convenience, varargs may be used when all arguments are
  340. numbers. This allows creating geodesic EPSG:4326 LineStrings given
  341. an even number of arguments, e.g.
  342. ee.Geometry.LineString(aLng, aLat, bLng, bLat, ...).
  343. Returns:
  344. An ee.Geometry describing a LineString.
  345. """
  346. all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError) +
  347. args)
  348. return Geometry(Geometry._parseArgs('LineString', 2, all_args))
  349. # pylint: disable=keyword-arg-before-vararg
  350. @staticmethod
  351. def LinearRing(coords=_UNSPECIFIED,
  352. proj=_UNSPECIFIED,
  353. geodesic=_UNSPECIFIED,
  354. maxError=_UNSPECIFIED,
  355. *args):
  356. """Constructs an ee.Geometry describing a LinearRing.
  357. If the last point is not equal to the first, a duplicate of the first
  358. point will be added at the end.
  359. Args:
  360. coords: A list of points in the ring. May be a list of coordinates in
  361. the GeoJSON 'LinearRing' format, a list of at least three ee.Geometry
  362. objects describing a point, or a list of at least six numbers defining
  363. the [x,y] coordinates of at least three points.
  364. proj: The projection of this geometry. If unspecified, the default is the
  365. projection of the input ee.Geometry, or EPSG:4326 if there are no
  366. ee.Geometry inputs.
  367. geodesic: If false, edges are straight in the projection. If true, edges
  368. are curved to follow the shortest path on the surface of the Earth.
  369. The default is the geodesic state of the inputs, or true if the
  370. inputs are numbers.
  371. maxError: Max error when input geometry must be reprojected to an
  372. explicitly requested result projection or geodesic state.
  373. *args: For convenience, varargs may be used when all arguments are
  374. numbers. This allows creating geodesic EPSG:4326 LinearRings given
  375. an even number of arguments, e.g.
  376. ee.Geometry.LinearRing(aLng, aLat, bLng, bLat, ...).
  377. Returns:
  378. A dictionary representing a GeoJSON LinearRing.
  379. """
  380. all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError) +
  381. args)
  382. return Geometry(Geometry._parseArgs('LinearRing', 2, all_args))
  383. # pylint: disable=keyword-arg-before-vararg
  384. @staticmethod
  385. def MultiLineString(coords=_UNSPECIFIED,
  386. proj=_UNSPECIFIED,
  387. geodesic=_UNSPECIFIED,
  388. maxError=_UNSPECIFIED,
  389. *args):
  390. """Constructs an ee.Geometry describing a MultiLineString.
  391. Create a GeoJSON MultiLineString from either a list of points, or an array
  392. of lines (each an array of Points). If a list of points is specified,
  393. only a single line is created.
  394. Args:
  395. coords: A list of linestrings. May be a list of coordinates in the
  396. GeoJSON 'MultiLineString' format, a list of at least two ee.Geometry
  397. objects describing a LineString, or a list of numbers defining a
  398. single linestring.
  399. proj: The projection of this geometry. If unspecified, the default is the
  400. projection of the input ee.Geometry, or EPSG:4326 if there are no
  401. ee.Geometry inputs.
  402. geodesic: If false, edges are straight in the projection. If true, edges
  403. are curved to follow the shortest path on the surface of the Earth.
  404. The default is the geodesic state of the inputs, or true if the
  405. inputs are numbers.
  406. maxError: Max error when input geometry must be reprojected to an
  407. explicitly requested result projection or geodesic state.
  408. *args: For convenience, varargs may be used when all arguments are
  409. numbers. This allows creating geodesic EPSG:4326 MultiLineStrings
  410. with a single LineString, given an even number of arguments, e.g.
  411. ee.Geometry.MultiLineString(aLng, aLat, bLng, bLat, ...).
  412. Returns:
  413. An ee.Geometry describing a MultiLineString.
  414. """
  415. all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError) +
  416. args)
  417. return Geometry(Geometry._parseArgs('MultiLineString', 3, all_args))
  418. # pylint: disable=keyword-arg-before-vararg
  419. @staticmethod
  420. def Polygon(coords=_UNSPECIFIED,
  421. proj=_UNSPECIFIED,
  422. geodesic=_UNSPECIFIED,
  423. maxError=_UNSPECIFIED,
  424. evenOdd=_UNSPECIFIED,
  425. *args):
  426. """Constructs an ee.Geometry describing a polygon.
  427. Args:
  428. coords: A list of rings defining the boundaries of the polygon. May be a
  429. list of coordinates in the GeoJSON 'Polygon' format, a list of
  430. ee.Geometry describing a LinearRing, or a list of numbers defining a
  431. single polygon boundary.
  432. proj: The projection of this geometry. If unspecified, the default is the
  433. projection of the input ee.Geometry, or EPSG:4326 if there are no
  434. ee.Geometry inputs.
  435. geodesic: If false, edges are straight in the projection. If true, edges
  436. are curved to follow the shortest path on the surface of the Earth.
  437. The default is the geodesic state of the inputs, or true if the
  438. inputs are numbers.
  439. maxError: Max error when input geometry must be reprojected to an
  440. explicitly requested result projection or geodesic state.
  441. evenOdd: If true, polygon interiors will be determined by the even/odd
  442. rule, where a point is inside if it crosses an odd number of edges to
  443. reach a point at infinity. Otherwise polygons use the left-inside
  444. rule, where interiors are on the left side of the shell's edges when
  445. walking the vertices in the given order. If unspecified, defaults to
  446. True.
  447. *args: For convenience, varargs may be used when all arguments are
  448. numbers. This allows creating geodesic EPSG:4326 Polygons with a
  449. single LinearRing given an even number of arguments, e.g.
  450. ee.Geometry.Polygon(aLng, aLat, bLng, bLat, ..., aLng, aLat).
  451. Returns:
  452. An ee.Geometry describing a polygon.
  453. """
  454. all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError,
  455. evenOdd) + args)
  456. return Geometry(Geometry._parseArgs('Polygon', 3, all_args))
  457. # pylint: disable=keyword-arg-before-vararg
  458. @staticmethod
  459. def MultiPolygon(coords=_UNSPECIFIED,
  460. proj=_UNSPECIFIED,
  461. geodesic=_UNSPECIFIED,
  462. maxError=_UNSPECIFIED,
  463. evenOdd=_UNSPECIFIED,
  464. *args):
  465. """Constructs an ee.Geometry describing a MultiPolygon.
  466. If created from points, only one polygon can be specified.
  467. Args:
  468. coords: A list of polygons. May be a list of coordinates in the GeoJSON
  469. 'MultiPolygon' format, a list of ee.Geometry objects describing a
  470. Polygon, or a list of numbers defining a single polygon boundary.
  471. proj: The projection of this geometry. If unspecified, the default is the
  472. projection of the input ee.Geometry, or EPSG:4326 if there are no
  473. ee.Geometry inputs.
  474. geodesic: If false, edges are straight in the projection. If true, edges
  475. are curved to follow the shortest path on the surface of the Earth.
  476. The default is the geodesic state of the inputs, or true if the
  477. inputs are numbers.
  478. maxError: Max error when input geometry must be reprojected to an
  479. explicitly requested result projection or geodesic state.
  480. evenOdd: If true, polygon interiors will be determined by the even/odd
  481. rule, where a point is inside if it crosses an odd number of edges to
  482. reach a point at infinity. Otherwise polygons use the left-inside
  483. rule, where interiors are on the left side of the shell's edges when
  484. walking the vertices in the given order. If unspecified, defaults to
  485. True.
  486. *args: For convenience, varargs may be used when all arguments are
  487. numbers. This allows creating geodesic EPSG:4326 MultiPolygons with
  488. a single Polygon with a single LinearRing given an even number of
  489. arguments, e.g.
  490. ee.Geometry.MultiPolygon(aLng, aLat, bLng, bLat, ..., aLng, aLat).
  491. Returns:
  492. An ee.Geometry describing a MultiPolygon.
  493. """
  494. all_args = Geometry._GetSpecifiedArgs((coords, proj, geodesic, maxError,
  495. evenOdd) + args)
  496. return Geometry(Geometry._parseArgs('MultiPolygon', 4, all_args))
  497. def encode(self, opt_encoder=None):
  498. """Returns a GeoJSON-compatible representation of the geometry."""
  499. if not getattr(self, '_type', None):
  500. return super(Geometry, self).encode(opt_encoder)
  501. result = {'type': self._type}
  502. if self._type == 'GeometryCollection':
  503. result['geometries'] = self._geometries
  504. else:
  505. result['coordinates'] = self._coordinates
  506. if self._proj is not None:
  507. result['crs'] = {'type': 'name', 'properties': {'name': self._proj}}
  508. if self._geodesic is not None:
  509. result['geodesic'] = self._geodesic
  510. if self._evenOdd is not None:
  511. result['evenOdd'] = self._evenOdd
  512. return result
  513. def encode_cloud_value(self, encoder):
  514. """Returns a server-side invocation of the appropriate constructor."""
  515. if not getattr(self, '_type', None):
  516. return super(Geometry, self).encode_cloud_value(encoder)
  517. return self._computed_equivalent.encode_cloud_value(encoder)
  518. def toGeoJSON(self):
  519. """Returns a GeoJSON representation of the geometry."""
  520. if self.func:
  521. raise ee_exception.EEException(
  522. 'Can\'t convert a computed geometry to GeoJSON. '
  523. 'Use getInfo() instead.')
  524. return self.encode()
  525. def toGeoJSONString(self):
  526. """Returns a GeoJSON string representation of the geometry."""
  527. if self.func:
  528. raise ee_exception.EEException(
  529. 'Can\'t convert a computed geometry to GeoJSON. '
  530. 'Use getInfo() instead.')
  531. return json.dumps(self.toGeoJSON())
  532. def serialize(self, for_cloud_api=True):
  533. """Returns the serialized representation of this object."""
  534. return serializer.toJSON(self, for_cloud_api=for_cloud_api)
  535. def __str__(self):
  536. return 'ee.Geometry(%s)' % serializer.toReadableJSON(self)
  537. def __repr__(self):
  538. return self.__str__()
  539. @staticmethod
  540. def _isValidGeometry(geometry):
  541. """Check if a geometry looks valid.
  542. Args:
  543. geometry: The geometry to check.
  544. Returns:
  545. True if the geometry looks valid.
  546. """
  547. if not isinstance(geometry, dict):
  548. return False
  549. geometry_type = geometry.get('type')
  550. if geometry_type == 'GeometryCollection':
  551. geometries = geometry.get('geometries')
  552. if not isinstance(geometries, (list, tuple)):
  553. return False
  554. for sub_geometry in geometries:
  555. if not Geometry._isValidGeometry(sub_geometry):
  556. return False
  557. return True
  558. else:
  559. coords = geometry.get('coordinates')
  560. nesting = Geometry._isValidCoordinates(coords)
  561. return ((geometry_type == 'Point' and nesting == 1) or
  562. (geometry_type == 'MultiPoint' and
  563. (nesting == 2 or not coords)) or
  564. (geometry_type == 'LineString' and nesting == 2) or
  565. (geometry_type == 'LinearRing' and nesting == 2) or
  566. (geometry_type == 'MultiLineString' and
  567. (nesting == 3 or not coords)) or
  568. (geometry_type == 'Polygon' and nesting == 3) or
  569. (geometry_type == 'MultiPolygon' and
  570. (nesting == 4 or not coords)))
  571. @staticmethod
  572. def _isValidCoordinates(shape):
  573. """Validate the coordinates of a geometry.
  574. Args:
  575. shape: The coordinates to validate.
  576. Returns:
  577. The number of nested arrays or -1 on error.
  578. """
  579. if not isinstance(shape, collections.abc.Iterable):
  580. return -1
  581. if (shape and isinstance(shape[0], collections.abc.Iterable) and
  582. not isinstance(shape[0], str)):
  583. count = Geometry._isValidCoordinates(shape[0])
  584. # If more than 1 ring or polygon, they should have the same nesting.
  585. for i in range(1, len(shape)):
  586. if Geometry._isValidCoordinates(shape[i]) != count:
  587. return -1
  588. return count + 1
  589. else:
  590. # Make sure the pts are all numbers.
  591. for i in shape:
  592. if not isinstance(i, numbers.Number):
  593. return -1
  594. # Test that we have an even number of pts.
  595. if len(shape) % 2 == 0:
  596. return 1
  597. else:
  598. return -1
  599. @staticmethod
  600. def _coordinatesToLine(coordinates):
  601. """Create a line from a list of points.
  602. Args:
  603. coordinates: The points to convert. Must be list of numbers of
  604. even length, in the format [x1, y1, x2, y2, ...]
  605. Returns:
  606. An array of pairs of points.
  607. """
  608. if not (coordinates and isinstance(coordinates[0], numbers.Number)):
  609. return coordinates
  610. if len(coordinates) == 2:
  611. return coordinates
  612. if len(coordinates) % 2 != 0:
  613. raise ee_exception.EEException(
  614. 'Invalid number of coordinates: %s' % len(coordinates))
  615. line = []
  616. for i in range(0, len(coordinates), 2):
  617. pt = [coordinates[i], coordinates[i + 1]]
  618. line.append(pt)
  619. return line
  620. @staticmethod
  621. def _parseArgs(ctor_name, depth, args):
  622. """Parses arguments into a GeoJSON dictionary or a ComputedObject.
  623. Args:
  624. ctor_name: The name of the constructor to use.
  625. depth: The nesting depth at which points are found.
  626. args: The array of values to test.
  627. Returns:
  628. If the arguments are simple, a GeoJSON object describing the geometry.
  629. Otherwise a ComputedObject calling the appropriate constructor.
  630. """
  631. result = {}
  632. keys = ['coordinates', 'crs', 'geodesic']
  633. if ctor_name != 'Rectangle':
  634. # The constructor for Rectangle does not accept maxError.
  635. keys.append('maxError')
  636. keys.append('evenOdd')
  637. if all(ee_types.isNumber(i) for i in args):
  638. # All numbers, so convert them to a true array.
  639. result['coordinates'] = args
  640. else:
  641. # Parse parameters by position.
  642. if len(args) > len(keys):
  643. raise ee_exception.EEException(
  644. 'Geometry constructor given extra arguments.')
  645. for key, arg in zip(keys, args):
  646. if arg is not None:
  647. result[key] = arg
  648. # Standardize the coordinates and test if they are simple enough for
  649. # client-side initialization.
  650. if (Geometry._hasServerValue(result['coordinates']) or
  651. result.get('crs') is not None or
  652. result.get('geodesic') is not None or
  653. result.get('maxError') is not None):
  654. # Some arguments cannot be handled in the client, so make a server call.
  655. # Note we don't declare a default evenOdd value, so the server can infer
  656. # a default based on the projection.
  657. server_name = 'GeometryConstructors.' + ctor_name
  658. return apifunction.ApiFunction.lookup(server_name).apply(result)
  659. else:
  660. # Everything can be handled here, so check the depth and init this object.
  661. result['type'] = ctor_name
  662. result['coordinates'] = Geometry._fixDepth(depth, result['coordinates'])
  663. # Enable evenOdd by default for any kind of polygon.
  664. if ('evenOdd' not in result and
  665. ctor_name in ['Polygon', 'Rectangle', 'MultiPolygon']):
  666. result['evenOdd'] = True
  667. return result
  668. @staticmethod
  669. def _hasServerValue(coordinates):
  670. """Returns whether any of the coordinates are computed values or geometries.
  671. Computed items must be resolved by the server (evaluated in the case of
  672. computed values, and processed to a single projection and geodesic state
  673. in the case of geometries.
  674. Args:
  675. coordinates: A nested list of ... of number coordinates.
  676. Returns:
  677. Whether all coordinates are lists or numbers.
  678. """
  679. if isinstance(coordinates, (list, tuple)):
  680. return any(Geometry._hasServerValue(i) for i in coordinates)
  681. else:
  682. return isinstance(coordinates, computedobject.ComputedObject)
  683. @staticmethod
  684. def _fixDepth(depth, coords):
  685. """Fixes the depth of the given coordinates.
  686. Checks that each element has the expected depth as all other elements
  687. at that depth.
  688. Args:
  689. depth: The desired depth.
  690. coords: The coordinates to fix.
  691. Returns:
  692. The fixed coordinates, with the deepest elements at the requested depth.
  693. Raises:
  694. EEException: if the depth is invalid and could not be fixed.
  695. """
  696. if depth < 1 or depth > 4:
  697. raise ee_exception.EEException('Unexpected nesting level.')
  698. # Handle a list of numbers.
  699. if all(isinstance(i, numbers.Number) for i in coords):
  700. coords = Geometry._coordinatesToLine(coords)
  701. # Make sure the number of nesting levels is correct.
  702. item = coords
  703. count = 0
  704. while isinstance(item, (list, tuple)):
  705. item = item[0] if item else None
  706. count += 1
  707. while count < depth:
  708. coords = [coords]
  709. count += 1
  710. if Geometry._isValidCoordinates(coords) != depth:
  711. raise ee_exception.EEException('Invalid geometry.')
  712. # Empty arrays should not be wrapped.
  713. item = coords
  714. while isinstance(item, (list, tuple)) and len(item) == 1:
  715. item = item[0]
  716. if isinstance(item, (list, tuple)) and not item:
  717. return []
  718. return coords
  719. @staticmethod
  720. def _GetSpecifiedArgs(args, keywords=(), **kwargs):
  721. """Returns args, filtering out _UNSPECIFIED and checking for keywords."""
  722. if keywords:
  723. args = list(args)
  724. for i, keyword in enumerate(keywords):
  725. if keyword in kwargs:
  726. assert args[i] is _UNSPECIFIED
  727. args[i] = kwargs[keyword]
  728. return [i for i in args if i != _UNSPECIFIED]
  729. @staticmethod
  730. def name():
  731. return 'Geometry'