├── ipygee ├── _version.py ├── docs.py ├── tabs │ ├── __init__.py │ ├── __pycache__ │ │ ├── layers.cpython-35.pyc │ │ └── __init__.cpython-35.pyc │ └── layers.py ├── __pycache__ │ ├── map.cpython-35.pyc │ ├── assets.cpython-35.pyc │ ├── tasks.cpython-35.pyc │ ├── utils.cpython-35.pyc │ ├── __init__.cpython-35.pyc │ ├── _version.cpython-35.pyc │ ├── maptools.cpython-35.pyc │ ├── threading.cpython-35.pyc │ ├── widgets.cpython-35.pyc │ └── dispatcher.cpython-35.pyc ├── __init__.py ├── threading.py ├── eprint.py ├── preview.py ├── utils.py ├── assets.py ├── widgets.py ├── dispatcher.py ├── maptools.py ├── chart.py ├── tasks.py └── map.py ├── .gitignore ├── README.md ├── LICENSE ├── examples ├── TaskManager.ipynb ├── AssetManager.ipynb ├── eprint.ipynb ├── Map.ipynb └── Chart.ipynb └── setup.py /ipygee/_version.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | __version__ = '0.0.18' 4 | -------------------------------------------------------------------------------- /ipygee/docs.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Google Earth Engine Documentation Widget """ -------------------------------------------------------------------------------- /ipygee/tabs/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Tab Widgets for the interactive Map """ 4 | 5 | -------------------------------------------------------------------------------- /ipygee/__pycache__/map.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/map.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/assets.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/assets.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/tasks.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/tasks.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/utils.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/utils.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/_version.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/_version.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/maptools.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/maptools.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/threading.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/threading.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/widgets.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/widgets.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/__pycache__/dispatcher.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/__pycache__/dispatcher.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/tabs/__pycache__/layers.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/tabs/__pycache__/layers.cpython-35.pyc -------------------------------------------------------------------------------- /ipygee/tabs/__pycache__/__init__.cpython-35.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fitoprincipe/ipygee/master/ipygee/tabs/__pycache__/__init__.cpython-35.pyc -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEA IDE 2 | .idea/ 3 | 4 | # any checkpoint 5 | **/.ipynb_checkpoints 6 | 7 | # Environments 8 | .env 9 | 10 | # Distribution / packaging 11 | /build 12 | /dist/ 13 | *.egg-info/ 14 | 15 | # Unit test / coverage reports 16 | .cache 17 | 18 | # Cache 19 | ipygee/__pycache__/ 20 | ipygee/tabs/__pycache__/ 21 | -------------------------------------------------------------------------------- /ipygee/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ A set of tools for working with Google Earth Engine Python API in Jupyter 4 | notebooks """ 5 | 6 | from ._version import __version__ 7 | from .map import Map 8 | from .assets import AssetManager 9 | from .tasks import TaskManager 10 | from . import chart, preview 11 | from .eprint import eprint, set_eprint_async, getInfo 12 | from .preview import set_preview_async 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IpyGEE 2 | 3 | A set of tools and Widgets for working with **Google Earth Engine** in Jupyter notebooks and Jupyter Lab 4 | 5 | ## Install 6 | 7 | > pip install ipygee 8 | 9 | *Note: Installation **does not** install Earth Engine Python API. You have to install it before installing `ipygee`, see: https://developers.google.com/earth-engine/python_install* 10 | 11 | Be sure notebook extensions are enabled 12 | 13 | ipywidgets: 14 | > jupyter nbextension enable --py widgetsnbextension 15 | 16 | ipyleaflet: 17 | > jupyter nbextension enable --py --sys-prefix ipyleaflet 18 | 19 | ## Main Widgets 20 | 21 | ### - Map 22 | 23 | ``` python 24 | from ipygee import * 25 | Map = Map() 26 | Map.show() 27 | ``` 28 | 29 | ### - AssetManager 30 | ``` python 31 | from ipygee import * 32 | AM = AssetManager() 33 | AM 34 | ``` 35 | 36 | ### - TaskManager 37 | ``` python 38 | from ipygee import * 39 | TM = TaskManager() 40 | TM 41 | ``` 42 | 43 | See examples in [here](https://github.com/fitoprincipe/ipygee/tree/master/examples) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Rodrigo E. Principe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /examples/TaskManager.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ee\n", 10 | "ee.Initialize()" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from ipygee import *" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "# Task Manager" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "TM = TaskManager()" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": { 42 | "scrolled": false 43 | }, 44 | "outputs": [], 45 | "source": [ 46 | "TM" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "## Methods" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "TM.get_selected_taskid()" 63 | ] 64 | } 65 | ], 66 | "metadata": { 67 | "kernelspec": { 68 | "display_name": "Python 3", 69 | "language": "python", 70 | "name": "python3" 71 | }, 72 | "language_info": { 73 | "codemirror_mode": { 74 | "name": "ipython", 75 | "version": 3 76 | }, 77 | "file_extension": ".py", 78 | "mimetype": "text/x-python", 79 | "name": "python", 80 | "nbconvert_exporter": "python", 81 | "pygments_lexer": "ipython3", 82 | "version": "3.5.2" 83 | } 84 | }, 85 | "nbformat": 4, 86 | "nbformat_minor": 2 87 | } 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from setuptools import setup, find_packages 5 | 6 | here = os.path.dirname(os.path.abspath(__file__)) 7 | 8 | # Utility function to read the README file. 9 | # Used for the long_description. It's nice, because now 1) we have a top level 10 | # README file and 2) it's easier to type in the README file than to put a raw 11 | # string in below ... 12 | def read(fname): 13 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 14 | 15 | version_ns = {} 16 | with open(os.path.join(here, 'ipygee', '_version.py')) as f: 17 | exec(f.read(), {}, version_ns) 18 | 19 | # the setup 20 | setup( 21 | name='ipygee', 22 | version=version_ns['__version__'], 23 | description='A set of tools for working with Google Earth Engine Python API in Jupyter notebooks', 24 | long_description=read('README.md'), 25 | url='', 26 | author='Rodrigo E. Principe', 27 | author_email='fitoprincipe82@gmail.com', 28 | license='MIT', 29 | keywords='google earth engine raster image processing gis satelite jupyter notebook ipython', 30 | packages=find_packages(exclude=('docs', 'js')), 31 | include_package_data=True, 32 | install_requires=['ipyleaflet>=0.10.2', 33 | 'pygal', 34 | 'pandas', 35 | 'geetools'], 36 | extras_require={ 37 | 'dev': [], 38 | 'docs': [], 39 | 'testing': [], 40 | }, 41 | classifiers=['Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 2.7', 43 | 'Programming Language :: Python :: 3', 44 | 'Programming Language :: Python :: 3.3', 45 | 'Programming Language :: Python :: 3.4', 46 | 'Programming Language :: Python :: 3.5', 47 | 'License :: OSI Approved :: MIT License',], 48 | ) 49 | -------------------------------------------------------------------------------- /ipygee/threading.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Multithreading hack to add the ability to stop a thread 4 | from http://tomerfiliba.com/recipes/Thread2/ 5 | """ 6 | 7 | import threading 8 | import inspect 9 | import ctypes 10 | 11 | 12 | def _async_raise(tid, exctype): 13 | """raises the exception, performs cleanup if needed""" 14 | if not inspect.isclass(exctype): 15 | raise TypeError("Only types can be raised (not instances)") 16 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), ctypes.py_object(exctype)) 17 | if res == 0: 18 | raise ValueError("invalid thread id") 19 | elif res != 1: 20 | # """if it returns a number greater than one, you're in trouble, 21 | # and you should call it again with exc=NULL to revert the effect""" 22 | ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, 0) 23 | raise SystemError("PyThreadState_SetAsyncExc failed") 24 | 25 | 26 | class Thread(threading.Thread): 27 | def _get_my_tid(self): 28 | """determines this (self's) thread id""" 29 | if not self.isAlive(): 30 | raise threading.ThreadError("the thread is not active") 31 | 32 | # do we have it cached? 33 | if hasattr(self, "_thread_id"): 34 | return self._thread_id 35 | 36 | # no, look for it in the _active dict 37 | for tid, tobj in threading._active.items(): 38 | if tobj is self: 39 | self._thread_id = tid 40 | return tid 41 | 42 | raise AssertionError("could not determine the thread's id") 43 | 44 | def raise_exc(self, exctype): 45 | """raises the given exception type in the context of this thread""" 46 | _async_raise(self._get_my_tid(), exctype) 47 | 48 | def terminate(self): 49 | """raises SystemExit in the context of the given thread, which should 50 | cause the thread to exit silently (unless caught)""" 51 | self.raise_exc(SystemExit) 52 | -------------------------------------------------------------------------------- /examples/AssetManager.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ee\n", 10 | "ee.Initialize()" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from ipygee import *" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Asset Manager" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "AM = AssetManager()" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": { 42 | "scrolled": true 43 | }, 44 | "outputs": [], 45 | "source": [ 46 | "AM" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "## Methods" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "### Get selected assets" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "AM.get_selected()" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [] 78 | } 79 | ], 80 | "metadata": { 81 | "kernelspec": { 82 | "display_name": "Python 3", 83 | "language": "python", 84 | "name": "python3" 85 | }, 86 | "language_info": { 87 | "codemirror_mode": { 88 | "name": "ipython", 89 | "version": 3 90 | }, 91 | "file_extension": ".py", 92 | "mimetype": "text/x-python", 93 | "name": "python", 94 | "nbconvert_exporter": "python", 95 | "pygments_lexer": "ipython3", 96 | "version": "3.5.2" 97 | } 98 | }, 99 | "nbformat": 4, 100 | "nbformat_minor": 2 101 | } 102 | -------------------------------------------------------------------------------- /ipygee/eprint.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Print a EE Object in the Jupyter environment """ 4 | 5 | from ipywidgets import * 6 | from . import dispatcher, threading 7 | from IPython.display import display 8 | 9 | CONFIG = {'do_async': True} 10 | 11 | 12 | def worker(obj, container): 13 | """ The worker to work in a Thread or not """ 14 | eewidget = dispatcher.dispatch(obj) 15 | dispatcher.set_container(container, eewidget) 16 | 17 | 18 | def process_object(obj, do_async): 19 | """ Process one object for printing """ 20 | if isinstance(obj, (str, int, float)): 21 | return Label(str(obj)) 22 | else: 23 | if do_async: 24 | container, button = dispatcher.create_container(True) 25 | thread = threading.Thread(target=worker, args=(obj, container)) 26 | button.on_click(lambda but: dispatcher.cancel(thread, container)) 27 | thread.start() 28 | else: 29 | container, _ = dispatcher.create_container(False) 30 | worker(obj, container) 31 | return container 32 | 33 | 34 | def getInfo(obj, do_async=None): 35 | """ Get Information Widget for the parsed EE object """ 36 | if do_async is None: 37 | do_async = CONFIG.get('do_async') 38 | 39 | return process_object(obj, do_async) 40 | 41 | 42 | def eprint(*objs, do_async=None, container=None): 43 | """ Print EE Objects. Similar to `print(object.getInfo())` but returns a 44 | widget for Jupyter notebooks 45 | 46 | :param eeobject: object to print 47 | :type eeobject: ee.ComputedObject 48 | :param container: any container widget 49 | (see https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20List.html#Container/Layout-widgets) 50 | :type container: ipywidget.Widget 51 | """ 52 | if container is None: 53 | container = VBox() 54 | 55 | children = [] 56 | for obj in objs: 57 | widget = getInfo(obj, do_async) 58 | children.append(widget) 59 | container.children = children 60 | 61 | display(container) 62 | 63 | 64 | def set_eprint_async(do_async): 65 | """ Set the global async for eprint """ 66 | CONFIG['do_async'] = do_async -------------------------------------------------------------------------------- /ipygee/preview.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Preview widget """ 4 | 5 | import requests 6 | from geetools import tools 7 | import base64 8 | from ipywidgets import HTML, Label, Accordion 9 | import threading 10 | from time import time 11 | from . import utils 12 | 13 | CONFIG = {'do_async': True} 14 | 15 | 16 | def image(image, region=None, visualization=None, name=None, 17 | dimensions=(500, 500), do_async=None): 18 | """ Preview an Earth Engine Image """ 19 | if do_async is None: 20 | do_async = CONFIG.get('do_async') 21 | 22 | start = time() 23 | 24 | if name: 25 | label = '{} (Image)'.format(name) 26 | loading = 'Loading {} preview...'.format(name) 27 | else: 28 | label = 'Image Preview' 29 | loading = 'Loading preview...' 30 | 31 | formatdimension = "x".join([str(d) for d in dimensions]) 32 | 33 | wid = Accordion([Label(loading)]) 34 | wid.set_title(0, loading) 35 | 36 | def compute(image, region, visualization): 37 | if not region: 38 | region = tools.geometry.getRegion(image) 39 | else: 40 | region = tools.geometry.getRegion(region) 41 | params = dict(dimensions=formatdimension, region=region) 42 | if visualization: 43 | params.update(visualization) 44 | url = image.getThumbURL(params) 45 | req = requests.get(url) 46 | content = req.content 47 | rtype = req.headers['Content-type'] 48 | if rtype in ['image/jpeg', 'image/png']: 49 | img64 = base64.b64encode(content).decode('utf-8') 50 | src = ''.format(img64) 51 | result = HTML(src) 52 | else: 53 | result = Label(content.decode('utf-8')) 54 | 55 | return result 56 | 57 | def setAccordion(acc): 58 | widget = compute(image, region, visualization) 59 | end = time() 60 | elapsed = end-start 61 | acc.children = [widget] 62 | elapsed = utils.format_elapsed(elapsed) 63 | acc.set_title(0, '{} [{}]'.format(label, elapsed)) 64 | 65 | if do_async: 66 | thread = threading.Thread(target=setAccordion, args=(wid,)) 67 | thread.start() 68 | else: 69 | setAccordion(wid) 70 | 71 | return wid 72 | 73 | 74 | def set_preview_async(do_async): 75 | """ Set the global async for eprint """ 76 | CONFIG['do_async'] = do_async -------------------------------------------------------------------------------- /ipygee/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Util functions """ 4 | 5 | from ipywidgets import * 6 | from .dispatcher import dispatch 7 | import datetime 8 | 9 | 10 | def create_accordion(dictionary): 11 | """ Create an Accordion output from a dict object """ 12 | widlist = [] 13 | ini = 0 14 | widget = Accordion() 15 | widget.selected_index = None # this will unselect all 16 | for key, val in dictionary.items(): 17 | if isinstance(val, dict): 18 | newwidget = create_accordion(val) 19 | widlist.append(newwidget) 20 | elif isinstance(val, list): 21 | # tranform list to a dictionary 22 | dictval = {k: v for k, v in enumerate(val)} 23 | newwidget = create_accordion(dictval) 24 | widlist.append(newwidget) 25 | else: 26 | value = HTML(str(val)) 27 | widlist.append(value) 28 | widget.set_title(ini, key) 29 | ini += 1 30 | widget.children = widlist 31 | return widget 32 | 33 | 34 | def get_datetime(timestamp): 35 | return datetime.datetime.fromtimestamp(float(timestamp)/1000) 36 | 37 | 38 | def format_timestamp(timestamp): 39 | """ Format a POSIX timestamp given in milliseconds """ 40 | dt = get_datetime(timestamp) 41 | return dt.strftime('%Y-%m-%d %H:%M:%S') 42 | 43 | 44 | def format_elapsed(seconds): 45 | if seconds < 60: 46 | return '{}s'.format(int(seconds)) 47 | elif seconds < 3600: 48 | minutes = seconds/60 49 | seconds = (minutes-int(minutes))*60 50 | return '{}m {}s'.format(int(minutes), int(seconds)) 51 | elif seconds < 86400: 52 | hours = seconds/3600 53 | minutes = (hours-int(hours))*60 54 | seconds = (minutes-int(minutes))*60 55 | return '{}h {}m {}s'.format(int(hours), int(minutes), int(seconds)) 56 | else: 57 | days = seconds/86400 58 | hours = (days-int(days))*24 59 | minutes = (hours-int(hours))*60 60 | seconds = (minutes-int(minutes))*60 61 | return '{}d {}h {}m {}s'.format(int(days), int(hours), 62 | int(minutes), int(seconds)) 63 | 64 | 65 | def create_object_output(object): 66 | """ Create a output Widget for Images, Geometries and Features """ 67 | 68 | ty = object.__class__.__name__ 69 | 70 | if ty == 'Image': 71 | return dispatch(object).widget 72 | elif ty == 'FeatureCollection': 73 | try: 74 | info = object.getInfo() 75 | except: 76 | print('FeatureCollection limited to 4000 features') 77 | info = object.limit(4000) 78 | 79 | return create_accordion(info) 80 | else: 81 | info = object.getInfo() 82 | return create_accordion(info) 83 | 84 | 85 | def create_async_output(object, widget): 86 | try: 87 | child = create_object_output(object) 88 | except Exception as e: 89 | child = HTML('There has been an error: {}'.format(str(e))) 90 | 91 | widget.children = [child] 92 | 93 | -------------------------------------------------------------------------------- /examples/eprint.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ee\n", 10 | "ee.Initialize()" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from ipygee import *" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Some EE objects" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 3, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "i = ee.Image.constant(0).rename('test').set('test_prop', 1)\n", 36 | "ic = ee.ImageCollection.fromImages([i])\n", 37 | "g = ee.Geometry.Point([-73, -43])\n", 38 | "f = ee.Feature(g, {'test': 1})\n", 39 | "fc = ee.FeatureCollection([f])\n", 40 | "d = ee.Date('2019-05-21')\n", 41 | "dr = ee.DateRange('2010-01-01', '2019-05-21')" 42 | ] 43 | }, 44 | { 45 | "cell_type": "markdown", 46 | "metadata": {}, 47 | "source": [ 48 | "## Print\n", 49 | "This is a work in progress. Many EE object types have not a dispatch method yet.\n", 50 | "\n", 51 | "Arguments:\n", 52 | "\n", 53 | "You can place as many positional arguments as you want and they will be printed.\n", 54 | "\n", 55 | "### ASYNC\n", 56 | "`eprint` is a pre-made instance of the `Eprint` class. This class has property named `ASYNC` that can be set to `True` or `False`. Also, you can make another instance of `Eprint` for different behavior." 57 | ] 58 | }, 59 | { 60 | "cell_type": "code", 61 | "execution_count": 99, 62 | "metadata": {}, 63 | "outputs": [], 64 | "source": [ 65 | "# eprint = Eprint() # this object is created when importing ipygee, so you can use it directly" 66 | ] 67 | }, 68 | { 69 | "cell_type": "code", 70 | "execution_count": 4, 71 | "metadata": {}, 72 | "outputs": [], 73 | "source": [ 74 | "eprint.ASYNC = True" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 5, 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "data": { 84 | "application/vnd.jupyter.widget-view+json": { 85 | "model_id": "0a78a60d956841cbb3dbb21bad853883", 86 | "version_major": 2, 87 | "version_minor": 0 88 | }, 89 | "text/plain": [ 90 | "VBox(children=(Accordion(children=(Output(),)),))" 91 | ] 92 | }, 93 | "metadata": {}, 94 | "output_type": "display_data" 95 | } 96 | ], 97 | "source": [ 98 | "eprint(d)" 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": 11, 104 | "metadata": {}, 105 | "outputs": [ 106 | { 107 | "data": { 108 | "application/vnd.jupyter.widget-view+json": { 109 | "model_id": "2d41e3f42b114827ad8c77cbd4b8f750", 110 | "version_major": 2, 111 | "version_minor": 0 112 | }, 113 | "text/plain": [ 114 | "VBox(children=(Accordion(children=(Output(),)),))" 115 | ] 116 | }, 117 | "metadata": {}, 118 | "output_type": "display_data" 119 | } 120 | ], 121 | "source": [ 122 | "eprint(dr)" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 6, 128 | "metadata": {}, 129 | "outputs": [ 130 | { 131 | "data": { 132 | "application/vnd.jupyter.widget-view+json": { 133 | "model_id": "7031f04eed99439285063d587c006a79", 134 | "version_major": 2, 135 | "version_minor": 0 136 | }, 137 | "text/plain": [ 138 | "VBox(children=(Label(value='My image'), Accordion(children=(Output(),))))" 139 | ] 140 | }, 141 | "metadata": {}, 142 | "output_type": "display_data" 143 | } 144 | ], 145 | "source": [ 146 | "eprint('My image', i)" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 7, 152 | "metadata": {}, 153 | "outputs": [ 154 | { 155 | "data": { 156 | "application/vnd.jupyter.widget-view+json": { 157 | "model_id": "e86c8038f3d143da8ae04501b8efd2aa", 158 | "version_major": 2, 159 | "version_minor": 0 160 | }, 161 | "text/plain": [ 162 | "VBox(children=(Accordion(children=(Output(),)), Accordion(children=(Output(),))))" 163 | ] 164 | }, 165 | "metadata": {}, 166 | "output_type": "display_data" 167 | } 168 | ], 169 | "source": [ 170 | "eprint(f, ic)" 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": 8, 176 | "metadata": {}, 177 | "outputs": [ 178 | { 179 | "data": { 180 | "application/vnd.jupyter.widget-view+json": { 181 | "model_id": "8d549aff58934a12984d254cc0a29978", 182 | "version_major": 2, 183 | "version_minor": 0 184 | }, 185 | "text/plain": [ 186 | "VBox(children=(Accordion(children=(Output(),)),))" 187 | ] 188 | }, 189 | "metadata": {}, 190 | "output_type": "display_data" 191 | } 192 | ], 193 | "source": [ 194 | "eprint(g)" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": 9, 200 | "metadata": {}, 201 | "outputs": [ 202 | { 203 | "data": { 204 | "application/vnd.jupyter.widget-view+json": { 205 | "model_id": "08a648964ba448f3a27c1d9f2424aca1", 206 | "version_major": 2, 207 | "version_minor": 0 208 | }, 209 | "text/plain": [ 210 | "VBox(children=(Accordion(children=(Output(),)),))" 211 | ] 212 | }, 213 | "metadata": {}, 214 | "output_type": "display_data" 215 | } 216 | ], 217 | "source": [ 218 | "eprint(f)" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 10, 224 | "metadata": {}, 225 | "outputs": [ 226 | { 227 | "data": { 228 | "application/vnd.jupyter.widget-view+json": { 229 | "model_id": "2b9d3a2ea44f450da14c0e7780385242", 230 | "version_major": 2, 231 | "version_minor": 0 232 | }, 233 | "text/plain": [ 234 | "VBox(children=(Accordion(children=(Output(),)),))" 235 | ] 236 | }, 237 | "metadata": {}, 238 | "output_type": "display_data" 239 | } 240 | ], 241 | "source": [ 242 | "eprint(fc)" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [] 251 | } 252 | ], 253 | "metadata": { 254 | "kernelspec": { 255 | "display_name": "Python 3", 256 | "language": "python", 257 | "name": "python3" 258 | }, 259 | "language_info": { 260 | "codemirror_mode": { 261 | "name": "ipython", 262 | "version": 3 263 | }, 264 | "file_extension": ".py", 265 | "mimetype": "text/x-python", 266 | "name": "python", 267 | "nbconvert_exporter": "python", 268 | "pygments_lexer": "ipython3", 269 | "version": "3.5.2" 270 | } 271 | }, 272 | "nbformat": 4, 273 | "nbformat_minor": 2 274 | } 275 | -------------------------------------------------------------------------------- /ipygee/assets.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Google Earth Engine Asset Manager """ 4 | 5 | from ipywidgets import * 6 | import ee 7 | from .threading import Thread 8 | from geetools import batch 9 | from .widgets import * 10 | from . import utils 11 | from . import dispatcher 12 | 13 | 14 | class AssetManager(VBox): 15 | """ Asset Manager Widget """ 16 | POOL_SIZE = 5 17 | 18 | def __init__(self, map=None, **kwargs): 19 | super(AssetManager, self).__init__(**kwargs) 20 | # Thumb height 21 | self.thumb_height = kwargs.get('thumb_height', 300) 22 | self.root_path = ee.data.getAssetRoots()[0]['id'] 23 | 24 | # Map 25 | self.map = map 26 | 27 | # Header 28 | self.user_label = HTML('User: {}'.format(self.root_path)) 29 | self.reload_button = Button(description='Reload') 30 | self.add2map = Button(description='Add to Map') 31 | self.delete = Button(description='Delete Selected') 32 | header_children = [self.reload_button, self.delete] 33 | 34 | # Add2map only if a Map has been passed 35 | if self.map: 36 | header_children.append(self.add2map) 37 | 38 | self.header = HBox(header_children) 39 | 40 | # Reload handler 41 | def reload_handler(button): 42 | new_accordion = self.core(self.root_path) 43 | # Set VBox children 44 | self.children = [self.header, new_accordion] 45 | 46 | # add2map handler 47 | def add2map_handler(themap): 48 | def wrap(button): 49 | selected_rows = self.get_selected() 50 | for asset, ty in selected_rows.items(): 51 | if ty == 'Image': 52 | im = ee.Image(asset) 53 | themap.addLayer(im, {}, asset) 54 | elif ty == 'ImageCollection': 55 | col = ee.ImageCollection(asset) 56 | themap.addLayer(col) 57 | return wrap 58 | 59 | # Set reload handler 60 | # self.reload_button.on_click(reload_handler) 61 | self.reload_button.on_click(self.reload) 62 | 63 | # Set reload handler 64 | self.add2map.on_click(add2map_handler(self.map)) 65 | 66 | # Set delete selected handler 67 | self.delete.on_click(self.delete_selected) 68 | 69 | # First Accordion 70 | self.root_acc = self.core(self.root_path) 71 | 72 | # Set VBox children 73 | self.children = [self.user_label, self.header, self.root_acc] 74 | 75 | def delete_selected(self, button=None): 76 | """ function to delete selected assets """ 77 | selected = self.get_selected() 78 | 79 | # Output widget 80 | output = HTML('') 81 | 82 | def handle_yes(button): 83 | self.children = [self.header, output] 84 | # pool = pp.ProcessPool(self.POOL_SIZE) 85 | if selected: 86 | assets = [ass for ass in selected.keys()] 87 | for assetid in assets: 88 | thread = Thread(target=batch.utils.recrusiveDeleteAsset, 89 | args=(assetid,)) 90 | thread.start() 91 | 92 | # when deleting end, reload 93 | self.reload() 94 | 95 | def handle_no(button): 96 | self.reload() 97 | def handle_cancel(button): 98 | self.reload() 99 | 100 | assets_str = ['{} ({})'.format(ass, ty) for ass, ty in selected.items()] 101 | assets_str = '
'.join(assets_str) 102 | confirm = ConfirmationWidget('

Delete {} assets

'.format(len(selected.keys())), 103 | 'The following assets are going to be deleted:
{}
Are you sure?'.format(assets_str), 104 | handle_yes=handle_yes, 105 | handle_no=handle_no, 106 | handle_cancel=handle_cancel) 107 | 108 | self.children = [self.header, confirm, output] 109 | 110 | def reload(self, button=None): 111 | new_accordion = self.core(self.root_path) 112 | # Set VBox children 113 | self.children = [self.header, new_accordion] 114 | 115 | def get_selected(self): 116 | """ get the selected assets 117 | 118 | :return: a dictionary with the type as key and asset root as value 119 | :rtype: dict 120 | """ 121 | def wrap(checkacc, assets={}, root=self.root_path): 122 | children = checkacc.children # list of CheckRow 123 | for child in children: 124 | checkbox = child.children[0] # checkbox of the CheckRow 125 | widget = child.children[1] # widget of the CheckRow (Accordion) 126 | state = checkbox.value 127 | 128 | if isinstance(widget.children[0], CheckAccordion): 129 | title = widget.get_title(0).split(' ')[0] 130 | new_root = '{}/{}'.format(root, title) 131 | newselection = wrap(widget.children[0], assets, new_root) 132 | assets = newselection 133 | else: 134 | if state: 135 | title = child.children[1].get_title(0) 136 | # remove type that is between () 137 | ass = title.split(' ')[0] 138 | ty = title.split(' ')[1][1:-1] 139 | # append root 140 | ass = '{}/{}'.format(root, ass) 141 | # append title to selected list 142 | # assets.append(title) 143 | assets[ass] = ty 144 | 145 | return assets 146 | 147 | # get selection on root 148 | begin = self.children[2] # CheckAccordion of root 149 | return wrap(begin) 150 | 151 | def core(self, path): 152 | # Get Assets data 153 | 154 | root_list = ee.data.getList({'id': path}) 155 | 156 | # empty lists to fill with ids, types, widgets and paths 157 | ids = [] 158 | types = [] 159 | widgets = [] 160 | paths = [] 161 | 162 | # iterate over the list of the root 163 | for content in root_list: 164 | # get data 165 | id = content['id'] 166 | ty = content['type'] 167 | # append data to lists 168 | paths.append(id) 169 | ids.append(id.replace(path+'/', '')) 170 | types.append(ty) 171 | wid = HTML('Loading..') 172 | widgets.append(wid) 173 | 174 | # super(AssetManager, self).__init__(widgets=widgets, **kwargs) 175 | # self.widgets = widgets 176 | asset_acc = CheckAccordion(widgets=widgets) 177 | 178 | # set titles 179 | for i, (title, ty) in enumerate(zip(ids, types)): 180 | final_title = '{title} ({type})'.format(title=title, type=ty) 181 | asset_acc.set_title(i, final_title) 182 | 183 | def handle_new_accordion(change): 184 | path = change['path'] 185 | index = change['index'] 186 | ty = change['type'] 187 | if ty == 'Folder' or ty == 'ImageCollection': 188 | wid = self.core(path) 189 | else: 190 | if ty == 'Image': 191 | obj = ee.Image(path) 192 | else: 193 | obj = ee.FeatureCollection(path) 194 | 195 | try: 196 | wid = dispatcher.dispatch(obj).widget 197 | except Exception as e: 198 | message = str(e) 199 | wid = HTML(message) 200 | 201 | asset_acc.set_widget(index, wid) 202 | 203 | def handle_checkbox(change): 204 | path = change['path'] 205 | widget = change['widget'] # Accordion 206 | wid_children = widget.children[0] # can be a HTML or CheckAccordion 207 | new = change['new'] 208 | 209 | if isinstance(wid_children, CheckAccordion): # set all checkboxes to True 210 | for child in wid_children.children: 211 | check = child.children[0] 212 | check.value = new 213 | 214 | # set handlers 215 | for i, (path, ty) in enumerate(zip(paths, types)): 216 | asset_acc.set_accordion_handler( 217 | i, handle_new_accordion, 218 | extra_params={'path':path, 'index':i, 'type': ty} 219 | ) 220 | asset_acc.set_checkbox_handler( 221 | i, handle_checkbox, 222 | extra_params={'path':path, 'index':i, 'type': ty} 223 | ) 224 | 225 | return asset_acc -------------------------------------------------------------------------------- /ipygee/widgets.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Generic Custom Widgets to use in this module """ 4 | 5 | from ipywidgets import * 6 | from traitlets import * 7 | 8 | 9 | class CheckRow(HBox): 10 | checkbox = Instance(Checkbox) 11 | widget = Instance(Widget) 12 | 13 | def __init__(self, widget, **kwargs): 14 | self.checkbox = Checkbox(indent=False, 15 | layout=Layout(flex='1 1 20', width='auto')) 16 | self.widget = widget 17 | super(CheckRow, self).__init__(children=(self.checkbox, self.widget), 18 | **kwargs) 19 | self.layout = Layout(display='flex', flex_flow='row', 20 | align_content='flex-start') 21 | 22 | @observe('widget') 23 | def _ob_wid(self, change): 24 | new = change['new'] 25 | self.children = (self.checkbox, new) 26 | 27 | def observe_checkbox(self, handler, extra_params={}, **kwargs): 28 | """ set handler for the checkbox widget. Use the property 'widget' of 29 | change to get the corresponding widget 30 | 31 | :param handler: callback function 32 | :type handler: function 33 | :param extra_params: extra parameters that can be passed to the handler 34 | :type extra_params: dict 35 | :param kwargs: parameters from traitlets.observe 36 | :type kwargs: dict 37 | """ 38 | # by default only observe value 39 | name = kwargs.get('names', 'value') 40 | 41 | def proxy_handler(handler): 42 | def wrap(change): 43 | change['widget'] = self.widget 44 | for key, val in extra_params.items(): 45 | change[key] = val 46 | return handler(change) 47 | return wrap 48 | self.checkbox.observe(proxy_handler(handler), names=name, **kwargs) 49 | 50 | def observe_widget(self, handler, extra_params={}, **kwargs): 51 | """ set handler for the widget alongside de checkbox 52 | 53 | :param handler: callback function 54 | :type handler: function 55 | :param extra_params: extra parameters that can be passed to the handler 56 | :type extra_params: dict 57 | :param kwargs: parameters from traitlets.observe 58 | :type kwargs: dict 59 | """ 60 | def proxy_handler(handler): 61 | def wrap(change): 62 | change['checkbox'] = self.checkbox 63 | for key, val in extra_params.items(): 64 | change[key] = val 65 | return handler(change) 66 | return wrap 67 | self.widget.observe(proxy_handler(handler), **kwargs) 68 | 69 | 70 | class CheckAccordion(VBox): 71 | # widgets = Tuple() 72 | widgets = List() 73 | 74 | def __init__(self, widgets, **kwargs): 75 | # self.widgets = widgets 76 | super(CheckAccordion, self).__init__(**kwargs) 77 | self.widgets = widgets 78 | 79 | @observe('widgets') 80 | def _on_child(self, change): 81 | new = change['new'] # list of any widget 82 | newwidgets = [] 83 | for widget in new: 84 | # constract the widget 85 | acc = Accordion(children=(widget,)) 86 | acc.selected_index = None # this will unselect all 87 | # create a CheckRow 88 | checkrow = CheckRow(acc) 89 | newwidgets.append(checkrow) 90 | newchildren = tuple(newwidgets) 91 | self.children = newchildren 92 | 93 | def set_title(self, index, title): 94 | """ set the title of the widget at indicated index""" 95 | checkrow = self.children[index] 96 | acc = checkrow.widget 97 | acc.set_title(0, title) 98 | 99 | def set_titles(self, titles): 100 | """ set the titles for all children, `titles` size must match 101 | `children` size """ 102 | for i, title in enumerate(titles): 103 | self.set_title(i, title) 104 | 105 | def get_title(self, index): 106 | """ get the title of the widget at indicated index""" 107 | checkrow = self.children[index] 108 | acc = checkrow.widget 109 | return acc.get_title(0) 110 | 111 | def get_check(self, index): 112 | """ get the state of checkbox in index """ 113 | checkrow = self.children[index] 114 | return checkrow.checkbox.value 115 | 116 | def set_check(self, index, state): 117 | """ set the state of checkbox in index """ 118 | checkrow = self.children[index] 119 | checkrow.checkbox.value = state 120 | 121 | def checked_rows(self): 122 | """ return a list of indexes of checked rows """ 123 | checked = [] 124 | for i, checkrow in enumerate(self.children): 125 | state = checkrow.checkbox.value 126 | if state: checked.append(i) 127 | return checked 128 | 129 | def get_widget(self, index): 130 | """ get the widget in index """ 131 | checkrow = self.children[index] 132 | return checkrow.widget 133 | 134 | def set_widget(self, index, widget): 135 | """ set the widget for index """ 136 | checkrow = self.children[index] 137 | checkrow.widget.children = (widget,) # Accordion has 1 child 138 | 139 | def set_row(self, index, title, widget): 140 | """ set values for the row """ 141 | self.set_title(index, title) 142 | self.set_widget(index, widget) 143 | 144 | def set_accordion_handler(self, index, handler, **kwargs): 145 | """ set the handler for Accordion in index """ 146 | checkrow = self.children[index] 147 | checkrow.observe_widget(handler, names=['selected_index'], **kwargs) 148 | 149 | def set_checkbox_handler(self, index, handler, **kwargs): 150 | """ set the handler for CheckBox in index """ 151 | checkrow = self.children[index] 152 | checkrow.observe_checkbox(handler, **kwargs) 153 | 154 | 155 | class ConfirmationWidget(VBox): 156 | def __init__(self, title='Confirmation', legend='Are you sure?', 157 | handle_yes=None, handle_no=None, handle_cancel=None, **kwargs): 158 | super(ConfirmationWidget, self).__init__(**kwargs) 159 | # Title Widget 160 | self.title = title 161 | self.title_widget = HTML(self.title) 162 | # Legend Widget 163 | self.legend = legend 164 | self.legend_widget = HTML(self.legend) 165 | # Buttons 166 | self.yes = Button(description='Yes') 167 | handler_yes = handle_yes if handle_yes else lambda x: x 168 | self.yes.on_click(handler_yes) 169 | 170 | self.no = Button(description='No') 171 | handler_no = handle_no if handle_no else lambda x: x 172 | self.no.on_click(handler_no) 173 | 174 | self.cancel = Button(description='Cancel') 175 | handler_cancel = handle_cancel if handle_cancel else lambda x: x 176 | self.cancel.on_click(handler_cancel) 177 | 178 | self.buttons = HBox([self.yes, self.no, self.cancel]) 179 | 180 | self.children = [self.title_widget, self.legend_widget, self.buttons] 181 | 182 | 183 | class RealBox(Box): 184 | """ Real Box Layout 185 | 186 | items: 187 | [[widget1, widget2], 188 | [widget3, widget4]] 189 | 190 | """ 191 | items = List() 192 | width = Int() 193 | border_inside = Unicode() 194 | border_outside = Unicode() 195 | 196 | def __init__(self, **kwargs): 197 | super(RealBox, self).__init__(**kwargs) 198 | 199 | self.layout = Layout(display='flex', flex_flow='column', 200 | border=self.border_outside) 201 | 202 | def max_row_elements(self): 203 | maxn = 0 204 | for el in self.items: 205 | n = len(el) 206 | if n>maxn: 207 | maxn = n 208 | return maxn 209 | 210 | @observe('items') 211 | def _ob_items(self, change): 212 | layout_columns = Layout(display='flex', flex_flow='row') 213 | new = change['new'] 214 | children = [] 215 | # recompute size 216 | maxn = self.max_row_elements() 217 | width = 100/maxn 218 | for el in new: 219 | for wid in el: 220 | if not wid.layout.width: 221 | if self.width: 222 | wid.layout = Layout(width='{}px'.format(self.width), 223 | border=self.border_inside) 224 | else: 225 | wid.layout = Layout(width='{}%'.format(width), 226 | border=self.border_inside) 227 | hbox = Box(el, layout=layout_columns) 228 | children.append(hbox) 229 | self.children = children 230 | 231 | 232 | class ErrorAccordion(Accordion): 233 | def __init__(self, error, traceback, **kwargs): 234 | super(ErrorAccordion, self).__init__(**kwargs) 235 | self.error = '{}'.format(error).replace('<','{').replace('>','}') 236 | 237 | newtraceback = '' 238 | for trace in traceback[1:]: 239 | newtraceback += '{}'.format(trace).replace('<','{').replace('>','}') 240 | newtraceback += '
' 241 | 242 | self.traceback = newtraceback 243 | 244 | self.errorWid = HTML(self.error) 245 | 246 | self.traceWid = HTML(self.traceback) 247 | 248 | self.children = (self.errorWid, self.traceWid) 249 | self.set_title(0, 'ERROR') 250 | self.set_title(1, 'TRACEBACK') -------------------------------------------------------------------------------- /ipygee/dispatcher.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Dispatch methods for different EE Object types 4 | 5 | The dispatcher functions must take as single argument the information return 6 | by ee.ComputedObject.getInfo(), process it and return an ipywidget 7 | """ 8 | 9 | import ee 10 | from ipywidgets import * 11 | from . import utils 12 | from geetools.ui.dispatcher import belongToEE 13 | from time import time 14 | import sys 15 | import traceback 16 | from .widgets import ErrorAccordion 17 | from datetime import datetime 18 | 19 | DISPATCHERS = dict() 20 | 21 | 22 | # HELPERS 23 | def order(d): 24 | """ Order a dict and return a list of [key, val] """ 25 | results = [] 26 | if isinstance(d, dict): 27 | keys = list(d.keys()) 28 | keys.sort() 29 | for k in keys: 30 | results.append([str(k), d[k]]) 31 | else: 32 | for i, val in enumerate(d): 33 | results.append([str(i), val]) 34 | return results 35 | 36 | 37 | def isurl(value): 38 | """ Determine if the parsed string is a url """ 39 | url = False 40 | if isinstance(value, str): 41 | if value[:4] == 'http' or value[:3] == 'www': 42 | url = True 43 | return url 44 | 45 | 46 | def new_line(val, n): 47 | newval = '' 48 | for i, v in enumerate(val): 49 | if i%n == 0 and i != 0: 50 | newval += '
'+v 51 | else: 52 | newval += v 53 | return newval 54 | 55 | 56 | def create_container(thread=False): 57 | """ Create the structure for each object. Returns an Accordion """ 58 | if thread: 59 | cancel_button = Button(description='Cancel') 60 | acc = Accordion([cancel_button]) 61 | acc.set_title(0, 'Loading...') 62 | else: 63 | acc = Accordion([Output()]) 64 | acc.set_title(0, 'Loading...') 65 | cancel_button = None 66 | return acc, cancel_button 67 | 68 | 69 | def set_container(container, eewidget): 70 | # format elapsed 71 | elapsed = utils.format_elapsed(eewidget.processing_time) 72 | if isinstance(container, (Accordion, Tab)): 73 | if eewidget.local_type != eewidget.server_type: 74 | title = '{} (local) / {} (server) [{}]'.format( 75 | eewidget.local_type, eewidget.server_type, elapsed) 76 | else: 77 | title = '{} [{}]'.format(eewidget.server_type, elapsed) 78 | 79 | container.set_title(0, title) 80 | container.children = [eewidget.widget] 81 | container.selected_index = None 82 | 83 | 84 | def cancel(thread, acc): 85 | """ Cancel a thread and set the title of the first element of accordion 86 | as CANCELLED """ 87 | if thread.isAlive(): 88 | thread.terminate() 89 | 90 | while True: 91 | if not thread.isAlive(): 92 | acc.set_title(0, 'CANCELLED') 93 | break 94 | 95 | 96 | def register(*names): 97 | """ Register dispatchers """ 98 | def wrap(func): 99 | for name in names: 100 | DISPATCHERS[name] = func 101 | def wrap2(info): 102 | return func(info) 103 | return wrap2 104 | return wrap 105 | 106 | 107 | class EEWidget(object): 108 | """ A simple class to hold the widget for dispatching and the type 109 | retrieved from the server """ 110 | def __init__(self, widget, server_type, local_type, processing_time): 111 | self.widget = widget 112 | self.server_type = server_type 113 | self.local_type = local_type 114 | self.processing_time = processing_time 115 | 116 | 117 | # GENERAL DISPATCHER 118 | def dispatch(obj): 119 | """ General dispatcher """ 120 | local_type = obj.__class__.__name__ 121 | 122 | start = time() 123 | 124 | try: 125 | # Create Widget 126 | if belongToEE(obj): 127 | info = obj.getInfo() 128 | try: 129 | obj_type = info['type'] 130 | except: 131 | obj_type = local_type 132 | 133 | if obj_type in DISPATCHERS.keys(): 134 | widget = DISPATCHERS[obj_type](info) 135 | else: 136 | if isinstance(info, (dict,)): 137 | widget = utils.create_accordion(info) 138 | else: 139 | widget = HTML(str(info)+'
') 140 | else: 141 | try: 142 | obj_type = obj['type'] 143 | except: 144 | obj_type = local_type 145 | 146 | if obj_type in DISPATCHERS.keys(): 147 | widget = DISPATCHERS[obj_type](obj) 148 | else: 149 | info = str(obj) 150 | widget = Label(info) 151 | 152 | except Exception as e: 153 | exc_type, exc_value, exc_traceback = sys.exc_info() 154 | trace = traceback.format_exception(exc_type, exc_value, 155 | exc_traceback) 156 | 157 | # Widget 158 | widget = ErrorAccordion(e, trace) 159 | obj_type = 'ERROR' 160 | local_type = 'ERROR' 161 | 162 | end = time() 163 | dt = end-start 164 | 165 | return EEWidget(widget, obj_type, local_type, dt) 166 | 167 | 168 | @register('str', 'String') 169 | def string(info): 170 | """ Dispatch Strings """ 171 | # info = new_line(info, 100) 172 | 173 | if isurl(info): 174 | if info[:3] == 'www': 175 | html = "{}" 176 | else: 177 | html = "{}" 178 | widget = HTML(html.format(info, info)) 179 | else: 180 | html = "{}" 181 | widget = HTML(html.format(info)) 182 | 183 | return widget 184 | 185 | 186 | @register('int', 'float', 'Number') 187 | def number(info): 188 | """ Dispatch Numbers """ 189 | return HTML(str(info)) 190 | 191 | 192 | @register('Dictionary', 'dict', 'List', 'list', 'tuple') 193 | def iterable(info): 194 | """ Dispatch Iterables (list and dict) """ 195 | widget = VBox() 196 | elements = [] 197 | 198 | info = order(info) 199 | 200 | for key, val in info: 201 | if isinstance(val, (str, int, float)): 202 | try: 203 | length = len(val) 204 | except: 205 | length = 1 206 | 207 | if length < 500: 208 | html = '
{}: {}
' 209 | container = HTML(html.format(key, dispatch(val).widget.value)) 210 | else: 211 | container = dispatch(val).widget 212 | container = Accordion([container]) 213 | container.set_title(0, key) 214 | container.selected_index = None 215 | else: 216 | container, _ = create_container() 217 | eewidget = dispatch(val) 218 | set_container(container, eewidget) 219 | container = Accordion([container]) 220 | container.set_title(0, key) 221 | container.selected_index = None 222 | elements.append(container) 223 | widget.children = elements 224 | 225 | return widget 226 | 227 | 228 | @register('Image') 229 | def image(info): 230 | """ Dispatch a Widget for an Image Object """ 231 | # IMAGE 232 | image_id = info['id'] if 'id' in info else 'No Image ID' 233 | prop = info.get('properties') 234 | bands = info.get('bands') 235 | bands_names = [band.get('id') for band in bands] 236 | 237 | # BAND PRECISION 238 | bands_precision = [] 239 | for band in bands: 240 | data = band.get('data_type') 241 | if data: 242 | precision = data.get('precision') 243 | bands_precision.append(precision) 244 | 245 | # BAND CRS 246 | bands_crs = [] 247 | for band in bands: 248 | crs = band.get('crs') 249 | bands_crs.append(crs) 250 | 251 | # BAND MIN AND MAX 252 | bands_min = [] 253 | for band in bands: 254 | data = band.get('data_type') 255 | if data: 256 | bmin = data.get('min') 257 | bands_min.append(bmin) 258 | 259 | bands_max = [] 260 | for band in bands: 261 | data = band.get('data_type') 262 | if data: 263 | bmax = data.get('max') 264 | bands_max.append(bmax) 265 | 266 | # BANDS 267 | new_band_names = [] 268 | zipped_data = zip(bands_names, bands_precision, bands_min, bands_max, 269 | bands_crs) 270 | for name, ty, mn, mx, epsg in zipped_data: 271 | value = '
  • {} ({}) {} to {} - {}
  • '.format(name,ty, 272 | mn,mx,epsg) 273 | new_band_names.append(value) 274 | bands_wid = HTML('') 275 | 276 | # PROPERTIES 277 | if prop: 278 | prop_wid = dispatch(prop).widget 279 | else: 280 | prop_wid = HTML('Image has no properties') 281 | 282 | # ID 283 | header = HTML('Image id: {id}
    '.format(id=image_id)) 284 | 285 | acc = Accordion([bands_wid, prop_wid]) 286 | acc.set_title(0, 'Bands') 287 | acc.set_title(1, 'Properties') 288 | acc.selected_index = None # thisp will unselect all 289 | 290 | return VBox([header, acc]) 291 | 292 | 293 | @register('Date') 294 | def date(info): 295 | """ Dispatch a ee.Date """ 296 | date = ee.Date(info.get('value')) 297 | return Label(date.format().getInfo()) 298 | 299 | 300 | @register('DateRange') 301 | def daterange(info): 302 | """ Dispatch a DateRange """ 303 | dates = info['dates'] 304 | start = datetime.utcfromtimestamp(dates[0]/1000).isoformat() 305 | end = datetime.utcfromtimestamp(dates[1]/1000).isoformat() 306 | value = '{} to {}'.format(start, end) 307 | return Label(value) 308 | 309 | 310 | @register('Point', 'LineString', 'LinearRing', 'Polygon', 'Rectangle', 311 | 'MultiPoint', 'MultiLineString', 'MultiPolygon') 312 | def geometry(info): 313 | """ Dispatch a ee.Geometry """ 314 | if not info: 315 | widget = Accordion() 316 | widget.children = [Label('None')] 317 | widget.set_title(0, 'coordinates') 318 | widget.selected_index = None 319 | return widget 320 | 321 | coords = info.get('coordinates') 322 | typee = info.get('type') 323 | widget = Accordion() 324 | 325 | if typee in ['MultiPoint', 'MultiPolygon', 'MultiLineString']: 326 | inner_children = [] 327 | for coord in coords: 328 | inner_children.append(Label(str(coord))) 329 | inner_acc = Accordion(inner_children) 330 | inner_acc.selected_index = None # this will unselect all 331 | for i, _ in enumerate(coords): 332 | inner_acc.set_title(i, str(i)) 333 | children = [inner_acc] 334 | else: 335 | children = [Label(str(coords))] 336 | 337 | widget.children = children 338 | widget.set_title(0, 'coordinates') 339 | widget.selected_index = None 340 | return widget 341 | 342 | 343 | @register('Feature') 344 | def feature(info): 345 | """ Dispatch a ee.Feature """ 346 | geom = info.get('geometry') 347 | # geomtype = geom.get('type') 348 | props = info.get('properties') 349 | 350 | # Contruct an accordion with the geometries 351 | acc = geometry(geom) 352 | children = list(acc.children) 353 | 354 | # Properties 355 | if props: 356 | # dispatch properties 357 | prop_acc = dispatch(props) 358 | else: 359 | prop_acc = dispatch('Feature has no properties') 360 | 361 | # Append properties as a child 362 | children.append(prop_acc.widget) 363 | acc.set_title(1, 'properties') 364 | acc.children = children 365 | acc.selected_index = None 366 | 367 | # Geometry Type 368 | # typewid = dispatch(geomtype) 369 | 370 | return acc 371 | 372 | 373 | @register('FeatureCollection', 'ImageCollection') 374 | def collection(info): 375 | """ Dispatch a Collection """ 376 | try: 377 | fcid = info['id'] 378 | except KeyError: 379 | try: 380 | fcid = info['properties']['system:index'] 381 | except: 382 | fcid = None 383 | 384 | idlabel = HTML('Collection ID: {}'.format(fcid)) 385 | 386 | # dispatch properties 387 | props = info.get('properties') 388 | if props: 389 | properties = dispatch(info['properties']) # acc 390 | else: 391 | properties = dispatch('{} has no properties'.format(info.get('type'))) 392 | properties_acc = Accordion([properties.widget]) 393 | properties_acc.set_title(0, 'Properties') 394 | properties_acc.selected_index = None 395 | 396 | # Features 397 | features = info['features'] 398 | 399 | features_children = [] 400 | for i, feat in enumerate(features): 401 | featacc = dispatch(feat).widget 402 | acc = Accordion([featacc]) 403 | acc.set_title(0, str(i)) 404 | acc.selected_index = None 405 | features_children.append(acc) 406 | 407 | features_wid = VBox(features_children) 408 | features_acc = Accordion([features_wid]) 409 | title = 'Features' if info['type'] == 'FeatureCollection' else 'Images' 410 | features_acc.set_title(0, title) 411 | features_acc.selected_index = None 412 | 413 | if info['type'] == 'FeatureCollection': 414 | # columns 415 | columns = dispatch(info['columns']).widget # acc 416 | columns_acc = Accordion([columns]) 417 | columns_acc.set_title(0, 'Columns') 418 | columns_acc.selected_index = None 419 | 420 | widgets = [idlabel, columns_acc, properties_acc, features_acc] 421 | else: 422 | widgets = [idlabel, properties_acc, features_acc] 423 | 424 | return VBox(widgets) 425 | -------------------------------------------------------------------------------- /examples/Map.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import ee\n", 10 | "ee.Initialize()" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "from ipygee import *" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## Create a Map instance\n", 27 | "- Arguments:\n", 28 | " - tabs: a `tuple` indicating which tabs to load in the map. Options are: Inspector, Layers, Assets, Tasks\n", 29 | " - kwargs: as this class inherits from `ipyleaflet.Map` it can accept all its arguments" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 3, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "Map = Map()" 39 | ] 40 | }, 41 | { 42 | "cell_type": "markdown", 43 | "metadata": {}, 44 | "source": [ 45 | "## Show map with method `show`\n", 46 | "- Arguments\n", 47 | " - tabs: show tabs (bool)\n", 48 | " - layer_control: show a control for layers (bool)\n", 49 | " - draw_control: show a control for drawings (bool)\n", 50 | " - fullscrean: show fullscreen button (bool)" 51 | ] 52 | }, 53 | { 54 | "cell_type": "code", 55 | "execution_count": 4, 56 | "metadata": { 57 | "scrolled": false 58 | }, 59 | "outputs": [ 60 | { 61 | "data": { 62 | "application/vnd.jupyter.widget-view+json": { 63 | "model_id": "b4bdceb806ff4ac68c46282d41bcc70f", 64 | "version_major": 2, 65 | "version_minor": 0 66 | }, 67 | "text/plain": [ 68 | "Map(basemap={'url': 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', 'attribution': 'Map data (c) ,\n", 380 | " 'type': 'Image',\n", 381 | " 'visParams': {'bands': 'B8,B11,B4', 'max': 5000, 'min': 0}}" 382 | ] 383 | }, 384 | "execution_count": 18, 385 | "metadata": {}, 386 | "output_type": "execute_result" 387 | } 388 | ], 389 | "source": [ 390 | "layer" 391 | ] 392 | }, 393 | { 394 | "cell_type": "markdown", 395 | "metadata": {}, 396 | "source": [ 397 | "### Directly get the asociated EE object" 398 | ] 399 | }, 400 | { 401 | "cell_type": "code", 402 | "execution_count": 19, 403 | "metadata": {}, 404 | "outputs": [], 405 | "source": [ 406 | "eelayer = Map.getObject('Sentinel 2 Image')" 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 20, 412 | "metadata": {}, 413 | "outputs": [ 414 | { 415 | "data": { 416 | "text/plain": [ 417 | "['B1',\n", 418 | " 'B2',\n", 419 | " 'B3',\n", 420 | " 'B4',\n", 421 | " 'B5',\n", 422 | " 'B6',\n", 423 | " 'B7',\n", 424 | " 'B8',\n", 425 | " 'B8A',\n", 426 | " 'B9',\n", 427 | " 'B10',\n", 428 | " 'B11',\n", 429 | " 'B12',\n", 430 | " 'QA10',\n", 431 | " 'QA20',\n", 432 | " 'QA60']" 433 | ] 434 | }, 435 | "execution_count": 20, 436 | "metadata": {}, 437 | "output_type": "execute_result" 438 | } 439 | ], 440 | "source": [ 441 | "eelayer.bandNames().getInfo()" 442 | ] 443 | }, 444 | { 445 | "cell_type": "markdown", 446 | "metadata": {}, 447 | "source": [ 448 | "# TABS\n", 449 | "## You can add a custom Tab with a custom handler. The handler is a function with 4 main parameters:\n", 450 | "- **type:** the interaction type. Can be 'click', 'mouseover', etc\n", 451 | "- **coordinates:** the coordinates where the interaction has taken place. If you have used ipyleaflet before, take in consideraton that coordinates are inverted (to match GEE format): [longitud, latitude]\n", 452 | "- **widget:** The widget inside the Tab. Defaults to an empty HTML widget\n", 453 | "- **map:** the Map instance. You can apply any of its methods, or get any of its properties" 454 | ] 455 | }, 456 | { 457 | "cell_type": "code", 458 | "execution_count": 21, 459 | "metadata": {}, 460 | "outputs": [ 461 | { 462 | "name": "stdout", 463 | "output_type": "stream", 464 | "text": [ 465 | " Add a Tab to the Panel. The handler is for the Map\n", 466 | "\n", 467 | " :param name: name for the new tab\n", 468 | " :type name: str\n", 469 | " :param handler: handle function for the new tab. Arguments of the\n", 470 | " function are:\n", 471 | "\n", 472 | " - type: the type of the event (click, mouseover, etc..)\n", 473 | " - coordinates: coordinates where the event occurred [lon, lat]\n", 474 | " - widget: the widget inside the Tab\n", 475 | " - map: the Map instance\n", 476 | "\n", 477 | " :param widget: widget inside the Tab. Defaults to HTML('')\n", 478 | " :type widget: ipywidgets.Widget\n", 479 | " \n" 480 | ] 481 | } 482 | ], 483 | "source": [ 484 | "print(Map.addTab.__doc__)" 485 | ] 486 | }, 487 | { 488 | "cell_type": "code", 489 | "execution_count": 22, 490 | "metadata": {}, 491 | "outputs": [ 492 | { 493 | "name": "stdout", 494 | "output_type": "stream", 495 | "text": [ 496 | "Check out the Map!\n" 497 | ] 498 | } 499 | ], 500 | "source": [ 501 | "def test_handler(**change): \n", 502 | " # PARAMS\n", 503 | " ty = change['type']\n", 504 | " coords = change['coordinates']\n", 505 | " wid = change['widget']\n", 506 | " themap = change['map']\n", 507 | " \n", 508 | " if ty == 'click': # If interaction was a click\n", 509 | " # Loading message before sending a request to EE\n", 510 | " wid.value = 'Loading...'\n", 511 | " # Map's bounds\n", 512 | " bounds = themap.getBounds().getInfo()['coordinates']\n", 513 | " # Change Widget Value\n", 514 | " wid.value = \"You have clicked on {} and map's bounds are {}\".format(coords, bounds) \n", 515 | "\n", 516 | "Map.addTab('TestTAB', test_handler)\n", 517 | "print(\"Check out the Map!\")" 518 | ] 519 | }, 520 | { 521 | "cell_type": "code", 522 | "execution_count": null, 523 | "metadata": {}, 524 | "outputs": [], 525 | "source": [] 526 | } 527 | ], 528 | "metadata": { 529 | "kernelspec": { 530 | "display_name": "Python 3", 531 | "language": "python", 532 | "name": "python3" 533 | }, 534 | "language_info": { 535 | "codemirror_mode": { 536 | "name": "ipython", 537 | "version": 3 538 | }, 539 | "file_extension": ".py", 540 | "mimetype": "text/x-python", 541 | "name": "python", 542 | "nbconvert_exporter": "python", 543 | "pygments_lexer": "ipython3", 544 | "version": "3.5.2" 545 | } 546 | }, 547 | "nbformat": 4, 548 | "nbformat_minor": 2 549 | } 550 | -------------------------------------------------------------------------------- /ipygee/tabs/layers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Layers widget for Map tab """ 4 | 5 | from ..widgets import RealBox 6 | from ipywidgets import * 7 | from ..threading import Thread 8 | from traitlets import * 9 | from .. import utils 10 | 11 | 12 | class FloatBandWidget(HBox): 13 | min = Float(0) 14 | max = Float(1) 15 | 16 | def __init__(self, **kwargs): 17 | super(FloatBandWidget, self).__init__(**kwargs) 18 | self.minWid = FloatText(value=self.min, description='min') 19 | self.maxWid = FloatText(value=self.max, description='max') 20 | 21 | self.children = [self.minWid, self.maxWid] 22 | 23 | self.observe(self._ob_min, names=['min']) 24 | self.observe(self._ob_max, names=['max']) 25 | 26 | def _ob_min(self, change): 27 | new = change['new'] 28 | self.minWid.value = new 29 | 30 | def _ob_max(self, change): 31 | new = change['new'] 32 | self.maxWid.value = new 33 | 34 | 35 | class LayersWidget(RealBox): 36 | def __init__(self, map=None, **kwargs): 37 | super(LayersWidget, self).__init__(**kwargs) 38 | self.map = map 39 | self.selector = Select() 40 | 41 | # define init EELayer 42 | self.EELayer = None 43 | 44 | # Buttons 45 | self.center = Button(description='Center') 46 | self.center.on_click(self.onClickCenter) 47 | 48 | self.remove = Button(description='Remove') 49 | self.remove.on_click(self.onClickRemove) 50 | 51 | self.show_prop = Button(description='Show Object') 52 | self.show_prop.on_click(self.onClickShowObject) 53 | 54 | self.vis = Button(description='Visualization') 55 | self.vis.on_click(self.onClickVis) 56 | 57 | self.move_up = Button(description='Move up') 58 | self.move_up.on_click(self.onUp) 59 | 60 | self.move_down = Button(description='Move down') 61 | self.move_down.on_click(self.onDown) 62 | 63 | # Buttons Group 1 64 | self.group1 = VBox([self.center, self.remove, 65 | self.vis, self.show_prop]) 66 | 67 | # Buttons Group 2 68 | self.group2 = VBox([self.move_up, self.move_down]) 69 | 70 | # self.children = [self.selector, self.group1] 71 | self.items = [[self.selector, self.group1, self.group2]] 72 | 73 | self.selector.observe(self.handle_selection, names='value') 74 | 75 | def onUp(self, button=None): 76 | if self.EELayer: 77 | self.map.moveLayer(self.layer.name, 'up') 78 | 79 | def onDown(self, button=None): 80 | if self.EELayer: 81 | self.map.moveLayer(self.layer.name, 'down') 82 | 83 | def handle_selection(self, change): 84 | new = change['new'] 85 | self.EELayer = new 86 | 87 | # set original display 88 | self.items = [[self.selector, self.group1, self.group2]] 89 | 90 | if new: 91 | self.layer = new['layer'] 92 | self.obj = new['object'] 93 | self.ty = new['type'] 94 | self.vis = new['visParams'] 95 | 96 | def onClickShowObject(self, button=None): 97 | if self.EELayer: 98 | loading = HTML('Loading {}...'.format(self.layer.name)) 99 | widget = VBox([loading]) 100 | thread = Thread(target=utils.create_async_output, 101 | args=(self.obj, widget)) 102 | self.items = [[self.selector, self.group1], 103 | [widget]] 104 | thread.start() 105 | 106 | def onClickCenter(self, button=None): 107 | if self.EELayer: 108 | self.map.centerObject(self.obj) 109 | 110 | def onClickRemove(self, button=None): 111 | if self.EELayer: 112 | self.map.removeLayer(self.layer.name) 113 | 114 | def onClickVis(self, button=None): 115 | if self.EELayer: 116 | # options 117 | selector = self.selector 118 | group1 = self.group1 119 | 120 | # map 121 | map = self.map 122 | layer_name = self.layer.name 123 | image = self.obj 124 | 125 | # Image Bands 126 | try: 127 | info = self.obj.getInfo() 128 | except Exception as e: 129 | self.items = [[self.selector, self.group1], 130 | [HTML(str(e))]] 131 | return 132 | 133 | # IMAGES 134 | if self.ty == 'Image': 135 | ### image data ### 136 | bands = info['bands'] 137 | imbands = [band['id'] for band in bands] 138 | bands_type = [band['data_type']['precision'] for band in bands] 139 | bands_min = [] 140 | bands_max = [] 141 | # as float bands don't hava an specific range, reduce region to get the 142 | # real range 143 | if 'float' in bands_type: 144 | try: 145 | minmax = image.reduceRegion(ee.Reducer.minMax()) 146 | for band in bands: 147 | bandname = band['id'] 148 | try: 149 | tmin = minmax.get('{}_min'.format(bandname)).getInfo() # 0 150 | tmax = minmax.get('{}_max'.format(bandname)).getInfo() # 1 151 | except: 152 | tmin = 0 153 | tmax = 1 154 | bands_min.append(tmin) 155 | bands_max.append(tmax) 156 | except: 157 | for band in bands: 158 | dt = band['data_type'] 159 | try: 160 | tmin = dt['min'] 161 | tmax = dt['max'] 162 | except: 163 | tmin = 0 164 | tmax = 1 165 | bands_min.append(tmin) 166 | bands_max.append(tmax) 167 | else: 168 | for band in bands: 169 | dt = band['data_type'] 170 | try: 171 | tmin = dt['min'] 172 | tmax = dt['max'] 173 | except: 174 | tmin = 0 175 | tmax = 1 176 | bands_min.append(tmin) 177 | bands_max.append(tmax) 178 | 179 | 180 | # dict of {band: min} and {band:max} 181 | min_dict = dict(zip(imbands, bands_min)) 182 | max_dict = dict(zip(imbands, bands_max)) 183 | ###### 184 | 185 | # Layer data 186 | layer_data = self.map.EELayers[layer_name] 187 | visParams = layer_data['visParams'] 188 | 189 | # vis bands 190 | visBands = visParams['bands'].split(',') 191 | 192 | # vis min 193 | visMin = visParams['min'] 194 | if isinstance(visMin, str): 195 | visMin = [float(vis) for vis in visMin.split(',')] 196 | else: 197 | visMin = [visMin] 198 | 199 | # vis max 200 | visMax = visParams['max'] 201 | if isinstance(visMax, str): 202 | visMax = [float(vis) for vis in visMax.split(',')] 203 | else: 204 | visMax = [visMax] 205 | 206 | # dropdown handler 207 | def handle_dropdown(band_slider): 208 | def wrap(change): 209 | new = change['new'] 210 | band_slider.min = min_dict[new] 211 | band_slider.max = max_dict[new] 212 | return wrap 213 | 214 | def slider_1band(float=False, name='band'): 215 | ''' Create the widget for one band ''' 216 | # get params to set in slider and dropdown 217 | vismin = visMin[0] 218 | vismax = visMax[0] 219 | band = visBands[0] 220 | 221 | drop = Dropdown(description=name, options=imbands, value=band) 222 | 223 | if float: 224 | slider = FloatBandWidget(min=min_dict[drop.value], 225 | max=max_dict[drop.value]) 226 | else: 227 | slider = FloatRangeSlider(min=min_dict[drop.value], 228 | max=max_dict[drop.value], 229 | value=[vismin, vismax], 230 | step=0.01) 231 | # set handler 232 | drop.observe(handle_dropdown(slider), names=['value']) 233 | 234 | # widget for band selector + slider 235 | band_slider = HBox([drop, slider]) 236 | # return VBox([band_slider], layout=Layout(width='500px')) 237 | return band_slider 238 | 239 | def slider_3bands(float=False): 240 | ''' Create the widget for one band ''' 241 | # get params to set in slider and dropdown 242 | if len(visMin) == 1: 243 | visminR = visminG = visminB = visMin[0] 244 | else: 245 | visminR = visMin[0] 246 | visminG = visMin[1] 247 | visminB = visMin[2] 248 | 249 | if len(visMax) == 1: 250 | vismaxR = vismaxG = vismaxB = visMax[0] 251 | else: 252 | vismaxR = visMax[0] 253 | vismaxG = visMax[1] 254 | vismaxB = visMax[2] 255 | 256 | if len(visBands) == 1: 257 | visbandR = visbandG = visbandB = visBands[0] 258 | else: 259 | visbandR = visBands[0] 260 | visbandG = visBands[1] 261 | visbandB = visBands[2] 262 | 263 | drop = Dropdown(description='red', options=imbands, value=visbandR) 264 | drop2 = Dropdown(description='green', options=imbands, value=visbandG) 265 | drop3 = Dropdown(description='blue', options=imbands, value=visbandB) 266 | slider = FloatRangeSlider(min=min_dict[drop.value], 267 | max=max_dict[drop.value], 268 | value=[visminR, vismaxR], 269 | step=0.01) 270 | slider2 = FloatRangeSlider(min=min_dict[drop2.value], 271 | max=max_dict[drop2.value], 272 | value=[visminG, vismaxG], 273 | step=0.01) 274 | slider3 = FloatRangeSlider(min=min_dict[drop3.value], 275 | max=max_dict[drop3.value], 276 | value=[visminB, vismaxB], 277 | step=0.01) 278 | # set handlers 279 | drop.observe(handle_dropdown(slider), names=['value']) 280 | drop2.observe(handle_dropdown(slider2), names=['value']) 281 | drop3.observe(handle_dropdown(slider3), names=['value']) 282 | 283 | # widget for band selector + slider 284 | band_slider = HBox([drop, slider]) 285 | band_slider2 = HBox([drop2, slider2]) 286 | band_slider3 = HBox([drop3, slider3]) 287 | 288 | return VBox([band_slider, band_slider2, band_slider3], 289 | layout=Layout(width='700px')) 290 | 291 | # Create widget for 1 or 3 bands 292 | bands = RadioButtons(options=['1 band', '3 bands'], 293 | layout=Layout(width='80px')) 294 | 295 | # Create widget for band, min and max selection 296 | selection = slider_1band() 297 | 298 | # Apply button 299 | apply = Button(description='Apply', layout=Layout(width='100px')) 300 | 301 | # new row 302 | new_row = [bands, selection, apply] 303 | 304 | # update row of widgets 305 | def update_row_items(new_row): 306 | self.items = [[selector, group1], 307 | new_row] 308 | 309 | # handler for radio button (1 band / 3 bands) 310 | def handle_radio_button(change): 311 | new = change['new'] 312 | if new == '1 band': 313 | # create widget 314 | selection = slider_1band() # TODO 315 | # update row of widgets 316 | update_row_items([bands, selection, apply]) 317 | else: 318 | red = slider_1band(name='red') # TODO 319 | green = slider_1band(name='green') 320 | blue = slider_1band(name='blue') 321 | selection = VBox([red, green, blue]) 322 | # selection = slider_3bands() 323 | update_row_items([bands, selection, apply]) 324 | 325 | def handle_apply(button): 326 | radio = self.items[1][0].value # radio button 327 | vbox = self.items[1][1] 328 | if radio == '1 band': # 1 band 329 | hbox_band = vbox.children[0].children 330 | 331 | band = hbox_band[0].value 332 | min = hbox_band[1].value[0] 333 | max = hbox_band[1].value[1] 334 | 335 | map.addLayer(image, {'bands':[band], 'min':min, 'max':max}, 336 | layer_name) 337 | else: # 3 bands 338 | hbox_bandR = vbox.children[0].children 339 | hbox_bandG = vbox.children[1].children 340 | hbox_bandB = vbox.children[2].children 341 | 342 | bandR = hbox_bandR[0].value 343 | bandG = hbox_bandG[0].value 344 | bandB = hbox_bandB[0].value 345 | 346 | minR = hbox_bandR[1].value[0] 347 | minG = hbox_bandG[1].value[0] 348 | minB = hbox_bandB[1].value[0] 349 | 350 | maxR = hbox_bandR[1].value[1] 351 | maxG = hbox_bandG[1].value[1] 352 | maxB = hbox_bandB[1].value[1] 353 | 354 | map.addLayer(image, {'bands':[bandR, bandG, bandB], 355 | 'min':[float(minR), float(minG), float(minB)], 356 | 'max':[float(maxR), float(maxG), float(maxB)]}, 357 | layer_name) 358 | 359 | bands.observe(handle_radio_button, names='value') 360 | update_row_items(new_row) 361 | apply.on_click(handle_apply) -------------------------------------------------------------------------------- /ipygee/maptools.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Tools for the Map """ 4 | 5 | import ee 6 | from geetools import tools 7 | import math 8 | from uuid import uuid4 9 | 10 | 11 | def getBounds(eeObject): 12 | if isinstance(eeObject, list): 13 | bounds = eeObject 14 | else: 15 | # Make a buffer if object is a Point 16 | if isinstance(eeObject, ee.Geometry): 17 | t = eeObject.type().getInfo() 18 | if t == 'Point': 19 | eeObject = eeObject.buffer(1000) 20 | 21 | bounds = tools.geometry.getRegion(eeObject, True) 22 | 23 | # Catch unbounded images 24 | unbounded = [[[-180.0, -90.0], [180.0, -90.0], 25 | [180.0, 90.0], [-180.0, 90.0], 26 | [-180.0, -90.0]]] 27 | 28 | if bounds == unbounded: 29 | print("can't center object because it is unbounded") 30 | return None 31 | 32 | bounds = inverseCoordinates(bounds) 33 | return bounds 34 | 35 | 36 | def getDefaultVis(image, stretch=0.8): 37 | bandnames = image.bandNames().getInfo() 38 | 39 | if len(bandnames) < 3: 40 | selected = image.select([0]).getInfo() 41 | bandnames = bandnames[0] 42 | else: 43 | selected = image.select([0, 1, 2]).getInfo() 44 | bandnames = [bandnames[0], bandnames[1], bandnames[2]] 45 | 46 | bands = selected['bands'] 47 | # bandnames = [bands[0]['id'], bands[1]['id'], bands[2]['id']] 48 | types = bands[0]['data_type'] 49 | 50 | maxs = {'float':1, 51 | 'double': 1, 52 | 'int8': 127, 'uint8': 255, 53 | 'int16': 32767, 'uint16': 65535, 54 | 'int32': 2147483647, 'uint32': 4294967295, 55 | 'int64': 9223372036854776000} 56 | 57 | precision = types['precision'] 58 | 59 | if precision == 'float': 60 | btype = 'float' 61 | elif precision == 'double': 62 | btype = 'double' 63 | elif precision == 'int': 64 | max = types['max'] 65 | maxs_inverse = dict((val, key) for key, val in maxs.items()) 66 | btype = maxs_inverse[int(max)] 67 | else: 68 | raise ValueError('Unknown data type {}'.format(precision)) 69 | 70 | limits = {'float': 0.8} 71 | 72 | for key, val in maxs.items(): 73 | limits[key] = val*stretch 74 | 75 | min = 0 76 | max = limits[btype] 77 | return {'bands':bandnames, 'min':min, 'max':max} 78 | 79 | 80 | def isPoint(item): 81 | """ Determine if the given list has the structure of a point. This is: 82 | it is a list or tuple with two int or float items """ 83 | if isinstance(item, list) or isinstance(item, tuple): 84 | if len(item) == 2: 85 | lon = item[0] 86 | if isinstance(lon, int) or isinstance(lon, float): 87 | return True 88 | else: 89 | return False 90 | else: 91 | return False 92 | else: 93 | return False 94 | 95 | 96 | def inverseCoordinates(coords): 97 | """ Inverse a set of coordinates (any nesting depth) 98 | 99 | :param coords: a nested list of points 100 | :type coords: list 101 | """ 102 | newlist = [] 103 | if isPoint(coords): 104 | return [coords[1], coords[0]] 105 | elif not isinstance(coords, list) and not isinstance(coords, tuple): 106 | raise ValueError('coordinates to inverse must be minimum a point') 107 | for i, it in enumerate(coords): 108 | p = isPoint(it) 109 | if not p and (isinstance(it, list) or isinstance(it, tuple)): 110 | newlist.append(inverseCoordinates(it)) 111 | else: 112 | newp = [it[1],it[0]] 113 | newlist.append(newp) 114 | return newlist 115 | 116 | 117 | def visparamsStrToList(params): 118 | """ Transform a string formated as needed by ee.data.getMapId to a list 119 | 120 | :param params: params to convert 121 | :type params: str 122 | :return: a list with the params 123 | :rtype: list 124 | """ 125 | proxy_bands = [] 126 | bands = params.split(',') 127 | for band in bands: 128 | proxy_bands.append(band.strip()) 129 | return proxy_bands 130 | 131 | 132 | def visparamsListToStr(params): 133 | """ Transform a list to a string formated as needed by 134 | ee.data.getMapId 135 | 136 | :param params: params to convert 137 | :type params: list 138 | :return: a string formated as needed by ee.data.getMapId 139 | :rtype: str 140 | """ 141 | n = len(params) 142 | if n == 1: 143 | newbands = '{}'.format(params[0]) 144 | elif n == 3: 145 | newbands = '{},{},{}'.format(params[0], params[1], params[2]) 146 | else: 147 | newbands = '{}'.format(params[0]) 148 | return newbands 149 | 150 | 151 | def getImageTile(image, visParams, show=True, opacity=None, 152 | overlay=True): 153 | 154 | proxy = {} 155 | params = visParams if visParams else {} 156 | 157 | # BANDS ############# 158 | def default_bands(image): 159 | bandnames = image.bandNames().getInfo() 160 | if len(bandnames) < 3: 161 | bands = [bandnames[0]] 162 | else: 163 | bands = [bandnames[0], bandnames[1], bandnames[2]] 164 | return bands 165 | bands = params.get('bands') if 'bands' in params else default_bands(image) 166 | 167 | # if the passed bands is a string formatted like required by GEE, get the 168 | # list out of it 169 | if isinstance(bands, str): 170 | bands_list = visparamsStrToList(bands) 171 | bands_str = visparamsListToStr(bands_list) 172 | 173 | # Transform list to getMapId format 174 | # ['b1', 'b2', 'b3'] == 'b1, b2, b3' 175 | if isinstance(bands, list): 176 | bands_list = bands 177 | bands_str = visparamsListToStr(bands) 178 | 179 | # Set proxy parameteres 180 | proxy['bands'] = bands_str 181 | 182 | # MIN ################# 183 | themin = params.get('min') if 'min' in params else '0' 184 | 185 | # if the passed min is a list, convert to the format required by GEE 186 | if isinstance(themin, list): 187 | themin = visparamsListToStr(themin) 188 | 189 | proxy['min'] = themin 190 | 191 | # MAX ################# 192 | def default_max(image, bands): 193 | proxy_maxs = [] 194 | maxs = {'float':1, 195 | 'double': 1, 196 | 'int8': ((2**8)-1)/2, 'uint8': (2**8)-1, 197 | 'int16': ((2**16)-1)/2, 'uint16': (2**16)-1, 198 | 'int32': ((2**32)-1)/2, 'uint32': (2**32)-1, 199 | 'int64': ((2**64)-1)/2} 200 | for band in bands: 201 | ty = image.select([band]).getInfo()['bands'][0]['data_type'] 202 | try: 203 | themax = maxs[ty] 204 | except: 205 | themax = 1 206 | proxy_maxs.append(themax) 207 | return proxy_maxs 208 | 209 | themax = params.get('max') if 'max' in params else default_max(image, 210 | bands_list) 211 | 212 | # if the passed max is a list or the max is computed by the default function 213 | # convert to the format required by GEE 214 | if isinstance(themax, list): 215 | themax = visparamsListToStr(themax) 216 | 217 | proxy['max'] = themax 218 | 219 | # PALETTE ################ 220 | if 'palette' in params: 221 | if len(bands_list) == 1: 222 | palette = params.get('palette') 223 | if isinstance(palette, str): 224 | palette = visparamsStrToList(palette) 225 | toformat = '{},'*len(palette) 226 | palette = toformat[:-1].format(*palette) 227 | proxy['palette'] = palette 228 | else: 229 | print("Can't use palette parameter with more than one band") 230 | 231 | # Get the MapID and Token after applying parameters 232 | image_info = image.getMapId(proxy) 233 | fetcher = image_info['tile_fetcher'] 234 | tiles = fetcher.url_format 235 | attribution = 'Map Data © Google Earth Engine ' 236 | overlay = overlay 237 | 238 | return {'url': tiles, 239 | 'attribution': attribution, 240 | 'overlay': overlay, 241 | 'show': show, 242 | 'opacity': opacity, 243 | 'visParams': proxy, 244 | } 245 | 246 | 247 | def featurePropertiesOutput(feat): 248 | """ generates a string for features properties """ 249 | info = feat.getInfo() 250 | properties = info['properties'] 251 | theid = info.get('id') 252 | if theid: 253 | stdout = '

    ID {}


    '.format(theid) 254 | else: 255 | stdout = '

    Feature has no ID


    ' 256 | 257 | if properties: 258 | for prop, value in properties.items(): 259 | stdout += '{}: {}
    '.format(prop, value) 260 | else: 261 | stdout += 'Feature has no properties' 262 | return stdout 263 | 264 | 265 | def getGeojsonTile(geometry, name=None, 266 | inspect={'data':None, 'reducer':None, 'scale':None}): 267 | ''' Get a GeoJson giving a ee.Geometry or ee.Feature ''' 268 | 269 | if isinstance(geometry, ee.Feature): 270 | feat = geometry 271 | geometry = feat.geometry() 272 | else: 273 | feat = None 274 | 275 | info = geometry.getInfo() 276 | type = info['type'] 277 | 278 | gjson_types = ['Polygon', 'LineString', 'MultiPolygon', 279 | 'LinearRing', 'MultiLineString', 'MultiPoint', 280 | 'Point', 'Polygon', 'Rectangle', 281 | 'GeometryCollection'] 282 | 283 | # newname = name if name else "{} {}".format(type, map.added_geometries) 284 | 285 | if type in gjson_types: 286 | data = inspect['data'] 287 | if feat: 288 | default_popup = featurePropertiesOutput(feat) 289 | else: 290 | default_popup = type 291 | red = inspect.get('reducer','first') 292 | sca = inspect.get('scale', None) 293 | popval = getData(geometry, data, red, sca, name) if data else default_popup 294 | geojson = geometry.getInfo() 295 | 296 | return {'geojson':geojson, 297 | 'pop': popval} 298 | # 'name': newname} 299 | else: 300 | print('unrecognized object type to add to map') 301 | 302 | 303 | def getZoom(bounds, method=1): 304 | ''' 305 | as ipyleaflet does not have a fit bounds method, try to get the zoom to fit 306 | 307 | from: https://stackoverflow.com/questions/6048975/google-maps-v3-how-to-calculate-the-zoom-level-for-a-given-bounds 308 | ''' 309 | bounds = bounds[0] 310 | sw = bounds[0] 311 | ne = bounds[2] 312 | 313 | sw_lon = sw[1] 314 | sw_lat = sw[0] 315 | ne_lon = ne[1] 316 | ne_lat = ne[0] 317 | 318 | def method1(): 319 | # Method 1 320 | WORLD_DIM = {'height': 256, 'width': 256} 321 | ZOOM_MAX = 21 322 | 323 | def latRad(lat): 324 | sin = math.sin(lat * math.pi / 180) 325 | radX2 = math.log((1 + sin) / (1 - sin)) / 2 326 | return max(min(radX2, math.pi), -math.pi) / 2 327 | 328 | def zoom(mapPx, worldPx, fraction): 329 | return math.floor(math.log(mapPx / worldPx / fraction) / math.log(2)) 330 | 331 | latFraction = float(latRad(ne_lat) - latRad(sw_lat)) / math.pi 332 | 333 | lngDiff = ne_lon - sw_lon 334 | 335 | lngFraction = (lngDiff + 360) if (lngDiff < 0) else lngDiff 336 | lngFraction = lngFraction / 360 337 | 338 | latZoom = zoom(400, WORLD_DIM['height'], latFraction) 339 | lngZoom = zoom(970, WORLD_DIM['width'], lngFraction) 340 | 341 | return int(min(latZoom, lngZoom, ZOOM_MAX)) 342 | 343 | def method2(): 344 | scale = 111319.49 345 | 346 | GLOBE_WIDTH = 256 # a constant in Google's map projection 347 | 348 | angle = ne_lon - sw_lon 349 | if angle < 0: 350 | angle += 360 351 | zoom = math.floor(math.log(scale * 360 / angle / GLOBE_WIDTH) / math.log(2)) 352 | return int(zoom)-8 353 | 354 | finalzoom = method1() if method == 1 else method2() 355 | return finalzoom 356 | 357 | 358 | # TODO: Multiple dispatch! https://www.artima.com/weblogs/viewpost.jsp?thread=101605 359 | def getData(geometry, obj, reducer='first', scale=None, name=None): 360 | ''' Get data from an ee.ComputedObject using a giving ee.Geometry ''' 361 | accepted = (ee.Image, ee.ImageCollection, ee.Feature, ee.FeatureCollection) 362 | 363 | reducers = {'first': ee.Reducer.first(), 364 | 'mean': ee.Reducer.mean(), 365 | 'median': ee.Reducer.median(), 366 | 'sum':ee.Reducer.sum()} 367 | 368 | if not isinstance(obj, accepted): 369 | return "Can't get data from that Object" 370 | elif isinstance(obj, ee.Image): 371 | t = geometry.type().getInfo() 372 | # Try to get the image scale 373 | scale = obj.select([0]).projection().nominalScale().getInfo() \ 374 | if not scale else scale 375 | 376 | # Reduce if computed scale is too big 377 | scale = 1 if scale > 500 else scale 378 | if t == 'Point': 379 | values = tools.image.getValue(obj, geometry, scale, 'client') 380 | val_str = '

    Data from {}'.format(name) 381 | for key, val in values.items(): 382 | val_str += '{}: {}
    '.format(key, val) 383 | return val_str 384 | elif t == 'Polygon': 385 | red = reducer if reducer in reducers.keys() else 'first' 386 | values = obj.reduceRegion(reducers[red], geometry, scale, maxPixels=1e13).getInfo() 387 | val_str = '

    {}:

    \n'.format(red) 388 | for key, val in values.items(): 389 | val_str += '{}: {}
    '.format(key, val) 390 | return val_str 391 | 392 | 393 | def paint(geometry, outline_color='black', fill_color=None, outline=2): 394 | """ Paint a Geometry, Feature or FeatureCollection """ 395 | 396 | def overlap(image_back, image_front): 397 | mask_back = image_back.mask() 398 | mask_front = image_front.mask() 399 | entire_mask = mask_back.add(mask_front) 400 | mask = mask_back.Not() 401 | masked = image_front.updateMask(mask).unmask() 402 | 403 | return masked.add(image_back.unmask()).updateMask(entire_mask) 404 | 405 | if isinstance(geometry, ee.Feature) or isinstance(geometry, ee.FeatureCollection): 406 | geometry = geometry.geometry() 407 | 408 | if fill_color: 409 | fill = ee.Image().paint(geometry, 1).visualize(palette=[fill_color]) 410 | 411 | if outline_color: 412 | out = ee.Image().paint(geometry, 1, outline).visualize(palette=[outline_color]) 413 | 414 | if fill_color and outline_color: 415 | rgbVector = overlap(out, fill) 416 | elif fill_color: 417 | rgbVector = fill 418 | else: 419 | rgbVector = out 420 | 421 | return rgbVector 422 | 423 | 424 | def createHTMLTable(header, rows): 425 | ''' Create a HTML table 426 | 427 | :param header: a list of headers 428 | :type header: list 429 | :param rows: a list with the values 430 | :type rows: list 431 | :return: a HTML string 432 | :rtype: str 433 | ''' 434 | uid = 'a'+uuid4().hex 435 | style = '\n' 436 | style = style.format(uid) 437 | general = '\n{{}}
    '.format(uid) 438 | row = ' \n{{}}\n'.format(uid) 439 | col = ' {{}}\n'.format(uid) 440 | headcol = ' {{}}\n'.format(uid) 441 | 442 | # header 443 | def create_row(alist, template): 444 | cols = '' 445 | for el in alist: 446 | cols += template.format(el) 447 | newrow = row.format(cols) 448 | return newrow 449 | 450 | header_row = create_row(header, headcol) 451 | 452 | rest = '' 453 | # rows 454 | for r in rows: 455 | newrow = create_row(r, col) 456 | rest += newrow 457 | 458 | body = '{}\n{}'.format(header_row, rest) 459 | html = '{}\n{}'.format(style, general.format(body)) 460 | 461 | return html -------------------------------------------------------------------------------- /ipygee/chart.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Charts from Google Earth Engine data. Inpired by this question 4 | https://gis.stackexchange.com/questions/291823/ui-charts-for-indices-time-series-in-python-api-of-google-earth-engine 5 | and https://youtu.be/FytuB8nFHPQ, but at the moment relaying on `pygal` 6 | library because it's the easiest to integrate with ipywidgets 7 | """ 8 | import pygal 9 | import base64 10 | import ee 11 | from geetools import tools, utils 12 | import pandas as pd 13 | 14 | # TODO: make not plotted bands values appear on tooltip 15 | # TODO: give capability to plot a secondary axis with other data 16 | 17 | 18 | def ydata2pandas(ydata): 19 | """ Convert data from charts y_data property to pandas """ 20 | dataframes = [] 21 | for serie, data in ydata.items(): 22 | index = [] 23 | values = [] 24 | for d in data: 25 | x = d[0] 26 | y = d[1] 27 | index.append(x) 28 | values.append(y) 29 | df = pd.DataFrame({serie:values}, index=index) 30 | dataframes.append(df) 31 | 32 | return pd.concat(dataframes, axis=1, sort=False) 33 | 34 | 35 | def concat(*plots): 36 | """ Concatenate plots. The type of the resulting plot will be the type 37 | of the first parsed plot 38 | """ 39 | first = plots[0] 40 | if isinstance(first, DateTimeLine): 41 | chart = DateTimeLine() 42 | else: 43 | chart = Line() 44 | 45 | y_data = {} 46 | for plot in plots: 47 | p_data = plot.y_data 48 | for serie, data in p_data.items(): 49 | y_data[serie] = data 50 | chart.add(serie, data) 51 | 52 | chart.y_data = y_data 53 | return chart 54 | 55 | 56 | def renderWidget(chart, width=None, height=None): 57 | """ Render a pygal chart into a Jupyter Notebook """ 58 | from ipywidgets import HTML 59 | 60 | b64 = base64.b64encode(chart.render()).decode('utf-8') 61 | 62 | src = 'data:image/svg+xml;charset=utf-8;base64,'+b64 63 | 64 | if width and not height: 65 | html = ''.format(src, width) 66 | elif height and not width: 67 | html = ''.format(src, height) 68 | elif width and height: 69 | html = ''.format(src, 70 | height, 71 | width) 72 | else: 73 | html = ''.format(src) 74 | 75 | return HTML(html) 76 | 77 | 78 | def fromPandas(line_chart, dataframe, x=None, y=None, datetime=False, 79 | drop_null=True): 80 | """ Creates a Line chart from a pandas dataFrame """ 81 | 82 | ### CHECK FOR PANDAS DATAFRAME 83 | if not isinstance(dataframe, pd.DataFrame): 84 | raise ValueError('first argument must be a pandas DataFrame') 85 | 86 | if drop_null: 87 | dataframe = dataframe.dropna() 88 | else: 89 | dataframe = dataframe.fillna(0) 90 | 91 | def column2list(df, col, null=0): 92 | """ 93 | Helper function to transform a column from a dataframe to a list. 94 | NaN values will be replaced with `null` parameter. 95 | String values will be replaced with float 96 | """ 97 | values = [] 98 | for val in df[col].values.tolist(): 99 | if pd.isnull(val): 100 | val = null 101 | if isinstance(val, str): 102 | val = float(val) 103 | values.append(val) 104 | return values 105 | 106 | if not x: 107 | # Sort dataframe by x values 108 | dataframe = dataframe.sort_index() 109 | labels = [int(n) for n in dataframe.index.tolist()] 110 | else: 111 | # Sort dataframe by x values 112 | dataframe = dataframe.sort_values(x) 113 | labels = column2list(dataframe, x) 114 | 115 | if not datetime: 116 | x_values = labels 117 | else: 118 | x_values = [tools.date.millisToDatetime(d) for d in labels] 119 | 120 | # add property to parsed line_chart object 121 | line_chart.x_labels = x_values 122 | 123 | if isinstance(y, list): 124 | for column in y: 125 | if column == x: 126 | continue 127 | 128 | ydata = column2list(dataframe, column) 129 | nydata = [] 130 | for dt, value in zip(x_values, ydata): 131 | nydata.append((dt, value)) 132 | ydata = nydata 133 | 134 | # TODO: add values config 135 | # pygal.org/en/latest/documentation/configuration/value.html 136 | line_chart.add(column, ydata) 137 | line_chart.y_data[column] = ydata 138 | else: 139 | ydata = column2list(dataframe, y) 140 | 141 | nydata = [] 142 | for dt, value in zip(x_values, ydata): 143 | nydata.append((dt, value)) 144 | 145 | ydata = nydata 146 | 147 | line_chart.add(y, ydata) 148 | line_chart.y_data[y] = ydata 149 | 150 | return line_chart 151 | 152 | 153 | class Line(pygal.XY): 154 | def __init__(self, **kwargs): 155 | super(Line, self).__init__(**kwargs) 156 | self.y_data = dict() 157 | self.x_label_rotation = 30 158 | 159 | @property 160 | def dataframe(self): 161 | return ydata2pandas(self.y_data) 162 | 163 | def renderWidget(self, width=None, height=None): 164 | """ Render a pygal chart into a Jupyter Notebook """ 165 | return renderWidget(self, width, height) 166 | 167 | def cat(self, *plots): 168 | """ Concatenate with other Line Graphics """ 169 | return concat(self, *plots) 170 | 171 | 172 | class DateTimeLine(pygal.DateTimeLine): 173 | def __init__(self, **kwargs): 174 | super(DateTimeLine, self).__init__(**kwargs) 175 | self.y_data = dict() 176 | self.x_label_rotation = 30 177 | 178 | @property 179 | def dataframe(self): 180 | return ydata2pandas(self.y_data) 181 | 182 | def renderWidget(self, width=None, height=None): 183 | """ Render a pygal chart into a Jupyter Notebook """ 184 | return renderWidget(self, width, height) 185 | 186 | def cat(self, *plots): 187 | """ Concatenate with other DateTimeLine Graphics """ 188 | return concat(self, *plots) 189 | 190 | 191 | class Image(object): 192 | """ Charts for Images """ 193 | def __init__(self, source): 194 | self.source = source 195 | 196 | @staticmethod 197 | def check_imageCollection(imageCollection): 198 | if not isinstance(imageCollection, ee.ImageCollection): 199 | msg = 'first parameter of Image.doySeries must be an ' \ 200 | 'ImageCollection, found {}' 201 | raise ValueError(msg.format(type(imageCollection))) 202 | 203 | @staticmethod 204 | def series(imageCollection, region, reducer='mean', scale=None, 205 | xProperty='system:time_start', bands=None, label_bands=None, 206 | properties=None, label_properties=None, **kwargs): 207 | """ Basic plot over an ImageCollection 208 | 209 | :param region: the region to reduce over an get a (unique) value 210 | :type region: ee.Geometry or ee.Feature or ee.FeatureCollection 211 | :param reducer: the reducer to apply over the region. 212 | :param scale: the scale to apply the reducer. If None, will use the 213 | nominal scale of the first band of the first image of the 214 | collection 215 | :param xProperty: the property that will be use for the x axis 216 | :param bands: the bands that will be used for the y axis 217 | :param label_bands: the names for the series of bands. Have to match 218 | the length of bands 219 | :param properties: the properties that will be used for the y axis 220 | :param label_properties: the names for the series of properties. Have 221 | to match the length of properties 222 | :return: a linear chart 223 | :rtype: pygal.XY 224 | """ 225 | Image.check_imageCollection(imageCollection) 226 | 227 | # first image (for getting bands and properties) 228 | first = ee.Image(imageCollection.first()) 229 | allbands = first.bandNames().getInfo() 230 | 231 | # scale 232 | if not scale: 233 | scale = first.select(0).projection().nominalScale() 234 | 235 | # Get Y (bands) 236 | if not bands and not properties: 237 | bands = allbands 238 | properties = [] 239 | label_properties = [] 240 | if not label_bands: 241 | label_bands = allbands 242 | elif bands and not properties: 243 | properties = [] 244 | label_properties = [] 245 | if not label_bands: 246 | label_bands = bands 247 | elif properties and not bands: 248 | bands = [] 249 | label_bands = [] 250 | if not label_properties: 251 | label_properties = properties 252 | else: 253 | if not label_bands: 254 | label_bands = bands 255 | if not label_properties: 256 | label_properties = properties 257 | 258 | # Check for consistance 259 | if len(bands) != len(label_bands): 260 | msg = 'The number of the labels for bands must be equal to the ' \ 261 | 'number of parsed bands. Found {} and {}'.format( 262 | len(label_bands), len(bands)) 263 | raise ValueError(msg) 264 | 265 | if len(properties) != len(label_properties): 266 | msg = 'The number of the labels for properties must be equal to ' \ 267 | 'the number of parsed properties. Found {} and {}'.format( 268 | len(label_properties), len(properties)) 269 | raise ValueError(msg) 270 | 271 | # Select bands 272 | imageCollection = imageCollection.select(bands) 273 | 274 | # If xProperty == 'system:time_start' will compute datetime 275 | datetime = True if xProperty == 'system:time_start' else False 276 | 277 | # generate data 278 | if isinstance(region, ee.Geometry): 279 | geom = region 280 | elif isinstance(region, (ee.Feature, ee.FeatureCollection)): 281 | geom = region.geometry() 282 | else: 283 | msg = 'Parameter `region` must be `ee.Geometry`, `ee.Feautre` or' \ 284 | ' or `ee.FeatureCollection, found {}' 285 | raise ValueError(msg.format(type(region))) 286 | 287 | x_properties = [xProperty] 288 | 289 | if properties is not None: 290 | x_properties = x_properties + properties 291 | 292 | data = tools.imagecollection.getValues( 293 | collection=imageCollection, 294 | geometry=geom, 295 | reducer=reducer, 296 | scale=scale, 297 | properties=x_properties, 298 | side='client') 299 | 300 | if label_bands and label_properties: 301 | ydata = label_bands + label_properties 302 | elif label_bands and not label_properties: 303 | ydata = label_bands 304 | else: 305 | ydata = label_properties 306 | 307 | # Replace band names with labels provided 308 | if label_bands: 309 | for iid, values_dict in data.items(): 310 | for old_name, new_name in zip(bands, label_bands): 311 | if new_name != old_name: 312 | data[iid][new_name] = data[iid][old_name] 313 | data[iid].pop(old_name) 314 | 315 | # Replace property names with labels provided 316 | if label_properties: 317 | for iid, values_dict in data.items(): 318 | for old_name, new_name in zip(properties, label_properties): 319 | if new_name != old_name: 320 | data[iid][new_name] = data[iid][old_name] 321 | data[iid].pop(old_name) 322 | 323 | df = tools.imagecollection.data2pandas(data) 324 | newdf = df.sort_values(xProperty) 325 | 326 | if datetime: 327 | chart = DateTimeLine(**kwargs) 328 | else: 329 | chart = Line(**kwargs) 330 | 331 | line_chart = fromPandas(chart, newdf, y=ydata, x=xProperty, 332 | datetime=datetime) 333 | 334 | if isinstance(reducer, str): 335 | reducer_name = reducer 336 | else: 337 | reducer_name = reducer.getInfo()['type'].split('.')[1] 338 | 339 | chart_title = 'Band {} in relation with {} across images' 340 | chart_title = chart_title.format(reducer_name, xProperty) 341 | line_chart.title = chart_title 342 | 343 | return line_chart 344 | 345 | @staticmethod 346 | def seriesByRegion(imageCollection, regions, reducer, band=None, 347 | scale=None, xProperty='system:time_start', 348 | seriesProperty='system:index'): 349 | 350 | # If xProperty == 'system:time_start' will compute datetime 351 | datetime = True if xProperty == 'system:time_start' else False 352 | 353 | Image.check_imageCollection(imageCollection) 354 | first = ee.Image(imageCollection.first()) 355 | 356 | # Make defaults 357 | if not band: 358 | band = ee.String(first.bandNames().get(0)) 359 | if not scale: 360 | # scale = first.select([0]).projection().nominalScale() 361 | scale = 1 362 | 363 | # select band 364 | imageCollection = imageCollection.select(band) 365 | 366 | # Generate data 367 | # Geometry 368 | if isinstance(regions, ee.Geometry): 369 | print('Using `seriesByRegion` with `ee.Geometry` will give you' 370 | ' the same output as `series`, use that method instead') 371 | 372 | chart_title = '{} values in merged geometry in relation with {}' 373 | chart_title = chart_title.format(band, xProperty) 374 | 375 | chart_line = Image.series(imageCollection, regions, reducer, 376 | scale=scale, xProperty=xProperty, 377 | bands=[band], labels=['geometry']) 378 | chart_line.title = chart_title 379 | return chart_line 380 | 381 | elif isinstance(regions, ee.Feature): 382 | reducer_name = reducer.getInfo()['type'].split('.')[1] 383 | 384 | chart_title = '{} {} values in one regions in relation ' \ 385 | 'with {}\nlabeled by {}' 386 | chart_title = chart_title.format(band, reducer_name, 387 | xProperty, seriesProperty) 388 | 389 | label = regions.get(seriesProperty).getInfo() 390 | label = label if label else 'unknown feature' 391 | chart_line = Image.series(imageCollection, regions, reducer, 392 | scale=scale, xProperty=xProperty, 393 | bands=[band], labels=[label]) 394 | chart_line.title = chart_title 395 | return chart_line 396 | 397 | elif isinstance(regions, ee.FeatureCollection): 398 | 399 | def over_col(img, inicol): 400 | inicol = ee.Dictionary(inicol) 401 | x_prop = img.get(xProperty) 402 | iid = img.get('system:index') 403 | 404 | def over_fc(feat, inifeat): 405 | inifeat = ee.Dictionary(inifeat) 406 | name = feat.get(seriesProperty) 407 | data = img.reduceRegion(reducer, 408 | feat.geometry(), 409 | scale).get(band) 410 | return inifeat.set(name, data) 411 | 412 | fc_data = ee.Dictionary( 413 | regions.iterate(over_fc, ee.Dictionary({})) 414 | ).set(xProperty, x_prop) 415 | 416 | return inicol.set(iid, fc_data) 417 | 418 | data = ee.Dictionary( 419 | imageCollection.iterate(over_col, ee.Dictionary({}))) 420 | 421 | data = data.getInfo() 422 | 423 | df = tools.imagecollection.data2pandas(data) 424 | newdf = df.sort_values(xProperty) 425 | 426 | y_labels = newdf.columns.values.tolist() 427 | 428 | if datetime: 429 | chart = DateTimeLine() 430 | else: 431 | chart = Line() 432 | 433 | line_chart = fromPandas(chart, newdf, y=y_labels, 434 | x=xProperty, datetime=datetime) 435 | 436 | reducer_name = reducer.getInfo()['type'].split('.')[1] 437 | chart_title = '{} {} values in different regions in relation ' \ 438 | 'with {}\nlabeled by {}' 439 | chart_title = chart_title.format(band, reducer_name, 440 | xProperty, seriesProperty) 441 | line_chart.title = chart_title 442 | 443 | return line_chart 444 | 445 | 446 | @staticmethod 447 | def bandsByRegion(image, collection, xProperty='system:index', bands=None, 448 | reducer='mean', scale=None, labels=None, **kwargs): 449 | """ Plot values for each region given an xBand 450 | 451 | :param image: The image to get the values from 452 | :param collection: The collection must be contained in the image 453 | :type collection: ee.FeatureCollection 454 | :param xProperty: The property of the Features that will be used as 455 | x value. If a band is parsed, the value will be obtained using 456 | the parsed reducer 457 | :param bands: the band to be in the y axis 458 | :param reducer: the reducer to apply in each Feature 459 | :param scale: the scale to apply the reducer 460 | :return: a chart 461 | :rtype: pygal.XY 462 | """ 463 | allbands = image.bandNames().getInfo() 464 | 465 | xProperty_is_band = xProperty in allbands 466 | 467 | if not bands: 468 | bands = [band for band in allbands] 469 | 470 | if not labels: 471 | labels = [band for band in bands] 472 | 473 | if xProperty_is_band and xProperty not in bands: 474 | bands.append(xProperty) 475 | labels.append(xProperty) 476 | 477 | # Check for consistance 478 | if len(labels) != len(bands): 479 | msg = 'The number of labels provided must be the same as the ' \ 480 | 'number of bands. Found {} and {}'.format( 481 | len(labels), len(bands)) 482 | raise ValueError(msg) 483 | 484 | if xProperty == 'system:index': 485 | xProperty = None 486 | 487 | if isinstance(collection, ee.Geometry): 488 | msg = 'the parsed collection must be a FeatureCollection or a ' \ 489 | 'Feature' 490 | raise ValueError(msg) 491 | 492 | if isinstance(collection, ee.Feature): 493 | collection = ee.FeatureCollection([collection]) 494 | 495 | image = image.select(bands, labels) 496 | 497 | fc = image.reduceRegions(collection=collection, reducer=reducer, 498 | scale=scale, **kwargs) 499 | 500 | if isinstance(reducer, ee.Reducer): 501 | rname = utils.getReducerName(reducer) 502 | else: 503 | rname = reducer 504 | 505 | if len(bands) == 1: 506 | band = bands[0] 507 | def rename(feat): 508 | condition = feat.propertyNames().contains(rname) 509 | return ee.Algorithms.If( 510 | condition, ee.Feature(feat).select([rname], [band]), feat) 511 | 512 | fc = fc.map(rename) 513 | 514 | pd = utils.reduceRegionsPandas(fc) 515 | 516 | l = Line() 517 | 518 | line_chart = fromPandas(l, pd, x=xProperty, y=labels) 519 | 520 | chart_title = 'Band {} in relation with {} across features' 521 | chart_title = chart_title.format(rname, xProperty) 522 | line_chart.title = chart_title 523 | 524 | return line_chart 525 | -------------------------------------------------------------------------------- /ipygee/tasks.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | """ Google Earth Engine Task Manager """ 4 | from .widgets import CheckAccordion, ConfirmationWidget 5 | from datetime import timedelta, datetime 6 | from collections import namedtuple 7 | from dateutil import tz 8 | from ipygee import utils 9 | from ipywidgets import * 10 | from .threading import Thread 11 | import time 12 | import ee 13 | import re 14 | 15 | EPOCH = datetime(1970, 1, 1, 0, 0, tzinfo=tz.tzutc()) 16 | 17 | TEMPLATES = dict() 18 | TEMPLATES['PENDING'] = """ 19 | state: {state}
    20 | created on: {creation}
    21 | ellapsed since creation: {elapsed} 22 | """ 23 | TEMPLATES['RUNNING'] = """ 24 | state: {state}
    25 | created on: {creation}
    26 | started running on: {start}
    27 | ellapsed since creation: {elapsed}
    28 | running: {running}
    29 | waiting: {waiting} 30 | """ 31 | TEMPLATES['SUCCEEDED'] = """ 32 | state: {state}
    33 | created on: {creation}
    34 | started running on: {start}
    35 | finished running on: {finish}
    36 | running: {running}
    37 | waiting: {waiting}
    38 | URLs: {url} 39 | """ 40 | TEMPLATES['FAILED'] = """ 41 | state: {state}
    42 | created on: {creation}
    43 | started running on: {start}
    44 | failed on: {failed}
    45 | ellapsed since creation: {elapsed}
    46 | running: {running}
    47 | waiting: {waiting}
    48 | error: {error} 49 | """ 50 | TEMPLATES['CANCELLED'] = """ 51 | state: {state}
    52 | created on: {creation}
    53 | cancelled on: {cancel}
    54 | running: {running}
    55 | ellapsed since creation: {elapsed}
    56 | waiting: {waiting} 57 | """ 58 | 59 | # HELPERS 60 | def now(): 61 | return datetime.now().astimezone(tz.tzlocal()) 62 | 63 | 64 | def get_asset_id(url): 65 | """ get the assetId from a URL """ 66 | try: 67 | assetId = re.search('users/.+', url).group() 68 | except AttributeError: 69 | try: 70 | assetId = re.search('projects/.+', url).group() 71 | except AttributeError: 72 | try: 73 | assetId = url.split('?')[1].split('=')[1] 74 | except: 75 | assetId = None 76 | 77 | return assetId 78 | 79 | 80 | def fromisoformat(iso): 81 | """ for python versions < 3.7 get datetime from isoformat """ 82 | d, t = iso.split('T') 83 | year, month, day = d.split('-') 84 | hours, minutes, seconds = t.split(':') 85 | seconds = float(seconds[0:-1]) 86 | sec = int(seconds) 87 | microseconds = int((seconds-sec)*1e6) 88 | 89 | return datetime(int(year), int(month), int(day), int(hours), int(minutes), sec, microseconds) 90 | 91 | 92 | class Task(object): 93 | 94 | @staticmethod 95 | def delta2millis(dt): 96 | """ convert a timedelta to milliseconds """ 97 | return dt.total_seconds() * 1000 98 | 99 | @staticmethod 100 | def millis2delta(ms): 101 | """ convert a timedelta to milliseconds""" 102 | return timedelta(seconds= ms / 1000) 103 | 104 | @staticmethod 105 | def _create_tuple_datetime(name, time): 106 | nt = namedtuple(name, ('utc', 'local', 'str')) 107 | if not time: 108 | return nt(utc=None, local=None, str='') 109 | 110 | try: 111 | utc = datetime.fromisoformat(time) 112 | except AttributeError: 113 | utc = fromisoformat(time) 114 | except ValueError: 115 | utc = fromisoformat(time) 116 | 117 | utc = utc.replace(tzinfo=tz.tzutc()) 118 | try: 119 | local = utc.astimezone(tz.tzlocal()) 120 | except OSError: 121 | local = utc 122 | string = local.isoformat() 123 | return nt(utc=utc, local=local, str=string) 124 | 125 | @staticmethod 126 | def _create_tuple_timedelta(name, delta): 127 | nt = namedtuple(name, ('delta', 'str')) 128 | if not delta: 129 | return nt(delta=None, str='0s') 130 | 131 | seconds = delta.total_seconds() 132 | string = utils.format_elapsed(seconds) 133 | return nt(delta=delta, str=string) 134 | 135 | def __init__(self, task=None, name=None, templates=TEMPLATES, **kwargs): 136 | super(Task, self).__init__(**kwargs) 137 | self.templates = templates 138 | if task: 139 | self.task = task 140 | self.name = task.get('name') 141 | elif name: 142 | self.name = name 143 | self.task = ee.data.getOperation(name) 144 | 145 | @property 146 | def status(self): 147 | return ee.data.getOperation(self.name) 148 | 149 | @property 150 | def metadata(self): 151 | return self.task.get('metadata') 152 | 153 | @property 154 | def state(self): 155 | return self.metadata.get('state') 156 | 157 | @property 158 | def description(self): 159 | return self.metadata.get('description') 160 | 161 | @property 162 | def response(self): 163 | return self.metadata.get('response') 164 | 165 | @property 166 | def task_type(self): 167 | return self.metadata.get('type') 168 | 169 | @property 170 | def create_time(self): 171 | t = self.metadata.get('createTime') 172 | return t[:-1] if t else None 173 | 174 | @property 175 | def update_time(self): 176 | t = self.metadata.get('updateTime') 177 | return t[:-1] if t else None 178 | 179 | @property 180 | def start_time(self): 181 | t = self.metadata.get('startTime') 182 | return t[:-1] if t else None 183 | 184 | def update(self): 185 | self.task = self.status 186 | 187 | def cancel(self): 188 | ee.data.cancelOperation(self.name) 189 | 190 | def is_done(self): 191 | done = self.task.get('done') 192 | return done or False 193 | 194 | def has_started(self): 195 | return False if self.started.utc == EPOCH else True 196 | 197 | def has_finished(self): 198 | return True if self.state == 'SUCCEEDED' else False 199 | 200 | def has_failed(self): 201 | return True if self.state == 'FAILED' else False 202 | 203 | def is_running(self): 204 | return True if self.state == 'RUNNING' else False 205 | 206 | def is_pending(self): 207 | return True if self.state == 'PENDING' else False 208 | 209 | def is_cancelled(self): 210 | return True if self.state == 'CANCELLED' else False 211 | 212 | @property 213 | def uris(self): 214 | return self.metadata.get('destinationUris') 215 | 216 | @property 217 | def error(self): 218 | err = self.task.get('error') 219 | msg = err.get('message') if err else '' 220 | return msg 221 | 222 | @property 223 | def created(self): 224 | return self._create_tuple_datetime('Created', self.create_time) 225 | 226 | @property 227 | def started(self): 228 | return self._create_tuple_datetime('Started', self.start_time) 229 | 230 | @property 231 | def updated(self): 232 | return self._create_tuple_datetime('Updated', self.update_time) 233 | 234 | @property 235 | def finished(self): 236 | t = self.update_time if self.has_finished() else 0 237 | return self._create_tuple_datetime('Finished', t) 238 | 239 | @property 240 | def failed(self): 241 | t = self.update_time if self.has_failed() else None 242 | return self._create_tuple_datetime('Failed', t) 243 | 244 | @property 245 | def cancelled(self): 246 | t = self.update_time if self.is_cancelled() else 0 247 | return self._create_tuple_datetime('Cancelled', t) 248 | 249 | @property 250 | def title(self): 251 | if not (self.has_finished() or self.has_failed() or self.is_cancelled()): 252 | value = '{} ({})'.format(self.description, self.task_type) 253 | else: 254 | value = '{} ({}) [{}]'.format(self.description, self.task_type, self.running.str) 255 | 256 | return value 257 | 258 | @property 259 | def since_creation(self): 260 | td = datetime.now().astimezone(tz.tzlocal()) - self.created.local 261 | return self._create_tuple_timedelta('SinceCreation', td) 262 | 263 | @property 264 | def waiting(self): 265 | if not self.has_started(): 266 | if self.is_cancelled(): 267 | td = self.cancelled.local - self.created.local 268 | else: 269 | td = now() - self.created.local 270 | else: 271 | td = self.started.local - self.created.local 272 | 273 | return self._create_tuple_timedelta('Waiting', td) 274 | 275 | @property 276 | def running(self): 277 | if not self.has_started(): 278 | td = timedelta(0) 279 | 280 | if self.has_started(): 281 | if self.is_running(): 282 | td = now() - self.started.local 283 | 284 | if self.has_finished(): 285 | td = self.finished.local - self.started.local 286 | 287 | if self.has_failed(): 288 | td = self.failed.local - self.started.local 289 | 290 | if self.is_cancelled(): 291 | td = self.cancelled.local - self.started.local 292 | 293 | return self._create_tuple_timedelta('Running', td) 294 | 295 | def urls(self): 296 | if self.uris: 297 | return self.uris 298 | else: 299 | return [] 300 | 301 | def html(self): 302 | template = self.templates.get(self.state) 303 | return template.format( 304 | creation = self.created.str, 305 | elapsed = self.since_creation.str, 306 | start = self.started.str, 307 | running = self.running.str, 308 | finish = self.finished.str, 309 | url = '
    '.join(self.urls()) if self.urls() else '', 310 | error = self.error, 311 | failed = self.failed.str, 312 | cancel = self.cancelled.str, 313 | waiting = self.waiting.str, 314 | state = self.state 315 | ) 316 | 317 | 318 | class TaskList(CheckAccordion): 319 | def __init__(self, raw=None, limit=None, **kwargs): 320 | self.tasks = list() 321 | self.limit = limit 322 | self.raw = raw or [] 323 | super(TaskList, self).__init__(widgets=self.tasks, **kwargs) 324 | 325 | def size(self): 326 | return len(self.raw) 327 | 328 | def filter(self, value='PENDING', by='state'): 329 | """ Filter the Task List and return a new Widget """ 330 | if by == 'state': 331 | filtered = [task for task in self.raw if task['metadata']['state'] == value] 332 | return TaskList(filtered) 333 | 334 | def make(self): 335 | tasklist = list() 336 | taskwid = list() 337 | titles = list() 338 | if not self.limit: 339 | limit = self.raw 340 | else: 341 | limit = self.raw[0:self.limit] 342 | 343 | for task in limit: 344 | t = Task(task=task) 345 | tasklist.append(t) 346 | taskwid.append(HTML(t.html())) 347 | titles.append(t.title) 348 | 349 | self.tasks = tasklist 350 | self.widgets = taskwid 351 | self.set_titles(titles) 352 | 353 | def selected_tasks(self): 354 | """ Get the selected tasks """ 355 | selected = self.checked_rows() 356 | return [self.tasks[sel] for sel in selected] 357 | 358 | def update_all_tasks(self): 359 | for i in range(self.size()): 360 | self.update_position(i) 361 | 362 | def update_position(self, position): 363 | task = self.tasks[position] 364 | wids = self.widgets 365 | title = task.title 366 | wids[position].value = 'Loading...' 367 | task.update() 368 | newcontent = task.html() 369 | wids[position].value = newcontent 370 | 371 | def cancel_position(self, position): 372 | task = self.tasks[position] 373 | task.cancel() 374 | wids = self.widgets 375 | wids[position].value = 'Cancelled request sent' 376 | 377 | def update_selected(self): 378 | selected = self.checked_rows() 379 | for sel in selected: 380 | self.update_position(sel) 381 | 382 | def cancel_selected(self): 383 | selected = self.checked_rows() 384 | name_list = [self.tasks[sel].title for sel in selected if self.tasks[sel].is_running()] 385 | if (name_list): 386 | names = '' 387 | for name in name_list: 388 | names += '
  • '+name+'
  • ' 389 | title = 'Cancelling selected tasks' 390 | legend = 'Are you sure you want to cancel the following tasks??
    '.format(names) 391 | def yes(button=None): 392 | for sel in selected: 393 | self.cancel_position(sel) 394 | confirmation = ConfirmationWidget(title=title, legend=legend, 395 | handle_yes=yes) 396 | return confirmation 397 | 398 | 399 | class TaskHeader(HBox): 400 | def __init__(self, tab, logger, **kwargs): 401 | super(TaskHeader, self).__init__(**kwargs) 402 | self.tab = tab 403 | self.logger = logger 404 | self.checkbox = Checkbox(indent=False, 405 | layout=Layout(flex='1 1 20', width='auto')) 406 | self.cancel_selected = Button(description='Cancel Selected', 407 | tooltip='Cancel all selected tasks') 408 | self.refresh_all = Button(description='Refresh', 409 | tooltip='Refresh Tasks List') 410 | self.update_selected = Button(description='Update Selected', 411 | tooltip='Update Selected Tasks') 412 | self.autorefresh = ToggleButton(description='auto-refresh', 413 | tooltip='click to enable/disable autorefresh') 414 | self.slider = IntSlider(min=5, max=120, step=1, value=15) 415 | self.children = [self.checkbox, self.refresh_all, self.update_selected, 416 | self.cancel_selected, self.autorefresh, self.slider] 417 | 418 | # Set handlers 419 | self.refresh_all.on_click(self.on_refresh_all) 420 | self.update_selected.on_click(self.on_update_selected) 421 | self.cancel_selected.on_click(self.on_cancel_selected) 422 | self.checkbox.observe(self.select_all, names='value') 423 | self.autorefresh.observe(self.autorefresh_handler, names='value') 424 | 425 | def tab_handler(change): 426 | self.checkbox.value = False 427 | self.tab.observe(tab_handler, names='selected_index') 428 | 429 | # Handlers 430 | def on_refresh_all(self, button=None): 431 | self.tab.refresh() 432 | 433 | def on_update_selected(self, button): 434 | self.tab.update_selected() 435 | 436 | def on_cancel_selected(self, button): 437 | wid = self.tab.cancel_selected() 438 | self.logger.children = [wid] 439 | 440 | def emptyLogger(button=None): 441 | self.logger.children = [] 442 | self.on_refresh_all() 443 | 444 | # add handlers 445 | wid.yes.on_click(emptyLogger) # this adds a second handler for yes 446 | wid.no.on_click(emptyLogger) 447 | wid.cancel.on_click(emptyLogger) 448 | 449 | def select_all(self, change): 450 | value = change['new'] 451 | TL = self.tab.get_tasklist() 452 | for checkrow in TL.children: 453 | checkrow.checkbox.value = value 454 | 455 | def autorefresh_loop(self, slider): 456 | while True: 457 | time.sleep(slider.value) 458 | self.tab.refresh() 459 | 460 | def autorefresh_handler(self, change): 461 | value = change['new'] 462 | owner = change['owner'] 463 | if value: 464 | p = Thread(target=self.autorefresh_loop, args=(self.slider,)) 465 | p.start() 466 | owner.process = p 467 | else: 468 | owner.process.terminate() 469 | owner.process.join() 470 | 471 | 472 | class TaskTab(Tab): 473 | categories = ['PENDING', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCELLED'] 474 | TL_index = 0 475 | 476 | def __init__(self, limit=None, **kwargs): 477 | super(TaskTab, self).__init__(**kwargs) 478 | self.limit = limit 479 | child = [] 480 | 481 | for cat in self.categories: 482 | child.append(VBox()) 483 | 484 | self.children = child 485 | self.make_tasklist() 486 | self.complete_titles() 487 | self.make(0) 488 | self.rendered = [0] 489 | 490 | def on_select(change): 491 | name = change.get('name') 492 | if name == 'selected_index': 493 | i = change.get('new') 494 | this = change.get('owner') 495 | if i not in self.rendered: 496 | self.show_loading(i) 497 | self.make(i) 498 | self.rendered.append(i) 499 | 500 | self.observe(on_select) 501 | 502 | def show_loading(self, i): 503 | category = self.categories[i] 504 | vbox = self.children[i] 505 | vbox.children = [Label('Loading...')] 506 | 507 | def make(self, i): 508 | """ Make a category TaskList """ 509 | category = self.categories[i] 510 | vbox = self.children[i] 511 | TL = self.TL.filter(category) 512 | if self.limit: 513 | TL.limit = self.limit 514 | TL.make() 515 | vbox.children = [TL] 516 | 517 | def make_tasklist(self): 518 | self.tasklist = ee.data.listOperations() 519 | self.TL = TaskList(self.tasklist, self.limit) 520 | 521 | def get_tasklist(self, i=None): 522 | """ Get the TaskList from a category """ 523 | i = i or self.selected_index 524 | vbox = self.children[i] 525 | TL = vbox.children[self.TL_index] 526 | return TL 527 | 528 | def complete_titles(self): 529 | for category in self.categories: 530 | i = self.categories.index(category) 531 | TL = self.TL.filter(category) 532 | size = TL.size() 533 | if self.limit and size>self.limit: 534 | size_str = '{}+'.format(self.limit) 535 | else: 536 | size_str = '{}'.format(size) 537 | 538 | name = '{} [{}]'.format(category, size_str) 539 | self.set_title(i, name) 540 | 541 | def refresh(self): 542 | i = self.selected_index 543 | self.show_loading(i) 544 | self.make_tasklist() 545 | self.complete_titles() 546 | self.make(i) 547 | self.rendered = [i] 548 | 549 | def update_selected(self, i=None): 550 | TL = self.get_tasklist(i) 551 | TL.update_selected() 552 | 553 | def cancel_selected(self, i=None): 554 | TL = self.get_tasklist(i) 555 | return TL.cancel_selected() 556 | 557 | 558 | class TaskManager(VBox): 559 | def __init__(self, limit=None, **kwargs): 560 | super(TaskManager, self).__init__(**kwargs) 561 | self.tabs = TaskTab(limit) 562 | self.logger = HBox() 563 | self.header = TaskHeader(self.tabs, self.logger) 564 | self.children = [self.header, self.logger, self.tabs] 565 | 566 | def selected_tasks(self): 567 | i = self.tabs.selected_index 568 | vbox = self.tabs.children[i] 569 | TL = vbox.children[0] 570 | return TL.selected_tasks() 571 | 572 | @staticmethod 573 | def _sum_deltas(deltas): 574 | if len(deltas) > 1: 575 | total = deltas[0] 576 | rest = deltas[1:] 577 | for d in rest: 578 | total += d 579 | elif len(deltas) == 1: 580 | total = deltas[0] 581 | else: 582 | total = timedelta(0) 583 | 584 | return total 585 | 586 | def waiting_time(self): 587 | """ Compute the waiting time of the selected tasks. It does not sum 588 | all waiting times but counts from the time of the first created to the 589 | time of the one that started at last 590 | """ 591 | tasks = self.selected_tasks() 592 | delta = None 593 | if tasks: 594 | created = [task.created.local for task in tasks] 595 | min_created = min(created) 596 | started = [] 597 | for task in tasks: 598 | if task.has_started(): 599 | started.append(task.started.local) 600 | if started: 601 | max_started = max(started) 602 | delta = max_started - min_created 603 | return delta 604 | 605 | def running_time(self): 606 | """ Compute the running time. It does not sum all running times but 607 | counts from the time of the first that started running to the last 608 | that finished 609 | """ 610 | tasks = self.selected_tasks() 611 | delta = None 612 | if tasks: 613 | started = [] 614 | finished = [] 615 | for task in tasks: 616 | if task.has_started() and task.has_finished(): 617 | started.append(task.started.local) 618 | finished.append(task.finished.local) 619 | if started and finished: 620 | min_started = min(started) 621 | max_finished = max(finished) 622 | delta = max_finished - min_started 623 | return delta 624 | 625 | def total_time(self): 626 | """ Compute running time + waiting time. It is NOT the same as summing 627 | both separately 628 | """ 629 | tasks = self.selected_tasks() 630 | delta = None 631 | if tasks: 632 | created = [] 633 | finished = [] 634 | for task in tasks: 635 | if task.has_started() and task.has_finished(): 636 | created.append(task.created.local) 637 | finished.append(task.finished.local) 638 | if created and finished: 639 | min_created = min(created) 640 | max_finished = max(finished) 641 | delta = max_finished - min_created 642 | return delta -------------------------------------------------------------------------------- /examples/Chart.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# chart module\n", 8 | "\n", 9 | "This module relies on `pygal` library, so the returned charts are instances of `pygal.chart`. See options at \n", 10 | "[pygal site][1]\n", 11 | "\n", 12 | "I made a JavaScript 'equivalent': https://code.earthengine.google.com/b2922b860b85c1120250794fb82dfda8\n", 13 | "\n", 14 | " [1]: http://www.pygal.org/en/latest/documentation/index.html" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": 1, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import ee\n", 24 | "ee.Initialize()" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": 2, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "from ipygee import *" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": 3, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "test_site = ee.Geometry.Point([-71, -42])" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "test_feat = ee.Feature(test_site, {'name': 'test feature', 'buffer':0})" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": 5, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "test_featcol = ee.FeatureCollection([\n", 61 | " test_feat, \n", 62 | " test_feat.buffer(100).set('name', 'buffer 100', 'buffer', 100),\n", 63 | " test_feat.buffer(1000).set('name', 'buffer 1000', 'buffer', 1000)\n", 64 | "])" 65 | ] 66 | }, 67 | { 68 | "cell_type": "markdown", 69 | "metadata": {}, 70 | "source": [ 71 | "## Time Series" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 6, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "col = ee.ImageCollection('COPERNICUS/S2').filterBounds(test_site)" 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 7, 86 | "metadata": {}, 87 | "outputs": [], 88 | "source": [ 89 | "time_series = col.filterDate('2018-01-01', '2018-04-01')" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 8, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "bands = ['B1', 'B2', 'B3']" 99 | ] 100 | }, 101 | { 102 | "cell_type": "markdown", 103 | "metadata": {}, 104 | "source": [ 105 | "## **Image.series**\n", 106 | "Chart band values across images in relation with a property or a band" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 9, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "chart_ts = chart.Image.series(**{\n", 116 | " 'imageCollection': time_series, \n", 117 | " 'region': test_site,\n", 118 | " 'scale': 10,\n", 119 | " 'bands': bands,\n", 120 | " 'label_bands':['band B1', 'B2 band', 'this is B3'],\n", 121 | " 'properties':['CLOUD_COVERAGE_ASSESSMENT'],\n", 122 | " 'label_properties':['CLOUD_COVER']\n", 123 | "})" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 10, 129 | "metadata": {}, 130 | "outputs": [ 131 | { 132 | "data": { 133 | "application/vnd.jupyter.widget-view+json": { 134 | "model_id": "5001c69bfc0c409db5f842678a119f4f", 135 | "version_major": 2, 136 | "version_minor": 0 137 | }, 138 | "text/plain": [ 139 | "HTML(value='\n", 159 | "\n", 172 | "\n", 173 | " \n", 174 | " \n", 175 | " \n", 176 | " \n", 177 | " \n", 178 | " \n", 179 | " \n", 180 | " \n", 181 | " \n", 182 | " \n", 183 | " \n", 184 | " \n", 185 | " \n", 186 | " \n", 187 | " \n", 188 | " \n", 189 | " \n", 190 | " \n", 191 | " \n", 192 | " \n", 193 | " \n", 194 | " \n", 195 | " \n", 196 | " \n", 197 | " \n", 198 | " \n", 199 | " \n", 200 | " \n", 201 | " \n", 202 | " \n", 203 | " \n", 204 | " \n", 205 | " \n", 206 | " \n", 207 | " \n", 208 | " \n", 209 | " \n", 210 | " \n", 211 | " \n", 212 | " \n", 213 | " \n", 214 | " \n", 215 | " \n", 216 | " \n", 217 | " \n", 218 | " \n", 219 | " \n", 220 | " \n", 221 | " \n", 222 | " \n", 223 | " \n", 224 | " \n", 225 | " \n", 226 | " \n", 227 | " \n", 228 | " \n", 229 | " \n", 230 | " \n", 231 | " \n", 232 | " \n", 233 | " \n", 234 | " \n", 235 | " \n", 236 | " \n", 237 | " \n", 238 | " \n", 239 | " \n", 240 | " \n", 241 | " \n", 242 | " \n", 243 | " \n", 244 | " \n", 245 | " \n", 246 | " \n", 247 | " \n", 248 | " \n", 249 | " \n", 250 | " \n", 251 | " \n", 252 | " \n", 253 | " \n", 254 | " \n", 255 | " \n", 256 | " \n", 257 | " \n", 258 | " \n", 259 | " \n", 260 | " \n", 261 | " \n", 262 | " \n", 263 | " \n", 264 | " \n", 265 | " \n", 266 | " \n", 267 | " \n", 268 | " \n", 269 | " \n", 270 | " \n", 271 | " \n", 272 | " \n", 273 | " \n", 274 | " \n", 275 | " \n", 276 | " \n", 277 | " \n", 278 | " \n", 279 | " \n", 280 | " \n", 281 | " \n", 282 | " \n", 283 | " \n", 284 | " \n", 285 | " \n", 286 | " \n", 287 | " \n", 288 | " \n", 289 | " \n", 290 | " \n", 291 | " \n", 292 | " \n", 293 | " \n", 294 | " \n", 295 | " \n", 296 | "
    this is B3B2 bandband B1CLOUD_COVER
    2018-01-01 14:37:52.5702673.02749.03622.088.2101
    2018-01-06 14:43:00.5006483.06841.06989.098.1513
    2018-01-16 14:38:41.7401012.01042.01196.00.0646
    2018-01-21 14:41:57.8901035.01070.01232.058.2937
    2018-01-26 14:40:02.1201038.01082.01210.00.2448
    2018-02-05 14:36:43.6901037.01072.01196.00.0204
    2018-02-10 14:42:58.2904290.04153.04730.030.9238
    2018-02-15 14:40:26.3701020.01069.01212.00.0000
    2018-02-20 14:33:53.5101036.01077.01230.00.0121
    2018-02-25 14:37:50.2401019.01091.01234.00.4717
    2018-03-02 14:43:01.8901052.01094.01228.00.0000
    2018-03-07 14:36:40.6901055.01076.01220.00.0148
    2018-03-12 14:36:45.6804814.04903.05176.016.0556
    2018-03-17 14:36:42.6904792.05218.05532.095.7127
    2018-03-22 14:36:47.6801316.01391.01467.056.3747
    2018-03-27 14:32:51.5101318.01378.01521.051.1411
    \n", 297 | "" 298 | ], 299 | "text/plain": [ 300 | " this is B3 B2 band band B1 CLOUD_COVER\n", 301 | "2018-01-01 14:37:52.570 2673.0 2749.0 3622.0 88.2101\n", 302 | "2018-01-06 14:43:00.500 6483.0 6841.0 6989.0 98.1513\n", 303 | "2018-01-16 14:38:41.740 1012.0 1042.0 1196.0 0.0646\n", 304 | "2018-01-21 14:41:57.890 1035.0 1070.0 1232.0 58.2937\n", 305 | "2018-01-26 14:40:02.120 1038.0 1082.0 1210.0 0.2448\n", 306 | "2018-02-05 14:36:43.690 1037.0 1072.0 1196.0 0.0204\n", 307 | "2018-02-10 14:42:58.290 4290.0 4153.0 4730.0 30.9238\n", 308 | "2018-02-15 14:40:26.370 1020.0 1069.0 1212.0 0.0000\n", 309 | "2018-02-20 14:33:53.510 1036.0 1077.0 1230.0 0.0121\n", 310 | "2018-02-25 14:37:50.240 1019.0 1091.0 1234.0 0.4717\n", 311 | "2018-03-02 14:43:01.890 1052.0 1094.0 1228.0 0.0000\n", 312 | "2018-03-07 14:36:40.690 1055.0 1076.0 1220.0 0.0148\n", 313 | "2018-03-12 14:36:45.680 4814.0 4903.0 5176.0 16.0556\n", 314 | "2018-03-17 14:36:42.690 4792.0 5218.0 5532.0 95.7127\n", 315 | "2018-03-22 14:36:47.680 1316.0 1391.0 1467.0 56.3747\n", 316 | "2018-03-27 14:32:51.510 1318.0 1378.0 1521.0 51.1411" 317 | ] 318 | }, 319 | "execution_count": 11, 320 | "metadata": {}, 321 | "output_type": "execute_result" 322 | } 323 | ], 324 | "source": [ 325 | "chart_ts.dataframe" 326 | ] 327 | }, 328 | { 329 | "cell_type": "markdown", 330 | "metadata": {}, 331 | "source": [ 332 | "## **Image.seriesByRegion**\n", 333 | "Chart the value of one band in different regions in relation with a property or a band" 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": 13, 339 | "metadata": {}, 340 | "outputs": [], 341 | "source": [ 342 | "chart_ts_region = chart.Image.seriesByRegion(**{\n", 343 | " 'imageCollection': time_series,\n", 344 | " 'reducer': ee.Reducer.median(),\n", 345 | " 'regions': test_featcol,\n", 346 | " 'scale': 10,\n", 347 | " 'band': 'B11',\n", 348 | " 'seriesProperty': 'name'\n", 349 | "})" 350 | ] 351 | }, 352 | { 353 | "cell_type": "code", 354 | "execution_count": 14, 355 | "metadata": {}, 356 | "outputs": [ 357 | { 358 | "data": { 359 | "application/vnd.jupyter.widget-view+json": { 360 | "model_id": "95b7526f5b6249adbe97144edbd1bcb6", 361 | "version_major": 2, 362 | "version_minor": 0 363 | }, 364 | "text/plain": [ 365 | "HTML(value='\n", 385 | "\n", 398 | "\n", 399 | " \n", 400 | " \n", 401 | " \n", 402 | " \n", 403 | " \n", 404 | " \n", 405 | " \n", 406 | " \n", 407 | " \n", 408 | " \n", 409 | " \n", 410 | " \n", 411 | " \n", 412 | " \n", 413 | " \n", 414 | " \n", 415 | " \n", 416 | " \n", 417 | " \n", 418 | " \n", 419 | " \n", 420 | " \n", 421 | " \n", 422 | " \n", 423 | " \n", 424 | " \n", 425 | " \n", 426 | " \n", 427 | " \n", 428 | " \n", 429 | " \n", 430 | " \n", 431 | " \n", 432 | " \n", 433 | " \n", 434 | " \n", 435 | " \n", 436 | " \n", 437 | " \n", 438 | " \n", 439 | " \n", 440 | " \n", 441 | " \n", 442 | " \n", 443 | " \n", 444 | " \n", 445 | " \n", 446 | " \n", 447 | " \n", 448 | " \n", 449 | " \n", 450 | " \n", 451 | " \n", 452 | " \n", 453 | " \n", 454 | " \n", 455 | " \n", 456 | " \n", 457 | " \n", 458 | " \n", 459 | " \n", 460 | " \n", 461 | " \n", 462 | " \n", 463 | " \n", 464 | " \n", 465 | " \n", 466 | " \n", 467 | " \n", 468 | " \n", 469 | " \n", 470 | " \n", 471 | " \n", 472 | " \n", 473 | " \n", 474 | " \n", 475 | " \n", 476 | " \n", 477 | " \n", 478 | " \n", 479 | " \n", 480 | " \n", 481 | " \n", 482 | " \n", 483 | " \n", 484 | " \n", 485 | " \n", 486 | " \n", 487 | " \n", 488 | " \n", 489 | " \n", 490 | " \n", 491 | " \n", 492 | " \n", 493 | " \n", 494 | " \n", 495 | " \n", 496 | " \n", 497 | " \n", 498 | " \n", 499 | " \n", 500 | " \n", 501 | " \n", 502 | " \n", 503 | " \n", 504 | " \n", 505 | " \n", 506 | " \n", 507 | " \n", 508 | " \n", 509 | " \n", 510 | " \n", 511 | " \n", 512 | " \n", 513 | " \n", 514 | " \n", 515 | " \n", 516 | " \n", 517 | " \n", 518 | " \n", 519 | " \n", 520 | " \n", 521 | " \n", 522 | "
    this is B3B2 bandband B1CLOUD_COVER
    2018-01-01 14:37:52.5702673.02749.03622.088.2101
    2018-01-06 14:43:00.5006483.06841.06989.098.1513
    2018-01-16 14:38:41.7401012.01042.01196.00.0646
    2018-01-21 14:41:57.8901035.01070.01232.058.2937
    2018-01-26 14:40:02.1201038.01082.01210.00.2448
    2018-02-05 14:36:43.6901037.01072.01196.00.0204
    2018-02-10 14:42:58.2904290.04153.04730.030.9238
    2018-02-15 14:40:26.3701020.01069.01212.00.0000
    2018-02-20 14:33:53.5101036.01077.01230.00.0121
    2018-02-25 14:37:50.2401019.01091.01234.00.4717
    2018-03-02 14:43:01.8901052.01094.01228.00.0000
    2018-03-07 14:36:40.6901055.01076.01220.00.0148
    2018-03-12 14:36:45.6804814.04903.05176.016.0556
    2018-03-17 14:36:42.6904792.05218.05532.095.7127
    2018-03-22 14:36:47.6801316.01391.01467.056.3747
    2018-03-27 14:32:51.5101318.01378.01521.051.1411
    \n", 523 | "" 524 | ], 525 | "text/plain": [ 526 | " this is B3 B2 band band B1 CLOUD_COVER\n", 527 | "2018-01-01 14:37:52.570 2673.0 2749.0 3622.0 88.2101\n", 528 | "2018-01-06 14:43:00.500 6483.0 6841.0 6989.0 98.1513\n", 529 | "2018-01-16 14:38:41.740 1012.0 1042.0 1196.0 0.0646\n", 530 | "2018-01-21 14:41:57.890 1035.0 1070.0 1232.0 58.2937\n", 531 | "2018-01-26 14:40:02.120 1038.0 1082.0 1210.0 0.2448\n", 532 | "2018-02-05 14:36:43.690 1037.0 1072.0 1196.0 0.0204\n", 533 | "2018-02-10 14:42:58.290 4290.0 4153.0 4730.0 30.9238\n", 534 | "2018-02-15 14:40:26.370 1020.0 1069.0 1212.0 0.0000\n", 535 | "2018-02-20 14:33:53.510 1036.0 1077.0 1230.0 0.0121\n", 536 | "2018-02-25 14:37:50.240 1019.0 1091.0 1234.0 0.4717\n", 537 | "2018-03-02 14:43:01.890 1052.0 1094.0 1228.0 0.0000\n", 538 | "2018-03-07 14:36:40.690 1055.0 1076.0 1220.0 0.0148\n", 539 | "2018-03-12 14:36:45.680 4814.0 4903.0 5176.0 16.0556\n", 540 | "2018-03-17 14:36:42.690 4792.0 5218.0 5532.0 95.7127\n", 541 | "2018-03-22 14:36:47.680 1316.0 1391.0 1467.0 56.3747\n", 542 | "2018-03-27 14:32:51.510 1318.0 1378.0 1521.0 51.1411" 543 | ] 544 | }, 545 | "execution_count": 15, 546 | "metadata": {}, 547 | "output_type": "execute_result" 548 | } 549 | ], 550 | "source": [ 551 | "chart_ts.dataframe" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "metadata": {}, 557 | "source": [ 558 | "## Chart band values in one image across features in relation with a property value or a band" 559 | ] 560 | }, 561 | { 562 | "cell_type": "code", 563 | "execution_count": 17, 564 | "metadata": {}, 565 | "outputs": [], 566 | "source": [ 567 | "chart_band = chart.Image.bandsByRegion(**{\n", 568 | " 'image': ee.Image(time_series.first()), \n", 569 | " 'collection': test_featcol,\n", 570 | " 'reducer': 'mean',\n", 571 | " 'scale': 10,\n", 572 | " 'bands': ['B1', 'B2', 'B3'],\n", 573 | " 'xProperty': 'buffer', # You can use a band too!\n", 574 | " 'labels': ['B1 mean', 'B2 mean', 'B3 mean']\n", 575 | "})" 576 | ] 577 | }, 578 | { 579 | "cell_type": "code", 580 | "execution_count": 18, 581 | "metadata": {}, 582 | "outputs": [ 583 | { 584 | "data": { 585 | "application/vnd.jupyter.widget-view+json": { 586 | "model_id": "f14ce1a54a0a45039745396015011897", 587 | "version_major": 2, 588 | "version_minor": 0 589 | }, 590 | "text/plain": [ 591 | "HTML(value='\n", 611 | "\n", 624 | "\n", 625 | " \n", 626 | " \n", 627 | " \n", 628 | " \n", 629 | " \n", 630 | " \n", 631 | " \n", 632 | " \n", 633 | " \n", 634 | " \n", 635 | " \n", 636 | " \n", 637 | " \n", 638 | " \n", 639 | " \n", 640 | " \n", 641 | " \n", 642 | " \n", 643 | " \n", 644 | " \n", 645 | " \n", 646 | " \n", 647 | " \n", 648 | " \n", 649 | " \n", 650 | " \n", 651 | " \n", 652 | " \n", 653 | "
    B1 meanB2 meanB3 mean
    03622.0000002749.0000002673.000000
    1003547.1964522781.7264262665.863711
    10003373.6239572983.5752092811.374592
    \n", 654 | "" 655 | ], 656 | "text/plain": [ 657 | " B1 mean B2 mean B3 mean\n", 658 | "0 3622.000000 2749.000000 2673.000000\n", 659 | "100 3547.196452 2781.726426 2665.863711\n", 660 | "1000 3373.623957 2983.575209 2811.374592" 661 | ] 662 | }, 663 | "execution_count": 19, 664 | "metadata": {}, 665 | "output_type": "execute_result" 666 | } 667 | ], 668 | "source": [ 669 | "chart_band.dataframe" 670 | ] 671 | }, 672 | { 673 | "cell_type": "markdown", 674 | "metadata": {}, 675 | "source": [ 676 | "## Concatenate Charts" 677 | ] 678 | }, 679 | { 680 | "cell_type": "markdown", 681 | "metadata": {}, 682 | "source": [ 683 | "### Concatenate mean with median" 684 | ] 685 | }, 686 | { 687 | "cell_type": "code", 688 | "execution_count": 20, 689 | "metadata": {}, 690 | "outputs": [], 691 | "source": [ 692 | "chart_band_median = chart.Image.bandsByRegion(**{\n", 693 | " 'image': ee.Image(time_series.first()), \n", 694 | " 'collection': test_featcol,\n", 695 | " 'reducer': 'median',\n", 696 | " 'scale': 10,\n", 697 | " 'bands': ['B1', 'B2', 'B3'],\n", 698 | " 'xProperty': 'buffer', # You can use a band too!\n", 699 | " 'labels': ['B1 median', 'B2 median', 'B3 median']\n", 700 | "})" 701 | ] 702 | }, 703 | { 704 | "cell_type": "code", 705 | "execution_count": 21, 706 | "metadata": {}, 707 | "outputs": [ 708 | { 709 | "data": { 710 | "application/vnd.jupyter.widget-view+json": { 711 | "model_id": "dfca5f5f2d0a4491a583afcee7933ee1", 712 | "version_major": 2, 713 | "version_minor": 0 714 | }, 715 | "text/plain": [ 716 | "HTML(value=' 0: 71 | # TODO: create widgets only if are in tuple 72 | # Inspector Widget (Accordion) 73 | self.inspector_wid = CustomInspector() 74 | self.inspector_wid.main.selected_index = None # this will unselect all 75 | 76 | # Task Manager Widget 77 | task_manager = TaskManager() 78 | 79 | # Asset Manager Widget 80 | # asset_manager = AssetManager(self) 81 | 82 | # Layers 83 | self.layers_widget = LayersWidget(map=self) 84 | 85 | widgets = {'Inspector': self.inspector_wid, 86 | 'Layers': self.layers_widget, 87 | # 'Assets': asset_manager, 88 | 'Tasks': task_manager, 89 | } 90 | handlers = {'Inspector': self.handle_inspector, 91 | 'Layers': None, 92 | # 'Assets': None, 93 | 'Tasks': None, 94 | } 95 | 96 | # Add tabs and handlers 97 | for tab in tabs: 98 | if tab in widgets.keys(): 99 | widget = widgets[tab] 100 | handler = handlers[tab] 101 | self.addTab(tab, handler, widget) 102 | else: 103 | raise ValueError('Tab {} is not recognized. Choose one of {}'.format(tab, widgets.keys())) 104 | 105 | # First handler: Inspector 106 | self.on_interaction(self.handlers[tabs[0]]) 107 | 108 | # As I cannot create a Geometry with a GeoJSON string I do a workaround 109 | self.draw_types = {'Polygon': ee.Geometry.Polygon, 110 | 'Point': ee.Geometry.Point, 111 | 'LineString': ee.Geometry.LineString, 112 | } 113 | # create EELayers 114 | self.EELayers = OrderedDict() 115 | 116 | def _add_EELayer(self, name, data): 117 | """ Add a pair of name, data to EELayers. Data must be: 118 | 119 | - type: str 120 | - object: ee object 121 | - visParams: dict 122 | - layer: ipyleaflet layer 123 | 124 | """ 125 | copyEELayers = copy(self.EELayers) 126 | copyEELayers[name] = data 127 | self.EELayers = copyEELayers 128 | 129 | def _remove_EELayer(self, name): 130 | """ remove layer from EELayers """ 131 | copyEELayers = copy(self.EELayers) 132 | if name in copyEELayers: 133 | copyEELayers.pop(name) 134 | self.EELayers = copyEELayers 135 | 136 | def addBasemap(self, name, url, **kwargs): 137 | """ Add a basemap with the given URL """ 138 | layer = ipyleaflet.TileLayer(url=url, name=name, base=True, **kwargs) 139 | self.add_layer(layer) 140 | 141 | def setDimensions(self, width=None, height=None): 142 | """ Set the dimensions for the map """ 143 | def check(value, t): 144 | if value is None: return value 145 | if isinstance(value, (int, float)): 146 | return '{}px'.format(value) 147 | elif isinstance(value, (str,)): 148 | search = re.search('(\d+)', value).groups() 149 | intvalue = search[0] 150 | splitted = value.split(intvalue) 151 | units = splitted[1] 152 | if units == '%': 153 | if t == 'width': return '{}%'.format(intvalue) 154 | else: return None 155 | else: 156 | return '{}px'.format(intvalue) 157 | else: 158 | msg = 'parameter {} of setDimensions must be int or str' 159 | raise ValueError(msg.format(t)) 160 | self.layout = Layout(width=check(width, 'width'), 161 | height=check(height, 'height')) 162 | 163 | def moveLayer(self, layer_name, direction='up'): 164 | """ Move one step up a layer """ 165 | names = list(self.EELayers.keys()) 166 | values = list(self.EELayers.values()) 167 | 168 | if direction == 'up': 169 | dir = 1 170 | elif direction == 'down': 171 | dir = -1 172 | else: 173 | dir = 0 174 | 175 | if layer_name in names: # if layer exists 176 | # index and value of layer to move_layer 177 | i = names.index(layer_name) 178 | condition = (i < len(names)-1) if dir == 1 else (i > 0) 179 | if condition: # if layer is not in the edge 180 | ival = values[i] 181 | # new index for layer 182 | newi = i+dir 183 | # get index and value that already exist in the new index 184 | iname_before = names[newi] 185 | ival_before = values[newi] 186 | # Change order 187 | # set layer and value in the new index 188 | names[newi] = layer_name 189 | values[newi] = ival 190 | # set replaced layer and its value in the index of moving layer 191 | names[i] = iname_before 192 | values[i] = ival_before 193 | 194 | newlayers = OrderedDict(zip(names, values)) 195 | self.EELayers = newlayers 196 | 197 | @observe('EELayers') 198 | def _ob_EELayers(self, change): 199 | new = change['new'] 200 | proxy_layers = [l for l in self.layers if l.base == True] 201 | 202 | for val in new.values(): 203 | layer = val['layer'] 204 | proxy_layers.append(layer) 205 | 206 | self.layers = tuple(proxy_layers) 207 | 208 | # UPDATE INSPECTOR 209 | # Clear options 210 | self.inspector_wid.selector.options = {} 211 | # Add layer to the Inspector Widget 212 | self.inspector_wid.selector.options = new # self.EELayers 213 | 214 | # UPDATE LAYERS WIDGET 215 | # update Layers Widget 216 | self.layers_widget.selector.options = {} 217 | self.layers_widget.selector.options = new # self.EELayers 218 | 219 | @property 220 | def addedImages(self): 221 | return sum( 222 | [1 for val in self.EELayers.values() if val['type'] == 'Image']) 223 | 224 | @property 225 | def addedGeometries(self): 226 | return sum( 227 | [1 for val in self.EELayers.values() if val['type'] == 'Geometry']) 228 | 229 | def show(self, tabs=True, layer_control=True, draw_control=False, 230 | fullscreen=True): 231 | """ Show the Map on the Notebook """ 232 | if not self.is_shown: 233 | if layer_control: 234 | # Layers Control 235 | lc = ipyleaflet.LayersControl() 236 | self.add_control(lc) 237 | if draw_control: 238 | # Draw Control 239 | dc = ipyleaflet.DrawControl(edit=False) 240 | dc.on_draw(self.handle_draw) 241 | self.add_control(dc) 242 | if fullscreen: 243 | # Control 244 | full_control = ipyleaflet.FullScreenControl() 245 | self.add_control(full_control) 246 | 247 | if tabs: 248 | display(self, self.tab_widget) 249 | else: 250 | display(self) 251 | else: 252 | if tabs: 253 | display(self, self.tab_widget) 254 | else: 255 | display(self) 256 | 257 | self.is_shown = True 258 | # Start with crosshair cursor 259 | self.default_style = {'cursor': 'crosshair'} 260 | 261 | def showTab(self, name): 262 | """ Show only a Tab Widget by calling its name. This is useful mainly 263 | in Jupyter Lab where you can see outputs in different tab_widget 264 | 265 | :param name: the name of the tab to show 266 | :type name: str 267 | """ 268 | try: 269 | widget = self.tab_children_dict[name] 270 | display(widget) 271 | except: 272 | print('Tab not found') 273 | 274 | def addImage(self, image, visParams=None, name=None, show=True, 275 | opacity=None, replace=True): 276 | """ Add an ee.Image to the Map 277 | 278 | :param image: Image to add to Map 279 | :type image: ee.Image 280 | :param visParams: visualization parameters. Can have the 281 | following arguments: bands, min, max. 282 | :type visParams: dict 283 | :param name: name for the layer 284 | :type name: str 285 | :return: the name of the added layer 286 | :rtype: str 287 | """ 288 | # Check if layer exists 289 | if name in self.EELayers.keys(): 290 | if not replace: 291 | return self.getLayer(name) 292 | else: 293 | # Get URL, attribution & vis params 294 | params = getImageTile(image, visParams, show, opacity) 295 | 296 | # Remove Layer 297 | self.removeLayer(name) 298 | else: 299 | # Get URL, attribution & vis params 300 | params = getImageTile(image, visParams, show, opacity) 301 | 302 | layer = ipyleaflet.TileLayer(url=params['url'], 303 | attribution=params['attribution'], 304 | name=name) 305 | 306 | EELayer = {'type': 'Image', 307 | 'object': image, 308 | 'visParams': params['visParams'], 309 | 'layer': layer} 310 | 311 | # self._add_EELayer(name, EELayer) 312 | # return name 313 | return EELayer 314 | 315 | def addMarker(self, marker, visParams=None, name=None, show=True, 316 | opacity=None, replace=True, 317 | inspect={'data':None, 'reducer':None, 'scale':None}): 318 | """ General method to add Geometries, Features or FeatureCollections 319 | as Markers """ 320 | 321 | if isinstance(marker, ee.Geometry): 322 | self.addGeometry(marker, visParams, name, show, opacity, replace, 323 | inspect) 324 | 325 | elif isinstance(marker, ee.Feature): 326 | self.addFeature(marker, visParams, name, show, opacity, replace, 327 | inspect) 328 | 329 | elif isinstance(marker, ee.FeatureCollection): 330 | geometry = marker.geometry() 331 | self.addGeometry(marker, visParams, name, show, opacity, replace, 332 | inspect) 333 | 334 | def addFeature(self, feature, visParams=None, name=None, show=True, 335 | opacity=None, replace=True, 336 | inspect={'data':None, 'reducer':None, 'scale':None}): 337 | """ Add a Feature to the Map 338 | 339 | :param feature: the Feature to add to Map 340 | :type feature: ee.Feature 341 | :param visParams: 342 | :type visParams: dict 343 | :param name: name for the layer 344 | :type name: str 345 | :param inspect: when adding a geometry or a feature you can pop up data 346 | from a desired layer. Params are: 347 | :data: the EEObject where to get the data from 348 | :reducer: the reducer to use 349 | :scale: the scale to reduce 350 | :type inspect: dict 351 | :return: the name of the added layer 352 | :rtype: str 353 | """ 354 | thename = name if name else 'Feature {}'.format(self.addedGeometries) 355 | 356 | # Check if layer exists 357 | if thename in self.EELayers.keys(): 358 | if not replace: 359 | print("Layer with name '{}' exists already, please choose another name".format(thename)) 360 | return 361 | else: 362 | self.removeLayer(thename) 363 | 364 | params = getGeojsonTile(feature, thename, inspect) 365 | layer = ipyleaflet.GeoJSON(data=params['geojson'], 366 | name=thename, 367 | popup=HTML(params['pop'])) 368 | 369 | self._add_EELayer(thename, {'type': 'Feature', 370 | 'object': feature, 371 | 'visParams': None, 372 | 'layer': layer}) 373 | return thename 374 | 375 | def addGeometry(self, geometry, visParams=None, name=None, show=True, 376 | opacity=None, replace=True, 377 | inspect={'data':None, 'reducer':None, 'scale':None}): 378 | """ Add a Geometry to the Map 379 | 380 | :param geometry: the Geometry to add to Map 381 | :type geometry: ee.Geometry 382 | :param visParams: 383 | :type visParams: dict 384 | :param name: name for the layer 385 | :type name: str 386 | :param inspect: when adding a geometry or a feature you can pop up data 387 | from a desired layer. Params are: 388 | :data: the EEObject where to get the data from 389 | :reducer: the reducer to use 390 | :scale: the scale to reduce 391 | :type inspect: dict 392 | :return: the name of the added layer 393 | :rtype: str 394 | """ 395 | thename = name if name else 'Geometry {}'.format(self.addedGeometries) 396 | 397 | # Check if layer exists 398 | if thename in self.EELayers.keys(): 399 | if not replace: 400 | print("Layer with name '{}' exists already, please choose another name".format(thename)) 401 | return 402 | else: 403 | self.removeLayer(thename) 404 | 405 | params = getGeojsonTile(geometry, thename, inspect) 406 | layer = ipyleaflet.GeoJSON(data=params['geojson'], 407 | name=thename, 408 | popup=HTML(params['pop'])) 409 | 410 | self._add_EELayer(thename, {'type': 'Geometry', 411 | 'object': geometry, 412 | 'visParams':None, 413 | 'layer': layer}) 414 | return thename 415 | 416 | def addFeatureLayer(self, feature, visParams=None, name=None, show=True, 417 | opacity=None, replace=True): 418 | """ Paint a Feature on the map, but the layer underneath is the 419 | actual added Feature """ 420 | 421 | visParams = visParams if visParams else {} 422 | 423 | if isinstance(feature, ee.Feature): 424 | ty = 'Feature' 425 | elif isinstance(feature, ee.FeatureCollection): 426 | ty = 'FeatureCollection' 427 | else: 428 | print('The object is not a Feature or FeatureCollection') 429 | return 430 | 431 | fill_color = visParams.get('fill_color', None) 432 | 433 | if 'outline_color' in visParams: 434 | out_color = visParams['outline_color'] 435 | elif 'border_color' in visParams: 436 | out_color = visParams['border_color'] 437 | else: 438 | out_color = 'black' 439 | 440 | outline = visParams.get('outline', 2) 441 | 442 | proxy_layer = paint(feature, out_color, fill_color, outline) 443 | 444 | thename = name if name else '{} {}'.format(ty, self.addedGeometries) 445 | 446 | img_params = {'bands':['vis-red', 'vis-green', 'vis-blue'], 447 | 'min': 0, 'max':255} 448 | 449 | # Check if layer exists 450 | if thename in self.EELayers.keys(): 451 | if not replace: 452 | print("{} with name '{}' exists already, please choose another name".format(ty, thename)) 453 | return 454 | else: 455 | # Get URL, attribution & vis params 456 | params = getImageTile(proxy_layer, img_params, show, opacity) 457 | 458 | # Remove Layer 459 | self.removeLayer(thename) 460 | else: 461 | # Get URL, attribution & vis params 462 | params = getImageTile(proxy_layer, img_params, show, opacity) 463 | 464 | layer = ipyleaflet.TileLayer(url=params['url'], 465 | attribution=params['attribution'], 466 | name=thename) 467 | 468 | self._add_EELayer(thename, {'type': ty, 469 | 'object': feature, 470 | 'visParams': visParams, 471 | 'layer': layer}) 472 | return thename 473 | 474 | def addMosaic(self, collection, visParams=None, name=None, show=False, 475 | opacity=None, replace=True): 476 | """ Add an ImageCollection to EELayer and its mosaic to the Map. 477 | When using the inspector over this layer, it will print all values from 478 | the collection """ 479 | proxy = ee.ImageCollection(collection).sort('system:time_start') 480 | mosaic = ee.Image(proxy.mosaic()) 481 | 482 | self.addImage(mosaic, visParams, name, show, opacity, replace) 483 | # modify EELayer 484 | # EELayer['type'] = 'ImageCollection' 485 | # EELayer['object'] = ee.ImageCollection(collection) 486 | # return EELayer 487 | 488 | def addImageCollection(self, collection, visParams=None, 489 | namePattern='{id}', show=False, opacity=None, 490 | datePattern='yyyyMMdd', replace=True, 491 | verbose=False): 492 | """ Add every Image of an ImageCollection to the Map 493 | 494 | :param collection: the ImageCollection 495 | :type collection: ee.ImageCollection 496 | :param visParams: visualization parameter for each image. See `addImage` 497 | :type visParams: dict 498 | :param namePattern: the name pattern (uses geetools.utils.makeName) 499 | :type namePattern: str 500 | :param show: If True, adds and shows the Image, otherwise only add it 501 | :type show: bool 502 | """ 503 | size = collection.size() 504 | collist = collection.toList(size) 505 | n = 0 506 | while True: 507 | try: 508 | img = ee.Image(collist.get(n)) 509 | extra = dict(position=n) 510 | name = utils.makeName(img, namePattern, datePattern, extra=extra) 511 | self.addLayer(img, visParams, name.getInfo(), show, opacity, 512 | replace=replace) 513 | if verbose: 514 | print('Adding {} to the Map'.format(name)) 515 | n += 1 516 | except ee.EEException as e: 517 | msg = 'List.get: List index must be between' 518 | if msg not in str(e): 519 | raise e 520 | break 521 | 522 | def addLayer(self, eeObject, visParams=None, name=None, show=True, 523 | opacity=None, replace=True, **kwargs): 524 | """ Adds a given EE object to the map as a layer. 525 | 526 | :param eeObject: Earth Engine object to add to map 527 | :type eeObject: ee.Image || ee.Geometry || ee.Feature 528 | :param replace: if True, if there is a layer with the same name, this 529 | replace that layer. 530 | :type replace: bool 531 | 532 | For ee.Image and ee.ImageCollection see `addImage` 533 | for ee.Geometry and ee.Feature see `addGeometry` 534 | """ 535 | if name in self.EELayers.keys(): 536 | return None 537 | 538 | visParams = visParams if visParams else {} 539 | 540 | # CASE: ee.Image 541 | if isinstance(eeObject, ee.Image): 542 | image_name = name if name else 'Image {}'.format(self.addedImages) 543 | EELayer = self.addImage(eeObject, visParams=visParams, 544 | name=image_name, show=show, 545 | opacity=opacity, replace=replace) 546 | 547 | self._add_EELayer(image_name, EELayer) 548 | 549 | # CASE: ee.Geometry 550 | elif isinstance(eeObject, ee.Geometry): 551 | geom = eeObject if isinstance(eeObject, ee.Geometry) else eeObject.geometry() 552 | kw = {'visParams':visParams, 'name':name, 'show':show, 'opacity':opacity} 553 | if kwargs.get('inspect'): kw.setdefault('inspect', kwargs.get('inspect')) 554 | self.addGeometry(geom, replace=replace, **kw) 555 | 556 | # CASE: ee.Feature & ee.FeatureCollection 557 | elif isinstance(eeObject, ee.Feature) or isinstance(eeObject, ee.FeatureCollection): 558 | feat = eeObject 559 | kw = {'visParams':visParams, 'name':name, 'show':show, 'opacity':opacity} 560 | self.addFeatureLayer(feat, replace=replace, **kw) 561 | 562 | # CASE: ee.ImageCollection 563 | elif isinstance(eeObject, ee.ImageCollection): 564 | thename = name if name else 'ImageCollection {}'.format(self.addedImages) 565 | EELayer = self.addMosaic(eeObject, visParams, thename, show, 566 | opacity, replace) 567 | self._add_EELayer(thename, EELayer) 568 | else: 569 | print("`addLayer` doesn't support adding {} objects to the map".format(type(eeObject))) 570 | 571 | 572 | def removeLayer(self, name): 573 | """ Remove a layer by its name """ 574 | if name in self.EELayers.keys(): 575 | self._remove_EELayer(name) 576 | else: 577 | print('Layer {} is not present in the map'.format(name)) 578 | return 579 | 580 | # GETTERS 581 | def getLayer(self, name): 582 | """ Get a layer by its name 583 | 584 | :param name: the name of the layer 585 | :type name: str 586 | :return: The complete EELayer which is a dict of 587 | 588 | :type: the type of the layer 589 | :object: the EE Object associated with the layer 590 | :visParams: the visualization parameters of the layer 591 | :layer: the TileLayer added to the Map (ipyleaflet.Map) 592 | 593 | :rtype: dict 594 | """ 595 | if name in self.EELayers: 596 | layer = self.EELayers[name] 597 | return layer 598 | else: 599 | print('Layer {} is not present in the map'.format(name)) 600 | return 601 | 602 | def getObject(self, name): 603 | """ Get the EE Object from a layer's name """ 604 | obj = self.getLayer(name)['object'] 605 | return obj 606 | 607 | def getVisParams(self, name): 608 | """ Get the Visualization Parameters from a layer's name """ 609 | vis = self.getLayer(name)['visParams'] 610 | return vis 611 | 612 | def getLayerURL(self, name): 613 | """ Get the layer URL by name """ 614 | layer = self.getLayer(name) 615 | tile = layer['layer'] 616 | return tile.url 617 | 618 | def getCenter(self): 619 | """ Returns the coordinates at the center of the map. 620 | 621 | No arguments. 622 | Returns: Geometry.Point 623 | 624 | :return: 625 | """ 626 | center = self.center 627 | coords = inverseCoordinates(center) 628 | return ee.Geometry.Point(coords) 629 | 630 | def getBounds(self, asGeoJSON=True): 631 | """ Returns the bounds of the current map view, as a list in the 632 | format [west, south, east, north] in degrees. 633 | 634 | Arguments: 635 | asGeoJSON (Boolean, optional): 636 | If true, returns map bounds as GeoJSON. 637 | 638 | Returns: GeoJSONGeometry|List|String 639 | """ 640 | bounds = inverseCoordinates(self.bounds) 641 | if asGeoJSON: 642 | return ee.Geometry.Rectangle(bounds) 643 | else: 644 | return bounds 645 | 646 | def centerObject(self, eeObject, zoom=None, method=1): 647 | """ Center an eeObject 648 | 649 | :param eeObject: 650 | :param zoom: 651 | :param method: experimetal methods to estimate zoom for fitting bounds 652 | Currently: 1 or 2 653 | :type: int 654 | """ 655 | bounds = getBounds(eeObject) 656 | if bounds: 657 | try: 658 | inverse = inverseCoordinates(bounds) 659 | centroid = ee.Geometry.Polygon(inverse) \ 660 | .centroid().getInfo()['coordinates'] 661 | except: 662 | centroid = [0, 0] 663 | 664 | self.center = inverseCoordinates(centroid) 665 | if zoom: 666 | self.zoom = zoom 667 | else: 668 | self.zoom = getZoom(bounds, method) 669 | 670 | def _update_tab_children(self): 671 | """ Update Tab children from tab_children_dict """ 672 | # Set tab_widget children 673 | self.tab_widget.children = tuple(self.tab_children_dict.values()) 674 | # Set tab_widget names 675 | for i, name in enumerate(self.tab_children_dict.keys()): 676 | self.tab_widget.set_title(i, name) 677 | 678 | def addTab(self, name, handler=None, widget=None): 679 | """ Add a Tab to the Panel. The handler is for the Map 680 | 681 | :param name: name for the new tab 682 | :type name: str 683 | :param handler: handle function for the new tab. Arguments of the 684 | function are: 685 | 686 | - type: the type of the event (click, mouseover, etc..) 687 | - coordinates: coordinates where the event occurred [lon, lat] 688 | - widget: the widget inside the Tab 689 | - map: the Map instance 690 | 691 | :param widget: widget inside the Tab. Defaults to HTML('') 692 | :type widget: ipywidgets.Widget 693 | """ 694 | # Widget 695 | wid = widget if widget else HTML('') 696 | # Get tab's children as a list 697 | # tab_children = list(self.tab_widget.children) 698 | tab_children = self.tab_children_dict.values() 699 | # Get a list of tab's titles 700 | # titles = [self.tab_widget.get_title(i) for i, child in enumerate(tab_children)] 701 | titles = self.tab_children_dict.keys() 702 | # Check if tab already exists 703 | if name not in titles: 704 | ntabs = len(tab_children) 705 | 706 | # UPDATE DICTS 707 | # Add widget as a new children 708 | self.tab_children_dict[name] = wid 709 | # Set the handler for the new tab 710 | if handler: 711 | def proxy_handler(f): 712 | def wrap(**kwargs): 713 | # Add widget to handler arguments 714 | kwargs['widget'] = self.tab_children_dict[name] 715 | coords = kwargs['coordinates'] 716 | kwargs['coordinates'] = inverseCoordinates(coords) 717 | kwargs['map'] = self 718 | return f(**kwargs) 719 | return wrap 720 | self.handlers[name] = proxy_handler(handler) 721 | else: 722 | self.handlers[name] = handler 723 | 724 | # Update tab children 725 | self._update_tab_children() 726 | else: 727 | print('Tab {} already exists, please choose another name'.format(name)) 728 | 729 | def removeTab(self, name): 730 | """ Remove a tab by its name """ 731 | children = self.tab_children_dict.keys() 732 | if name in children: 733 | self.tab_children_dict.pop(name) 734 | self._update_tab_children() 735 | 736 | def handle_change_tab(self, change): 737 | """ Handle function to trigger when tab changes """ 738 | # Remove all handlers 739 | if change['name'] == 'selected_index': 740 | old = change['old'] 741 | new = change['new'] 742 | old_name = self.tab_widget.get_title(old) 743 | new_name = self.tab_widget.get_title(new) 744 | # Remove all handlers 745 | for handl in self.handlers.values(): 746 | self.on_interaction(handl, True) 747 | # Set new handler if not None 748 | if new_name in self.handlers.keys(): 749 | handler = self.handlers[new_name] 750 | if handler: 751 | self.on_interaction(handler) 752 | 753 | # Set cursor type 754 | if new_name == 'Inspector': 755 | self.default_style = {'cursor': 'crosshair'} 756 | else: 757 | self.default_style = {'cursor': 'grab'} 758 | 759 | def handle_inspector(self, **change): 760 | """ Handle function for the Inspector Widget """ 761 | # Get click coordinates 762 | coords = change['coordinates'] 763 | 764 | point_name = 'point inspect at {}'.format(coords) 765 | # Widget for adding/removing the point at click 766 | def point_widget(coords): 767 | coords = inverseCoordinates(coords) 768 | add_button = Button(description='ADD', tooltip='add point to map') 769 | rem_button = Button(description='REMOVE', 770 | tooltip='remove point from map') 771 | 772 | def add_func(button=None): 773 | p = ipyleaflet.Marker(name=point_name, 774 | location=coords, 775 | draggable=False) 776 | 777 | self._add_EELayer(point_name, { 778 | 'type': 'temp', 779 | 'object': None, 780 | 'visParams': None, 781 | 'layer': p 782 | }) 783 | 784 | def remove_func(button=None): 785 | self._remove_EELayer(point_name) 786 | 787 | add_button.on_click(add_func) 788 | rem_button.on_click(remove_func) 789 | 790 | return HBox([add_button, rem_button]) 791 | 792 | event = change['type'] # event type 793 | if event == 'click': # If the user clicked 794 | # create a point where the user clicked 795 | point = ee.Geometry.Point(coords) 796 | 797 | # Get widget 798 | thewidget = change['widget'].main # Accordion 799 | 800 | # First Accordion row text (name) 801 | first = 'Point {} at {} zoom'.format(coords, self.zoom) 802 | namelist = [first] 803 | # wids4acc = [HTML('')] # first row has no content 804 | wids4acc = [point_widget(coords)] 805 | 806 | # Get only Selected Layers in the Inspector Selector 807 | selected_layers = dict(zip(self.inspector_wid.selector.label, 808 | self.inspector_wid.selector.value)) 809 | 810 | length = len(selected_layers.keys()) 811 | i = 1 812 | 813 | for name, obj in selected_layers.items(): # for every added layer 814 | # Clear children // Loading 815 | thewidget.children = [HTML('wait a second please..')] 816 | thewidget.set_title(0, 'Loading {} of {}...'.format(i, length)) 817 | i += 1 818 | 819 | # Image 820 | if obj['type'] == 'Image': 821 | # Get the image's values 822 | try: 823 | image = obj['object'] 824 | values = tools.image.getValue(image, point, 825 | scale=ZOOM_SCALES[self.zoom], 826 | side='client') 827 | values = tools.dictionary.sort(values) 828 | # Create the content 829 | img_html = '' 830 | for band, value in values.items(): 831 | img_html += '{}: {}
    '.format(band, 832 | value) 833 | wid = HTML(img_html) 834 | # append widget to list of widgets 835 | wids4acc.append(wid) 836 | namelist.append(name) 837 | except Exception as e: 838 | # wid = HTML(str(e).replace('<','{').replace('>','}')) 839 | exc_type, exc_value, exc_traceback = sys.exc_info() 840 | trace = traceback.format_exception(exc_type, exc_value, 841 | exc_traceback) 842 | wid = ErrorAccordion(e, trace) 843 | wids4acc.append(wid) 844 | namelist.append('ERROR at layer {}'.format(name)) 845 | 846 | # ImageCollection 847 | if obj['type'] == 'ImageCollection': 848 | # Get the values from all images 849 | try: 850 | collection = obj['object'] 851 | values = tools.imagecollection.getValues( 852 | collection, point, scale=ZOOM_SCALES[self.zoom], 853 | properties=['system:time_start'], 854 | side='client') 855 | 856 | # header 857 | allbands = [val.keys() for bands, val in values.items()] 858 | bands = [] 859 | for bandlist in allbands: 860 | for band in bandlist: 861 | if band not in bands: 862 | bands.append(band) 863 | 864 | header = ['image']+bands 865 | 866 | # rows 867 | rows = [] 868 | for imgid, val in values.items(): 869 | row = ['']*len(header) 870 | row[0] = str(imgid) 871 | for bandname, bandvalue in val.items(): 872 | pos = header.index(bandname) if bandname in header else None 873 | if pos: 874 | row[pos] = str(bandvalue) 875 | rows.append(row) 876 | 877 | # Create the content 878 | html = createHTMLTable(header, rows) 879 | wid = HTML(html) 880 | # append widget to list of widgets 881 | wids4acc.append(wid) 882 | namelist.append(name) 883 | except Exception as e: 884 | exc_type, exc_value, exc_traceback = sys.exc_info() 885 | trace = traceback.format_exception(exc_type, exc_value, 886 | exc_traceback) 887 | wid = ErrorAccordion(e, trace) 888 | wids4acc.append(wid) 889 | namelist.append('ERROR at layer {}'.format(name)) 890 | 891 | # Features 892 | if obj['type'] == 'Feature': 893 | try: 894 | feat = obj['object'] 895 | feat_geom = feat.geometry() 896 | if feat_geom.contains(point).getInfo(): 897 | info = featurePropertiesOutput(feat) 898 | wid = HTML(info) 899 | # append widget to list of widgets 900 | wids4acc.append(wid) 901 | namelist.append(name) 902 | except Exception as e: 903 | # wid = HTML(str(e).replace('<','{').replace('>','}')) 904 | exc_type, exc_value, exc_traceback = sys.exc_info() 905 | trace = traceback.format_exception(exc_type, exc_value, 906 | exc_traceback) 907 | wid = ErrorAccordion(e, trace) 908 | wids4acc.append(wid) 909 | namelist.append('ERROR at layer {}'.format(name)) 910 | 911 | # FeatureCollections 912 | if obj['type'] == 'FeatureCollection': 913 | try: 914 | fc = obj['object'] 915 | filtered = fc.filterBounds(point) 916 | if filtered.size().getInfo() > 0: 917 | feat = ee.Feature(filtered.first()) 918 | info = featurePropertiesOutput(feat) 919 | wid = HTML(info) 920 | # append widget to list of widgets 921 | wids4acc.append(wid) 922 | namelist.append(name) 923 | except Exception as e: 924 | wid = HTML(str(e).replace('<','{').replace('>','}')) 925 | wids4acc.append(wid) 926 | namelist.append('ERROR at layer {}'.format(name)) 927 | 928 | # Set children and children's name of inspector widget 929 | thewidget.children = wids4acc 930 | for i, n in enumerate(namelist): 931 | thewidget.set_title(i, n) 932 | 933 | def handle_object_inspector(self, **change): 934 | """ Handle function for the Object Inspector Widget 935 | 936 | DEPRECATED 937 | """ 938 | event = change['type'] # event type 939 | thewidget = change['widget'] 940 | if event == 'click': # If the user clicked 941 | # Clear children // Loading 942 | thewidget.children = [HTML('wait a second please..')] 943 | thewidget.set_title(0, 'Loading...') 944 | 945 | widgets = [] 946 | i = 0 947 | 948 | for name, obj in self.EELayers.items(): # for every added layer 949 | the_object = obj['object'] 950 | try: 951 | properties = the_object.getInfo() 952 | wid = create_accordion(properties) # Accordion 953 | wid.selected_index = None # this will unselect all 954 | except Exception as e: 955 | wid = HTML(str(e)) 956 | widgets.append(wid) 957 | thewidget.set_title(i, name) 958 | i += 1 959 | 960 | thewidget.children = widgets 961 | 962 | def handle_draw(self, dc_widget, action, geo_json): 963 | """ Handles drawings """ 964 | ty = geo_json['geometry']['type'] 965 | coords = geo_json['geometry']['coordinates'] 966 | geom = self.draw_types[ty](coords) 967 | if action == 'created': 968 | self.addGeometry(geom) 969 | dc_widget.clear() 970 | elif action == 'deleted': 971 | for key, val in self.EELayers.items(): 972 | if geom == val: 973 | self.removeLayer(key) 974 | 975 | class CustomInspector(HBox): 976 | def __init__(self, **kwargs): 977 | desc = 'Select one or more layers' 978 | super(CustomInspector, self).__init__(description=desc, **kwargs) 979 | self.selector = SelectMultiple() 980 | self.main = Accordion() 981 | self.children = [self.selector, self.main] 982 | 983 | --------------------------------------------------------------------------------