Source code for eemont.common

import copy
import json
import os
import re
import warnings

import ee
import ee_extra
import ee_extra.Spectral.core
import ee_extra.STAC.core
import ee_extra.STAC.utils
import pkg_resources
import requests
from box import Box
from geopy.geocoders import get_geocoder_for_service

from .extending import extend

warnings.simplefilter("always", UserWarning)


[docs]def indices(online=False): """Gets the dictionary of available indices as a Box object. Parameters ---------- online : boolean Whether to retrieve the most recent list of indices directly from the GitHub repository and not from the local copy. Returns ------- Box Dictionary of available indices. For each index, the keys 'short_name', 'long_name', 'formula', 'bands', 'reference', 'type', 'date_of_addition' and 'contributor' can be checked. See Also -------- listIndices : Gets the list of available indices. Examples -------- >>> import eemont >>> indices = eemont.indices() >>> indices.BAIS2.long_name 'Burned Area Index for Sentinel 2' >>> indices.BAIS2.formula '(1.0 - ((RE2 * RE3 * RE4) / R) ** 0.5) * (((S2 - RE4)/(S2 + RE4) ** 0.5) + 1.0)' >>> indices.BAIS2.reference 'https://doi.org/10.3390/ecrs-2-05177' """ return Box(ee_extra.Spectral.core.indices(online), frozen_box=True)
[docs]def listIndices(online=False): """Gets the list of available indices. Parameters ---------- online : boolean Whether to retrieve the most recent list of indices directly from the GitHub repository and not from the local copy. Returns ------- list List of available indices. See Also -------- indices : Gets the dictionary of available indices as a Box object. Examples -------- >>> import eemont >>> eemont.listIndices() ['BNDVI','CIG','CVI','EVI','EVI2','GBNDVI','GNDVI',...] """ return ee_extra.Spectral.core.listIndices(online)
[docs]@extend(ee) def listDatasets(): """Returns all datasets from the GEE STAC as a list. Returns ------- list List of all datasets from the GEE STAC. Examples -------- >>> import eemont >>> eemont.listDatasets() """ return ee_extra.STAC.core.listDatasets()
# Geocoding # -------------------------- def _retrieve_location(query, geocoder, exactly_one, **kwargs): """Retrieves a location from a query. Parameters ---------- query : str Address, query or structured query to geocode. geocoder : str Geocoder to use. Please visit https://geopy.readthedocs.io/ for more info. exactly_one : boolean Whether to retrieve just one location. **kwargs : Keywords arguments for geolocator.geocode(). The user_agent argument is mandatory (this argument can be set as user_agent = 'my-gee-username' or user_agent = 'my-gee-app-name'). Please visit https://geopy.readthedocs.io/ for more info. Returns ------- Location Retrieved location. """ cls = get_geocoder_for_service(geocoder) geolocator = cls(**kwargs) location = geolocator.geocode(query, exactly_one=exactly_one) if location is None: raise Exception("No matches were found for your query!") else: return location def _lnglat_from_location(location): """Returns the longitude and latitude from a location. Parameters ---------- location : Location Retrieved location. Must be only one location. Returns ------- tuple The longitude and latitude geocoded from the query. """ return [location.longitude, location.latitude] # Plus Codes # -------------------------- def _load_openlocationcode(): """Attempt to load the openlocationcode.openlocationcode module and return it. Because the package is not available through conda-forge, it cannot be made an installation dependency of eemont, so it is only loaded if needed. Returns ------- module The openlocationcode.openlocationcode module. """ try: from openlocationcode import openlocationcode return openlocationcode except ImportError: raise ImportError( 'openlocationcode could not be loaded. Try installing with "pip install openlocationcode".' ) def _convert_lnglat_to_pluscode(lng, lat, code_length): """Take a single longitude and latitude coordinate and convert it to a Plus Code. Parameters ---------- lng : float Longitude. lat : float Latitude. code_length : int The number of significant digits in the output code, between 2 and 15. Shorter codes are less precise. Returns ------- str The Plus Code represented by the coordinate """ olc = _load_openlocationcode() return olc.encode(lat, lng, code_length) def _convert_pluscode_to_lnglat(pluscode, geocoder, **kwargs): """Take a single full or shortened Plus Code and convert it to a longitude and latitude. Parameters ---------- pluscode : str Either a full Plus Code or short Plus Code with a queryable reference location appended to it. geocoder : str Geocoder to use. Please visit https://geopy.readthedocs.io/ for more info. **kwargs : Keywords arguments for geolocator.geocode(). The user_agent argument is mandatory (this argument can be set as user_agent = 'my-gee-username' or user_agent = 'my-gee-app-name'). Please visit https://geopy.readthedocs.io/ for more info. Returns ------- tuple The longitude and latitude of the Plus Code centroid. """ olc = _load_openlocationcode() if not olc.isFull(pluscode): if olc.isShort(pluscode): raise ValueError( 'Short Plus Codes must include a reference location (e.g. "QXGV+XH Denver, CO, USA").' ) shortcode, reference = _parse_code_and_reference_from_pluscode(pluscode) location = _retrieve_location(reference, geocoder, exactly_one=True, **kwargs) ref_lng, ref_lat = _lnglat_from_location(location) pluscode = olc.recoverNearest(shortcode, ref_lat, ref_lng) area = olc.decode(pluscode) return [area.longitudeCenter, area.latitudeCenter] def _parse_code_and_reference_from_pluscode(pluscode): """Split a short Plus Code into a Plus Code and reference using regex. For example, "QXGV+XH Denver, CO, USA" will return ("QXGV+XH", "Denver, CO, USA"). Parameters ---------- pluscode : str A short Plus Code with a queryable reference location appended to it, delimited by whitespace. Returns ------- tuple The short Plus Code and the reference. """ pattern = r"\w{1,8}\+\w{,7}" code = None for chunk in pluscode.split(" "): match = re.search(pattern, chunk) code = match.group(0) if match else code if not code: raise ValueError("Plus code could not be decoded.") reference = pluscode.replace(code, "") return (code, reference) def _is_coordinate_like(x): """Test if an object appears to be a longitude, latitude coordinate. This doesn't test if the coordinate is valid, only that it has the correct data structure: an iterable containing two numbers. Parameters ---------- x : Object Any object that will be tested for coordinate-like structure. Returns ------- bool True if the input object resembles a longitude, latitude coordinate. """ if not isinstance(x, (list, tuple)) or len(x) != 2: return False for element in x: if not isinstance(element, (int, float)): return False return True def _convert_lnglats_to_pluscodes(arr, code_length): """Take an arbitrarily nested array and recursively replace any element that looks like a coordinate with an equivalent Plus Code. Raise a ValueError if any non-coordinate elements are found. Parameters ---------- arr : iterable An arbitrarily nested array containing tuples of longitude, latitude coordinates. code_length : int The number of significant digits in the output code, between 2 and 15. Shorter codes are less precise. Returns ------- iterable An array matching the structure of the input array, with coordinate tuples replaced with Plus Code strings. """ converted = copy.deepcopy(arr) if not isinstance(arr, (list, tuple)): raise ValueError( "{} is not a coordinate or iterable of coordinates.".format(arr) ) if _is_coordinate_like(arr): converted = _convert_lnglat_to_pluscode(arr[0], arr[1], code_length) else: for i, element in enumerate(arr): converted[i] = _convert_lnglats_to_pluscodes(element, code_length) return converted def _convert_pluscodes_to_lnglats(arr, geocoder, **kwargs): """Take an arbitrarily nested array and recursively replace any element that looks like a Plus Code with an equivalent longitude, latitude tuple. Raise a ValueError if any non-Plus Code elements are found. Parameters ---------- arr : iterable An arbitrarily nested array containing Plus Code strings. geocoder : str Geocoder to use. Please visit https://geopy.readthedocs.io/ for more info. **kwargs : Keywords arguments for geolocator.geocode(). The user_agent argument is mandatory (this argument can be set as user_agent = 'my-gee-username' or user_agent = 'my-gee-app-name'). Please visit https://geopy.readthedocs.io/ for more info. Returns ------- iterable An array matching the structure of the input array, with Plus Code strings replaced with coordinate tuples. """ converted = copy.deepcopy(arr) if not isinstance(arr, (list, tuple, str)): raise ValueError("{} is not a Plus Code or iterable of Plus Codes.".format(arr)) if isinstance(arr, str): converted = _convert_pluscode_to_lnglat(arr, geocoder, **kwargs) else: for i, element in enumerate(arr): converted[i] = _convert_pluscodes_to_lnglats(element, geocoder, **kwargs) return converted