Source code for geetools.FeatureCollection

"""Toolbox for the `ee.FeatureCollection` class."""
from __future__ import annotations

from typing import Optional, Union

import ee
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.colors import to_rgba

from geetools.accessors import register_class_accessor
from geetools.types import ee_int, ee_list, ee_str


@register_class_accessor(ee.FeatureCollection, "geetools")
[docs] class FeatureCollectionAccessor: """Toolbox for the `ee.FeatureCollection` class.""" def __init__(self, obj: ee.FeatureCollection): """Initialize the FeatureCollection class.""" self._obj = obj
[docs] def toImage( self, color: Union[ee_str, ee_int] = 0, width: Union[ee_str, ee_int] = "", ) -> ee.Image: """Paint the current FeatureCollection to an Image. It's simply a wrapper on Image.paint() method Args: color: The pixel value to paint into every band of the input image, either as a number which will be used for all features, or the name of a numeric property to take from each feature in the collection. width: Line width, either as a number which will be the line width for all geometries, or the name of a numeric property to take from each feature in the collection. If unspecified, the geometries will be filled instead of outlined. """ params = {"color": color} width == "" or params.update(width=width) return ee.Image().paint(self._obj, **params)
[docs] def addId(self, name: ee_str = "id", start: ee_int = 1) -> ee.FeatureCollection: """Add a unique numeric identifier, starting from parameter ``start``. Returns: The parsed collection with a new id property Example: .. code-block:: python import ee import geetools ee.Initialize() fc = ee.FeatureCollection('FAO/GAUL/2015/level0') fc = fc.geetools.addId() print(fc.first().get('id').getInfo()) """ start, name = ee.Number(start).toInt(), ee.String(name) indexes = ee.List(self._obj.aggregate_array("system:index")) ids = ee.List.sequence(start, start.add(self._obj.size()).subtract(1)) idByIndex = ee.Dictionary.fromLists(indexes, ids) return self._obj.map(lambda f: f.set(name, idByIndex.get(f.get("system:index"))))
[docs] def mergeGeometries(self) -> ee.Geometry: """Merge the geometries the included features. Returns: the dissolved geometry Example: .. code-block:: python import ee import geetools ee.Initialize() fc = ee.FeatureCollection("FAO/GAUL/2015/level0") fc =fc.filter(ee.Filter.inList("ADM0_CODE", [122, 237, 85])) geom = fc.geetools.mergeGeometries() print(geom.getInfo()) """ first = self._obj.first().geometry() union = self._obj.iterate(lambda f, g: f.geometry().union(g), first) return ee.Geometry(union).dissolve()
[docs] def toPolygons(self) -> ee.FeatureCollection: """Drop any geometry that is not a Polygon or a multipolygon. This method is made to avoid errors when performing zonal statistics and/or other surfaces operations. These operations won't work on geometries that are Lines or points. The methods remove these geometry types from GEometryCollections and rremove features that don't have any polygon geometry Returns: The parsed collection with only polygon/MultiPolygon geometries Example: .. code-block:: python import ee import geetools ee.Initialize() point0 = ee.Geometry.Point([0,0], proj="EPSG:4326") point1 = ee.Geometry.Point([0,1], proj="EPSG:4326") poly0 = point0.buffer(1, proj="EPSG:4326") poly1 = point1.buffer(1, proj="EPSG:4326").bounds(proj="EPSG:4326") line = ee.Geometry.LineString([point1, point0], proj="EPSG:4326") multiPoly = ee.Geometry.MultiPolygon([poly0, poly1], proj="EPSG:4326") geometryCol = ee.Algorithms.GeometryConstructors.MultiGeometry([multiPoly, poly0, poly1, point0, line], crs="EPSG:4326", geodesic=True, maxError=1) fc = ee.FeatureCollection([geometryCol]) fc = fc.geetools.toPolygons() print(fc.getInfo()) """ def filterGeom(geom): geom = ee.Geometry(geom) return ee.Algorithms.If(geom.type().compareTo("Polygon"), None, geom) def removeNonPoly(feat): filteredGeoms = feat.geometry().geometries().map(filterGeom, True) proj = feat.geometry().projection() return feat.setGeometry(ee.Geometry.MultiPolygon(filteredGeoms, proj)) return self._obj.map(removeNonPoly)
[docs] def byProperties( self, featureId: ee_str = "system:index", properties: ee_list = [], labels: list = [] ) -> ee.Dictionary: """Get a dictionary with all feature values for each properties. This method is returning a dictionary with all the properties as keys and their values in each feaure as a list. .. code-block:: { "property1": {"feature1": value1, "feature2": value2, ...}, "property2": {"feature1": value1, "feature2": value2, ...}, ... } The output remain server side and can be used to create a client side plot. Args: featureId: The property used to label features. Defaults to "system:index". properties: A list of properties to get the values from. labels: A list of names to replace properties names. Default to the properties names. Returns: A dictionary with all the properties as keys and their values in each feaure as a list. Example: .. code-block:: python import ee, geetools fc = ee.FeatureCollection("FAO/GAUL/2015/level2").limit(10) d = fc.geetools.byProperties(["ADM1_CODE", "ADM2_CODE"]) d.getInfo() """ # get all the id values, they must be string so we are forced to cast them manually # the default casting is broken from Python side: https://issuetracker.google.com/issues/329106322 features = self._obj.aggregate_array(featureId) isString = lambda i: ee.Algorithms.ObjectType(i).compareTo("String").eq(0) # noqa: E731 features = features.map(lambda i: ee.Algorithms.If(isString(i), i, ee.Number(i).format())) # retrieve properties for each feature properties = ee.List(properties) if properties else self._obj.first().propertyNames() properties = properties.remove(featureId) values = properties.map( lambda p: ee.Dictionary.fromLists(features, self._obj.aggregate_array(p)) ) # get the label to use in the dictionary if requested labels = ee.List(labels) if labels else properties return ee.Dictionary.fromLists(labels, values)
[docs] def byFeatures( self, featureId: ee_str = "system:index", properties: ee_list = [], labels: list = [] ) -> ee.Dictionary: """Get a dictionary with all property values for each feature. This method is returning a dictionary with all the feature ids as keys and their properties as a dictionary. .. code-block:: { "feature1": {"property1": value1, "property2": value2, ...}, "feature2": {"property1": value1, "property2": value2, ...}, ... } The output remain server side and can be used to create a client side plot. Args: featureId: The property to use as the feature id. Defaults to "system:index". This property needs to be a string property. properties: A list of properties to get the values from. labels: A list of names to replace properties names. Default to the properties names. Returns: A dictionary with all the feature ids as keys and their properties as a dictionary. Examples: .. code-block:: python import ee, geetools fc = ee.FeatureCollection("FAO/GAUL/2015/level2").limit(10) d = fc.geetools.byFeature(featureId="ADM2_CODE", properties=["ADM0_CODE"]) d.getInfo() """ # compute the properties and their labels props = ee.List(properties) if properties else self._obj.first().propertyNames() props = props.remove(featureId) labels = ee.List(labels) if labels else props # create a function to get the properties of a feature # we need to map the featureCollection into a list as it's not possible to return something else than a # featureCollection mapping a FeatureCollection. very expensive process but we don't have any other choice. fc = self._obj.select(propertySelectors=props, newProperties=props) fc_list = fc.toList(self._obj.size()) values = fc_list.map(lambda f: ee.Feature(f).select(props, labels).toDictionary(labels)) # get all the id values, they must be string so we are forced to cast them manually # the default casting is broken from Python side: https://issuetracker.google.com/issues/329106322 features = self._obj.aggregate_array(featureId) isString = lambda i: ee.Algorithms.ObjectType(i).compareTo("String").eq(0) # noqa: E731 features = features.map(lambda i: ee.Algorithms.If(isString(i), i, ee.Number(i).format())) return ee.Dictionary.fromLists(features, values)
[docs] def plot_by_features( self, type: str = "bar", featureId: str = "system:index", properties: list = [], labels: list = [], colors: list = [], ax: Optional[Axes] = None, **kwargs, ) -> Axes: """Plot the values of a ``ee.FeatureCollection`` by feature. Each feature property selected in properties will be plotted using the ``featureId`` as the x-axis. If no ``properties`` are provided, all properties will be plotted. If no ``featureId`` is provided, the "system:index" property will be used. Args: type: The type of plot to use. Defaults to "bar". can be any type of plot from the python lib `matplotlib.pyplot`. If the one you need is missing open an issue! featureId: The property to use as the x-axis (name the features). Defaults to "system:index". properties: A list of properties to plot. Defaults to all properties. labels: A list of labels to use for plotting the properties. If not provided, the default labels will be used. It needs to match the properties length. colors: A list of colors to use for plotting the properties. If not provided, the default colors from the matplotlib library will be used. ax: The matplotlib axes to use. If not provided, the plot will be send to a new figure. kwargs: Additional arguments from the ``pyplot`` function. Examples: .. code-block:: python import ee, geetools fc = ee.FeatureCollection("FAO/GAUL/2015/level2").limit(10) fc.geetools.plot_by_features(properties=["ADM1_CODE", "ADM2_CODE"]) Note: This function is a client-side function. """ # Get the features and properties props = ee.List(properties) if properties else self._obj.first().propertyNames().getInfo() props = props.remove(featureId) # get the data from server data = self.byProperties(featureId, props, labels).getInfo() return self._plot( type=type, data=data, label_name=featureId, colors=colors, ax=ax, **kwargs )
[docs] def plot_by_properties( self, type: str = "bar", featureId: str = "system:index", properties: ee_list = [], labels: list = [], colors: list = [], ax: Optional[Axes] = None, **kwargs, ) -> Axes: """Plot the values of a FeatureCollection by property. Each features will be represented by a color and each property will be a bar of the bar chart. Args: type: The type of plot to use. Defaults to "bar". can be any type of plot from the python lib `matplotlib.pyplot`. If the one you need is missing open an issue! featureId: The property to use as the y-axis (name the features). Defaults to "system:index". properties: A list of properties to plot. Defaults to all properties. labels: A list of labels to use for plotting the properties. If not provided, the default labels will be used. It needs to match the properties length. colors: A list of colors to use for plotting the properties. If not provided, the default colors from the matplotlib library will be used. ax: The matplotlib axes to use. If not provided, the plot will be send to a new figure. kwargs: Additional arguments from the ``pyplot`` function. Examples: .. code-block:: python import ee, geetools fc = ee.FeatureCollection("FAO/GAUL/2015/level2").limit(10) fc.geetools.plot_by_properties(xProperties=["ADM1_CODE", "ADM2_CODE"]) Note: This function is a client-side function. """ # Get the features and properties fc = self._obj props = ee.List(properties) if properties else fc.first().propertyNames() props = props.remove(featureId) # get the data from server data = self.byFeatures(featureId, props, labels).getInfo() return self._plot( type=type, data=data, label_name=featureId, colors=colors, ax=ax, **kwargs )
[docs] def plot_hist( self, property: ee_str, label: str = "", ax: Optional[Axes] = None, color=None, **kwargs ) -> Axes: """Plot the histogram of a specific property. Args: property: The property to display label: The label to use for the property. If not provided, the property name will be used. ax: The matplotlib axes to use. If not provided, the plot will be send to the current axes (``plt.gca()``) color: The color to use for the plot. If not provided, the default colors from the matplotlib library will be used. kwargs: Additional arguments from the ``pyplot.hist`` function. Examples: .. code-block:: python import ee, geetools normClim = ee.ImageCollection('OREGONSTATE/PRISM/Norm81m').toBands() region = ee.Geometry.Rectangle(-123.41, 40.43, -116.38, 45.14) climSamp = normClim.sample(region, 5000) climSamp.geetools.plot_hist("07_ppt") """ # gather the data from parameters properties, labels = ee.List([property]), ee.List([label]) # get the data from the server data = self.byProperties(properties=properties, labels=labels).getInfo() # define the ax if not provided by the user if ax is None: fig, ax = plt.subplots() # gather the data from the data variable labels = list(data.keys()) if len(labels) != 1: raise ValueError("Pie chart can only be used with one property") kwargs["rwidth"] = kwargs.get("rwidth", 0.9) kwargs["color"] = color or plt.cm.get_cmap("tab10").colors[0] ax.hist(list(data[labels[0]].values()), **kwargs) ax.set_xlabel(labels[0]) ax.set_ylabel("frequency") # customize the layout of the axis ax.grid(axis="y") ax.set_axisbelow(True) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) # make sure the canvas is only rendered once. ax.figure.canvas.draw_idle() return ax
@staticmethod def _plot( type: str, data: dict, label_name: str, colors: list = [], ax: Optional[Axes] = None, **kwargs, ) -> Axes: """Plotting mechanism used in all the plotting functions. It binds the matplotlib capabilities with the data aggregated either by feature or by properties. the shape of the data should as follows: .. code-block:: { "label1": {"properties1": value1, "properties2": value2, ...} "label2": {"properties1": value1, "properties2": value2, ...}, ... } Args: type: The type of plot to use. can be any type of plot from the python lib `matplotlib.pyplot`. If the one you need is missing open an issue! data: the data to use as inputs of the graph. please follow the fomrmat specified in the documentation. label_name: The name of the property that was used to generate the labels property_names: The list of names that was used to name the values. They will be used to order the keys of the data dictionary. colors: A list of colors to use for the plot. If not provided, the default colors from the matplotlib library will be used. ax: The matplotlib axes to use. If not provided, the plot will be send to a new figure. kwargs: Additional arguments from the ``pyplot`` chat type selected. """ # define the ax if not provided by the user if ax is None: fig, ax = plt.subplots() # gather the data from parameters labels = list(data.keys()) props = list(data[labels[0]].keys()) colors = colors if colors else plt.cm.get_cmap("tab10").colors # draw the chart based on the type if type == "plot": for i, label in enumerate(labels): kwargs["color"] = colors[i] name = props[0] if len(props) == 1 else "Properties values" values = list(data[label].values()) ax.plot(props, values, label=label, **kwargs) ax.set_ylabel(name) ax.set_xlabel(f"Features (labeled by {label_name})") elif type == "scatter": for i, label in enumerate(labels): kwargs["color"] = colors[i] name = props[0] if len(props) == 1 else "Properties values" values = list(data[label].values()) ax.scatter(props, values, label=label, **kwargs) ax.set_ylabel(name) ax.set_xlabel(f"Features (labeled by {label_name})") elif type == "fill_between": for i, label in enumerate(labels): kwargs["facecolor"] = to_rgba(colors[i], 0.2) kwargs["edgecolor"] = to_rgba(colors[i], 1) name = props[0] if len(props) == 1 else "Properties values" values = list(data[label].values()) ax.fill_between(props, values, label=label, **kwargs) ax.set_ylabel(name) ax.set_xlabel(f"Features (labeled by {label_name})") elif type == "bar": x = np.arange(len(props)) width = 1 / (len(labels) + 0.8) margin = width / 10 kwargs["width"] = width - margin ax.set_xticks(x + width * len(labels) / 2, props) for i, label in enumerate(labels): kwargs["color"] = colors[i] values = list(data[label].values()) ax.bar(x + width * i, values, label=label, **kwargs) elif type == "stacked": x = np.arange(len(props)) bottom = np.zeros(len(props)) for i, label in enumerate(labels): kwargs.update(color=colors[i], bottom=bottom) values = list(data[label].values()) ax.bar(x, values, label=label, **kwargs) bottom += values elif type == "pie": if len(labels) != 1: raise ValueError("Pie chart can only be used with one property") kwargs["autopct"] = kwargs.get("autopct", "%1.1f%%") kwargs["normalize"] = kwargs.get("normalize", True) kwargs["labeldistance"] = kwargs.get("labeldistance", None) kwargs["wedgeprops"] = kwargs.get("wedgeprops", {"edgecolor": "w"}) kwargs["textprops"] = kwargs.get("textprops", {"color": "w"}) kwargs.update(autopct="%1.1f%%", colors=colors) values = [data[labels[0]][p] for p in props] ax.pie(values, labels=props, **kwargs) elif type == "donut": if len(labels) != 1: raise ValueError("Pie chart can only be used with one property") kwargs["autopct"] = kwargs.get("autopct", "%1.1f%%") kwargs["normalize"] = kwargs.get("normalize", True) kwargs["labeldistance"] = kwargs.get("labeldistance", None) kwargs["wedgeprops"] = kwargs.get("wedgeprops", {"width": 0.6, "edgecolor": "w"}) kwargs["textprops"] = kwargs.get("textprops", {"color": "w"}) kwargs["pctdistance"] = kwargs.get("pctdistance", 0.7) kwargs.update(autopct="%1.1f%%", colors=colors) values = [data[labels[0]][p] for p in props] ax.pie(values, labels=props, **kwargs) else: raise ValueError(f"Type {type} is not (yet?) supported") # customize the layout of the axis ax.grid(axis="y") ax.set_axisbelow(True) ax.spines["top"].set_visible(False) ax.spines["right"].set_visible(False) ax.legend(bbox_to_anchor=(1.02, 1), loc="upper left") # make sure the canvas is only rendered once. ax.figure.canvas.draw_idle() return ax