├── 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(''+''.join(new_band_names)+'
')
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) Google Earth Engine ', name='Sentinel 2 Image', options=['min_zoom', 'detect_retina', 'attribution', 'max_zoom', 'tile_size'], url='https://earthengine.googleapis.com/map/12d55689141a1230e95c5f1668144339/{z}/{x}/{y}?token=af553ad8bdd65700c3d7bd80297a96b7'),\n",
379 | " 'object': ,\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 = '
'.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 = '