mirror of
https://github.com/google/earthengine-api.git
synced 2025-12-08 19:26:12 +00:00
375 lines
11 KiB
Python
375 lines
11 KiB
Python
# Copyright 2012 Google Inc. All Rights Reserved.
|
|
|
|
"""An object representing EE Features."""
|
|
|
|
|
|
|
|
# Using old-style python function naming on purpose to match the
|
|
# javascript version's naming.
|
|
# pylint: disable-msg=C6003,C6409
|
|
|
|
import collections
|
|
import numbers
|
|
|
|
import ee_exception
|
|
import image
|
|
import serializer
|
|
|
|
|
|
class Feature(object):
|
|
"""An object representing EE Features."""
|
|
|
|
def __init__(self, geometry, opt_properties=None):
|
|
"""Create a feature from either a GeoJSON geometry or another feature.
|
|
|
|
Args:
|
|
geometry: The geometry to use, or a pre-constructed Feature, or a JSON
|
|
description of a call that returns a feature.
|
|
opt_properties: A dictionary of metadata properties. If a Feature
|
|
is passed (instead of a geometry) this is unused.
|
|
|
|
Raises:
|
|
EEException: if the given geometry isn't valid.
|
|
|
|
"""
|
|
if isinstance(geometry, Feature):
|
|
if opt_properties is not None:
|
|
raise ee_exception.EEException(
|
|
'Can\'t create Feature out of a Feature and properties.')
|
|
# pylint: disable-msg=protected-access
|
|
self._description = dict(geometry._description)
|
|
# pylint: enable-msg=protected-access
|
|
return
|
|
|
|
if 'algorithm' in geometry or geometry.get('type') == 'Variable':
|
|
if opt_properties:
|
|
self._description = {
|
|
'algorithm': 'Feature',
|
|
'geometry': geometry,
|
|
'metadata': opt_properties
|
|
}
|
|
else:
|
|
self._description = geometry
|
|
else:
|
|
if Feature.isValidGeometry(geometry):
|
|
self._description = {
|
|
'algorithm': 'Feature',
|
|
'geometry': geometry,
|
|
'metadata': opt_properties or {}
|
|
}
|
|
else:
|
|
raise ee_exception.EEException('Invalid geometry.')
|
|
|
|
def __eq__(self, other):
|
|
return self._description == other._description # pylint: disable-msg=W0212
|
|
|
|
def __ne__(self, other):
|
|
return self._description != other._description # pylint: disable-msg=W0212
|
|
|
|
def __str__(self):
|
|
"""Writes out the feature in a human-readable form."""
|
|
return 'Feature(%s)' % serializer.toJSON(self._description)
|
|
|
|
def __repr__(self):
|
|
"""Writes out the feature in an eval-able form."""
|
|
return 'ee.Feature(%s)' % self._description
|
|
|
|
def serialize(self, opt_pretty=True):
|
|
"""Serialize this object into a JSON string.
|
|
|
|
Args:
|
|
opt_pretty: A flag indicating whether to pretty-print the JSON.
|
|
|
|
Returns:
|
|
A JSON represenation of this image.
|
|
"""
|
|
return serializer.toJSON(self._description, opt_pretty)
|
|
|
|
def getMapId(self, vis_params=None):
|
|
"""Fetch and return a map id and token, suitable for use in a Map overlay.
|
|
|
|
Args:
|
|
vis_params: The visualization parameters. Currently only one parameter,
|
|
'color', containing a hex RGB color string is allowed.
|
|
|
|
Returns:
|
|
An object containing a mapid string, an access token, plus a DrawVector
|
|
image wrapping a FeatureCollection containing this feature.
|
|
"""
|
|
painted = image.Image({
|
|
'algorithm': 'DrawVector',
|
|
'collection': {
|
|
'type': 'FeatureCollection',
|
|
'features': [self]
|
|
},
|
|
'color': (vis_params or {}).get('color', '000000')
|
|
})
|
|
return painted.getMapId({})
|
|
|
|
@staticmethod
|
|
def isValidGeometry(geometry):
|
|
"""Check if a geometry looks valid.
|
|
|
|
Args:
|
|
geometry: The geometry to check.
|
|
|
|
Returns:
|
|
True if the geometry looks valid.
|
|
"""
|
|
t = geometry.get('type')
|
|
coords = geometry.get('coordinates')
|
|
return (
|
|
((t == 'Point' and Feature.validateCoordinates(coords) == 1) or
|
|
(t == 'MultiPoint' and Feature.validateCoordinates(coords) == 2) or
|
|
(t == 'LineString' and Feature.validateCoordinates(coords) == 2) or
|
|
(t == 'LinearRing' and Feature.validateCoordinates(coords) == 2) or
|
|
(t == 'MultiLine' and Feature.validateCoordinates(coords) == 3) or
|
|
(t == 'Polygon' and Feature.validateCoordinates(coords) == 3) or
|
|
(t == 'MultiPolygon' and Feature.validateCoordinates(coords) == 4)))
|
|
|
|
@staticmethod
|
|
def validateCoordinates(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 = Feature.validateCoordinates(shape[0])
|
|
# If more than 1 ring or polygon, they should have the same nesting.
|
|
for i in xrange(1, len(shape)):
|
|
if Feature.validateCoordinates(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 = []
|
|
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 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 = Feature.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
|
|
|
|
return geometry
|
|
|
|
@staticmethod
|
|
def Point(lon, lat):
|
|
"""Construct a GeoJSON Point.
|
|
|
|
Args:
|
|
lon: The longitude of the point.
|
|
lat: The latitude of the point.
|
|
|
|
Returns:
|
|
A dictionary representing a GeoJSON Point.
|
|
"""
|
|
return {
|
|
'type': 'Point',
|
|
'coordinates': [lon, lat]
|
|
}
|
|
|
|
@staticmethod
|
|
def MultiPoint(coordinates, *rest):
|
|
"""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 {
|
|
'type': 'MultiPoint',
|
|
'coordinates': Feature._makeGeometry(coordinates, 2, rest)
|
|
}
|
|
|
|
@staticmethod
|
|
def Rectangle(xlo, ylo, xhi, yhi):
|
|
"""Construct a rectangular polygon from the given corner points.
|
|
|
|
Args:
|
|
xlo: The lower left x coordinate.
|
|
ylo: The lower left y coordinate.
|
|
xhi: The upper right x coordinate.
|
|
yhi: The upepr right y coordinate.
|
|
|
|
Returns:
|
|
A dictionary representing a GeoJSON Polygon.
|
|
"""
|
|
return {
|
|
'type': 'Polygon',
|
|
'coordinates': [[[xlo, yhi], [xlo, ylo], [xhi, ylo], [xhi, yhi]]]
|
|
}
|
|
|
|
@staticmethod
|
|
def LineString(coordinates, *rest):
|
|
"""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 {
|
|
'type': 'LineString',
|
|
'coordinates': Feature._makeGeometry(coordinates, 2, rest)
|
|
}
|
|
|
|
@staticmethod
|
|
def LinearRing(coordinates, *rest):
|
|
"""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 {
|
|
'type': 'LinearRing',
|
|
'coordinates': Feature._makeGeometry(coordinates, 2, rest)
|
|
}
|
|
|
|
@staticmethod
|
|
def MultiLine(coordinates, *rest):
|
|
"""Create a GeoJSON MutliLine.
|
|
|
|
Create a GeoJSON MultiLine 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 MultiLine(1, 2, 3, 4, 5, 6).
|
|
|
|
Returns:
|
|
A dictionary representing a GeoJSON MultiLine.
|
|
|
|
TODO(user): This actually doesn't accept an array of
|
|
Feature.LineString, but it should.
|
|
"""
|
|
return {
|
|
'type': 'MultiLine',
|
|
'coordinates': Feature._makeGeometry(coordinates, 3, rest)
|
|
}
|
|
|
|
@staticmethod
|
|
def Polygon(coordinates, *rest):
|
|
"""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
|
|
Feature.LinearRings, but it should.
|
|
"""
|
|
return {
|
|
'type': 'Polygon',
|
|
'coordinates': Feature._makeGeometry(coordinates, 3, rest)
|
|
}
|
|
|
|
@staticmethod
|
|
def MultiPolygon(coordinates, *rest):
|
|
"""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
|
|
Feature.Polygon, but it should.
|
|
"""
|
|
return {
|
|
'type': 'MultiPolygon',
|
|
'coordinates': Feature._makeGeometry(coordinates, 4, rest)
|
|
}
|