├── .gitignore ├── CHANGELOG ├── CONTRIBUTE.md ├── DEVDOC.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── app.png ├── chat.svg ├── docs.svg ├── globe.svg └── logo.png ├── frontend ├── .env.development ├── .env.production ├── README.md ├── babel.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.png │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── logo.svg │ │ ├── marker.svg │ │ └── tailwind.css │ ├── components │ │ ├── BaseContainer.vue │ │ ├── CenterContainer.vue │ │ ├── LeftContainer.vue │ │ ├── RightContainer.vue │ │ ├── chart-components │ │ │ ├── BarChart.vue │ │ │ ├── BubbleChart.vue │ │ │ ├── ChartControl.vue │ │ │ ├── ChartTest.vue │ │ │ ├── DoughnutChart.vue │ │ │ ├── HorizontalBarChart.vue │ │ │ ├── LineChart.vue │ │ │ ├── PieChart.vue │ │ │ ├── PolarChart.vue │ │ │ ├── RadarChart.vue │ │ │ └── ScatterChart.vue │ │ ├── display-components │ │ │ ├── DisplayControl.vue │ │ │ └── MarkdownDisplay.vue │ │ ├── functional-components │ │ │ ├── AppInfoModal.vue │ │ │ └── ErrorModal.vue │ │ ├── input-components │ │ │ ├── InputControl.vue │ │ │ ├── InputTest.vue │ │ │ ├── MultiSelectInput.vue │ │ │ ├── NumberInput.vue │ │ │ ├── SelectInput.vue │ │ │ └── TextInput.vue │ │ ├── map-components │ │ │ ├── BaseLayer.vue │ │ │ ├── BaseLayerControl.vue │ │ │ ├── DrawFeature.vue │ │ │ ├── DrawFeatureControl.vue │ │ │ ├── ImageLayer.vue │ │ │ ├── LayerControl.vue │ │ │ ├── MapContainer.vue │ │ │ ├── OverlayLayerControl.vue │ │ │ ├── TileLayer.vue │ │ │ ├── VectorLayer.vue │ │ │ └── WMSTileLayer.vue │ │ └── utils │ │ │ ├── color-space.js │ │ │ └── include-unicons.js │ ├── event-hub.js │ ├── main.js │ └── store │ │ ├── index.js │ │ └── modules │ │ └── backend-api.js ├── tailwind.config.js └── vue.config.js └── library ├── .pre-commit-config.yaml ├── MANIFEST.in ├── __init__.py ├── description.md ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── src └── greppo │ ├── __init__.py │ ├── cli.py │ ├── colorbrewer.py │ ├── greppo.py │ ├── greppo_server.py │ ├── input_types │ ├── __init__.py │ ├── bar_chart.py │ ├── display.py │ ├── draw_feature.py │ ├── line_chart.py │ ├── multiselect.py │ ├── number.py │ ├── select.py │ └── text.py │ ├── layers │ ├── __init__.py │ ├── base_layer.py │ ├── ee_layer.py │ ├── image_layer.py │ ├── overlay_layer.py │ ├── raster_layer.py │ ├── tile_layer.py │ ├── vector_layer.py │ └── wms_tile_layer.py │ ├── osm.py │ ├── static │ ├── css │ │ ├── app.a14da775.css │ │ └── chunk-vendors.db50bcea.css │ ├── favicon.png │ ├── img │ │ ├── logo.824287d2.svg │ │ ├── marker.d242732c.svg │ │ └── spritesheet.fd5728f2.svg │ ├── index.html │ └── js │ │ ├── app.7529265e.js │ │ ├── app.7529265e.js.map │ │ ├── chunk-vendors.526a29f2.js │ │ └── chunk-vendors.526a29f2.js.map │ └── user_script_utils.py └── tests ├── __init__.py ├── app.py ├── app_gee.py ├── data ├── buildings.geojson ├── communes.geojson ├── features.geojson ├── line.geojson ├── point.geojson ├── polygon.geojson ├── sfo.jpg └── us-states.geojson ├── test.py ├── unit_tests ├── __init__.py ├── test_greppo_app.py ├── test_user_script_utils.py ├── user_script_1.py ├── user_script_2.py ├── user_script_3.py └── user_script_4.py └── v30.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Project 2 | .python-version 3 | /.python-version 4 | .idea 5 | library/static/ 6 | rvrnbrt.TIF 7 | AAA.JPG 8 | 9 | # Mac 10 | .DS_Store 11 | /.DS_Store 12 | 13 | # Logs 14 | logs 15 | *.log 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | lerna-debug.log* 20 | .pnpm-debug.log* 21 | 22 | # Diagnostic reports (https://nodejs.org/api/report.html) 23 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 24 | 25 | # Runtime data 26 | pids 27 | *.pid 28 | *.seed 29 | *.pid.lock 30 | 31 | # Directory for instrumented libs generated by jscoverage/JSCover 32 | lib-cov 33 | 34 | # Coverage directory used by tools like istanbul 35 | coverage 36 | *.lcov 37 | 38 | # nyc test coverage 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | bower_components 46 | 47 | # node-waf configuration 48 | .lock-wscript 49 | 50 | # Compiled binary addons (https://nodejs.org/api/addons.html) 51 | build/Release 52 | 53 | # Dependency directories 54 | node_modules/ 55 | jspm_packages/ 56 | 57 | # Snowpack dependency directory (https://snowpack.dev/) 58 | web_modules/ 59 | 60 | # TypeScript cache 61 | *.tsbuildinfo 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Microbundle cache 70 | .rpt2_cache/ 71 | .rts2_cache_cjs/ 72 | .rts2_cache_es/ 73 | .rts2_cache_umd/ 74 | 75 | # Optional REPL history 76 | .node_repl_history 77 | 78 | # Output of 'npm pack' 79 | *.tgz 80 | 81 | # Yarn Integrity file 82 | .yarn-integrity 83 | 84 | # dotenv environment variables file 85 | .env 86 | .env.test 87 | # .env.production 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # Byte-compiled / optimized / DLL files 133 | __pycache__/ 134 | *.py[cod] 135 | *$py.class 136 | 137 | # C extensions 138 | *.so 139 | 140 | # Distribution / packaging 141 | .Python 142 | build/ 143 | develop-eggs/ 144 | dist/ 145 | downloads/ 146 | eggs/ 147 | .eggs/ 148 | lib/ 149 | lib64/ 150 | parts/ 151 | sdist/ 152 | var/ 153 | wheels/ 154 | share/python-wheels/ 155 | *.egg-info/ 156 | .installed.cfg 157 | *.egg 158 | MANIFEST 159 | 160 | # PyInstaller 161 | # Usually these files are written by a python script from a template 162 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 163 | *.manifest 164 | *.spec 165 | 166 | # Installer logs 167 | pip-log.txt 168 | pip-delete-this-directory.txt 169 | 170 | # Unit test / coverage reports 171 | htmlcov/ 172 | .tox/ 173 | .nox/ 174 | .coverage 175 | .coverage.* 176 | .cache 177 | nosetests.xml 178 | coverage.xml 179 | *.cover 180 | *.py,cover 181 | .hypothesis/ 182 | .pytest_cache/ 183 | cover/ 184 | 185 | # Translations 186 | *.mo 187 | *.pot 188 | 189 | # Django stuff: 190 | *.log 191 | local_settings.py 192 | db.sqlite3 193 | db.sqlite3-journal 194 | 195 | # Flask stuff: 196 | instance/ 197 | .webassets-cache 198 | 199 | # Scrapy stuff: 200 | .scrapy 201 | 202 | # Sphinx documentation 203 | docs/_build/ 204 | 205 | # PyBuilder 206 | .pybuilder/ 207 | target/ 208 | 209 | # Jupyter Notebook 210 | .ipynb_checkpoints 211 | 212 | # IPython 213 | profile_default/ 214 | ipython_config.py 215 | 216 | # pyenv 217 | # For a library or package, you might want to ignore these files since the code is 218 | # intended to run in multiple environments; otherwise, check them in: 219 | # .python-version 220 | 221 | # pipenv 222 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 223 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 224 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 225 | # install all needed dependencies. 226 | #Pipfile.lock 227 | 228 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 229 | __pypackages__/ 230 | 231 | # Celery stuff 232 | celerybeat-schedule 233 | celerybeat.pid 234 | 235 | # SageMath parsed files 236 | *.sage.py 237 | 238 | # Environments 239 | .env 240 | .venv 241 | env/ 242 | venv/ 243 | ENV/ 244 | env.bak/ 245 | venv.bak/ 246 | 247 | # Spyder project settings 248 | .spyderproject 249 | .spyproject 250 | 251 | # Rope project settings 252 | .ropeproject 253 | 254 | # mkdocs documentation 255 | /site 256 | 257 | # mypy 258 | .mypy_cache/ 259 | .dmypy.json 260 | dmypy.json 261 | 262 | # Pyre type checker 263 | .pyre/ 264 | 265 | # pytype static type analyzer 266 | .pytype/ 267 | 268 | # Cython debug symbols 269 | cython_debug/ 270 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | ## [0.0.33] - 2022-02-22 6 | - Added raster_layer and image_layer api. 7 | - Change port and host setting options in cli. 8 | 9 | ## [0.0.25] - 2022-02-22 10 | - Added multi-line and multi-bar for the visualizations. 11 | 12 | ## [0.0.22] - 2022-02-22 13 | - Fix import errors in locals. (import ee error) 14 | 15 | ## [0.0.21] - 2022-02-18 16 | - Fix lineand bar chart typing and settings. 17 | - Add app.map method for map settings. 18 | - Fix reevaluate error. 19 | - Add error modal to the frontend. 20 | 21 | ## [0.0.16] - 2022-02-16 22 | - Added `choropleth` for the `vector_layer` 23 | - Added support for `color` for the charts. 24 | 25 | ## [0.0.15] - 2022-02-11 26 | - Added `app.tile_layer`, `app.wms_tile_layer`, `app.vector_layer`, `app.ee_layer` 27 | - Added support for Google Earth Engine. 28 | - Added support for `app.base_layer` to use `geopandas/xyzservices`. 29 | - `app.bar_chart`, `app.line_chart` will not have `title` argument anymore. Will only have `name` argument. 30 | - `title` argument to components are removed and will now will have only `name` 31 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # Contributing to Greppo 2 | 3 | Wooohooo, thanks for taking the time to contribute! :tada: 4 | 5 | The project is hosted in the [Greppo organisation](https://github.com/greppo-io/) on GitHub. 6 | 7 | The following is a set of guidelines for contributing. These are not rules, but mere guidelines. So, please feel free to propose changes to any part of the organisation and the material within. 8 | 9 | ## How do you start? 10 | 11 | For getting the development environment setup, please read the DEVDOC.md in the root of this repository. The project consists of two parts, the frontend components, and the backend components which includes the Python package. 12 | 13 | ## What do you start with? 14 | 15 | Before proposing new features, please add an issue and discuss it with the community before a PR. If you don't know where to start with, take a look at the roadmap/milestone to track what is planned. And, if you feel comfortable in contributing to one of them, pick up the respective issue and comment your proposal on how to solve it. 16 | 17 | ## Yes, it doesn't have all the bells and whistles of a complete project. 18 | 19 | What started of as a solution for a personal project, has turned out to be a Python package. It is a contribution back to the community. So, please feel to add in those bells and whistles you feel would make this project grow! 20 | 21 | :heart::v: OSS 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /DEVDOC.md: -------------------------------------------------------------------------------- 1 | # For backend 2 | 3 | ## Development 4 | `pip install pre-commit` 5 | `pre-commit install` 6 | `pip install -r requirements.txt` 7 | `pip install -e .` 8 | 9 | ## Run Backend Server 10 | `greppo serve tests/app.py` 11 | 12 | docs -- build and source are separated `make clean && make html` to generate the docs as html files. 13 | 14 | In `docs/` 15 | `sphinx-apidoc -o source/ ../src` on making source changes in `src` dir under library 16 | 17 | ## Unit tests 18 | We use pytests, `pytest tests/unit_tests` 19 | 20 | # Checklist for build and deploy to pypi 21 | 22 | - [ ] Build frontend: `cd frontend` and `npm run build` 23 | - [ ] Update setup.cfg: bump version number 24 | - [ ] Build backend: `cd library` and `python -m build` 25 | - [ ] Upload to pypi using twine: `twine upload --verbose --skip-existing dist/*` -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Usage: $ make {command} 2 | 3 | .ONESHELL: 4 | 5 | .PHONY: all 6 | all: clean 7 | 8 | .PHONY: serve-dev 9 | serve-dev: 10 | npm run --prefix ./frontend serve & python ./tests/test_server/test.py & 11 | 12 | .PHONY: serve-fe 13 | serve-fe: 14 | cd frontend && npm run serve 15 | 16 | .PHONY: serve-be 17 | serve-be: 18 | cd library && greppo serve tests/app.py 19 | 20 | .PHONY: build-frontend 21 | build-frontend: 22 | cd frontend && npm run build 23 | 24 | .PHONY: build-package 25 | build-package: 26 | cd library && python -m build 27 | 28 | .PHONY: upload-package 29 | upload-package: 30 | cd library && twine upload -u "__token__" -p "$(PYPI_GREPPO_API)" --skip-existing --verbose dist/* 31 | 32 | .PHONY: run-unit-tests 33 | run-unit-tests: 34 | pytest library/tests/unit_tests 35 | 36 | .PHONY: clean 37 | clean: 38 | echo "To be implemented..." 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hey there, this is Greppo... 2 | 3 | [![Discord](https://badgen.net/badge/icon/discord?icon=discord&label)](https://discord.gg/RNJBjgh8gz) ![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Fgreppo_io) 4 | 5 | **A Python framework for building geospatial web-applications.** 6 | 7 | Greppo is an open-source Python framework that makes it easy to build applications. It provides a toolkit to quickly integrate data, algorithms, visualizations and UI for interactivity. 8 | 9 | **Documentation**: [docs.greppo.io](https://docs.greppo.io) 10 | 11 | **Website**: https://greppo.io 12 | 13 | **Discord Community**: https://discord.gg/RNJBjgh8gz 14 | 15 | If you run into any problems, ping us on Discord, Twitter or open an issue on GitHub. 16 | 17 | ## Installation 18 | 19 | ```shell 20 | $ pip install greppo 21 | ``` 22 | 23 | We suggest you use a virtual environment to manage your packages for this project. For more infromation and troubleshooting visit the [Installation Guide](https://docs.greppo.io). 24 | 25 | **Windows users**: Installation of Fiona (one of Greppo's dependencies) on Windows machines usually doesn't work by default. A manual installation with e.g. [wheel files by Christoph Gohlke](https://www.lfd.uci.edu/~gohlke/pythonlibs/) the would be a work around. 26 | 27 | ## A simple example 28 | 29 | ```python 30 | # inside app.py 31 | 32 | from greppo import app 33 | import geopandas as gpd 34 | 35 | data_gdf = gpd.read_file("geospatial_data.geojson") 36 | 37 | buildings_gdf = gpd.read_file("./data/buildings.geojson") 38 | 39 | app.overlay_layer( 40 | buildings_gdf, 41 | name="Buildings", 42 | description="Buildings in a neighbourhood in Amsterdam", 43 | style={"fillColor": "#F87979"}, 44 | visible=True, 45 | ) 46 | 47 | app.base_layer( 48 | name="Open Street Map", 49 | visible=True, 50 | url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 51 | subdomains=None, 52 | attribution='© OpenStreetMap contributors', 53 | ) 54 | ``` 55 | 56 | Then run the aplication using the `greppo` cli command: 57 | 58 | ```shell 59 | greppo serve app.py 60 | ``` 61 | 62 | To view the app that is being served, enter this address of the localhost `localhost:8080/` in your web browser. (Note: the port of `8080` might be different depending on other programs you're running. Check the port indicated in the command line interface.) 63 | 64 | 65 | 66 | ## Support & Community 67 | 68 | Do you have questions? Ideas? Want to share your project? Join us on discord [Invite Link](https://discord.gg/RNJBjgh8gz). 69 | 70 | ## Under the hood 71 | 72 | Greppo is open-source and is built on open-source. Under the hood it uses [Starlette](https://github.com/encode/starlette), [Vue](https://github.com/vuejs/vue), [Leaflet](https://github.com/Leaflet/Leaflet), [ChartJS](https://github.com/chartjs/Chart.js), [TailwindCSS](https://github.com/tailwindlabs/tailwindcss) to name a few. A detailed list of the open-source projects used is listed [here](https://docs.greppo.io/under-the-hood.html). 73 | 74 | Greppo is our contribution back to the geospatial community. We want to make the development of geosaptial apps easy, to make it easy for data-scientists to showcase their work. 75 | 76 | ## License 77 | 78 | Greppo is licensed under Apache V2. 79 | -------------------------------------------------------------------------------- /assets/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/assets/app.png -------------------------------------------------------------------------------- /assets/chat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/docs.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/assets/logo.png -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | # Environmental variables for development mode. 2 | VUE_APP_API='http://0.0.0.0:8080/api' -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | # Environmental variables for production mode. Takes precedence over general .env. 2 | VUE_APP_API='/api' -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # frontend 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/**/*"] 3 | } 4 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "greppo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build --dest ../library/src/greppo/static", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@tailwindcss/forms": "^0.3.3", 12 | "@tailwindcss/postcss7-compat": "^2.0.2", 13 | "@tailwindcss/typography": "^0.4.1", 14 | "autoprefixer": "^9", 15 | "axios": "^0.21.2", 16 | "chart.js": "^2.9.4", 17 | "core-js": "^3.6.5", 18 | "leaflet": "^1.7.1", 19 | "leaflet-draw": "^1.0.4", 20 | "postcss": "^8", 21 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.2", 22 | "vue": "^2.6.11", 23 | "vue-chartjs": "^3.5.1", 24 | "vue-fullscreen": "^2.5.2", 25 | "vue-js-toggle-button": "^1.3.3", 26 | "vue-multipane": "^0.9.5", 27 | "vue-showdown": "^2.4.1", 28 | "vue-unicons": "^3.2.1", 29 | "vue2-leaflet": "^2.7.1", 30 | "vue2-leaflet-draw-toolbar": "^0.1.1", 31 | "vuex": "^3.6.2" 32 | }, 33 | "devDependencies": { 34 | "@vue/cli-plugin-babel": "~4.5.0", 35 | "@vue/cli-plugin-eslint": "~4.5.0", 36 | "@vue/cli-service": "~4.5.0", 37 | "babel-eslint": "^10.1.0", 38 | "eslint": "^6.7.2", 39 | "eslint-plugin-vue": "^6.2.2", 40 | "vue-cli-plugin-tailwind": "~2.0.6", 41 | "vue-template-compiler": "^2.6.11" 42 | }, 43 | "eslintConfig": { 44 | "root": true, 45 | "env": { 46 | "node": true 47 | }, 48 | "extends": [ 49 | "plugin:vue/essential", 50 | "eslint:recommended" 51 | ], 52 | "parserOptions": { 53 | "parser": "babel-eslint" 54 | }, 55 | "rules": {} 56 | }, 57 | "browserslist": [ 58 | "> 1%", 59 | "last 2 versions", 60 | "not dead" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/assets/marker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/src/assets/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /frontend/src/components/BaseContainer.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /frontend/src/components/CenterContainer.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /frontend/src/components/LeftContainer.vue: -------------------------------------------------------------------------------- 1 | 79 | 80 | 123 | 124 | 134 | -------------------------------------------------------------------------------- /frontend/src/components/RightContainer.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 45 | 46 | 56 | 57 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/BarChart.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/BubbleChart.vue: -------------------------------------------------------------------------------- 1 | 67 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/ChartControl.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/ChartTest.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/DoughnutChart.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/HorizontalBarChart.vue: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/LineChart.vue: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/PieChart.vue: -------------------------------------------------------------------------------- 1 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/PolarChart.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/RadarChart.vue: -------------------------------------------------------------------------------- 1 | 54 | -------------------------------------------------------------------------------- /frontend/src/components/chart-components/ScatterChart.vue: -------------------------------------------------------------------------------- 1 | 81 | -------------------------------------------------------------------------------- /frontend/src/components/display-components/DisplayControl.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /frontend/src/components/display-components/MarkdownDisplay.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend/src/components/functional-components/AppInfoModal.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 56 | 57 | 67 | -------------------------------------------------------------------------------- /frontend/src/components/functional-components/ErrorModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 37 | 38 | 48 | -------------------------------------------------------------------------------- /frontend/src/components/input-components/InputControl.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/input-components/InputTest.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /frontend/src/components/input-components/MultiSelectInput.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /frontend/src/components/input-components/NumberInput.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | 49 | -------------------------------------------------------------------------------- /frontend/src/components/input-components/SelectInput.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /frontend/src/components/input-components/TextInput.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/BaseLayer.vue: -------------------------------------------------------------------------------- 1 | 14 | 26 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/BaseLayerControl.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/DrawFeatureControl.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/ImageLayer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/LayerControl.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/MapContainer.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 152 | 153 | 177 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/OverlayLayerControl.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 77 | 78 | 83 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/TileLayer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/VectorLayer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /frontend/src/components/map-components/WMSTileLayer.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /frontend/src/components/utils/color-space.js: -------------------------------------------------------------------------------- 1 | // Shade, Blend and Convert a Web Color (pSBC.js) 2 | // https://stackoverflow.com/questions/5560248/programmatically-lighten-or-darken-a-hex-color-or-rgb-and-blend-colors 3 | // https://github.com/PimpTrizkit/PJs/wiki/12.-Shade,-Blend-and-Convert-a-Web-Color-(pSBC.js) 4 | 5 | const pSBC=(p,c0,c1,l)=>{ 6 | let r,g,b,P,f,t,h,i=parseInt,m=Math.round,a=typeof(c1)=="string"; 7 | if(typeof(p)!="number"||p<-1||p>1||typeof(c0)!="string"||(c0[0]!='r'&&c0[0]!='#')||(c1&&!a))return null; 8 | const pSBCr=(d)=>{ 9 | let n=d.length,x={}; 10 | if(n>9){ 11 | [r,g,b,a]=d=d.split(","),n=d.length; 12 | if(n<3||n>4)return null; 13 | x.r=i(r[3]=="a"?r.slice(5):r.slice(4)),x.g=i(g),x.b=i(b),x.a=a?parseFloat(a):-1 14 | }else{ 15 | if(n==8||n==6||n<4)return null; 16 | if(n<6)d="#"+d[1]+d[1]+d[2]+d[2]+d[3]+d[3]+(n>4?d[4]+d[4]:""); 17 | d=i(d.slice(1),16); 18 | if(n==9||n==5)x.r=d>>24&255,x.g=d>>16&255,x.b=d>>8&255,x.a=m((d&255)/0.255)/1000; 19 | else x.r=d>>16,x.g=d>>8&255,x.b=d&255,x.a=-1 20 | }return x}; 21 | h=c0.length>9,h=a?c1.length>9?true:c1=="c"?!h:false:h,f=pSBCr(c0),P=p<0,t=c1&&c1!="c"?pSBCr(c1):P?{r:0,g:0,b:0,a:-1}:{r:255,g:255,b:255,a:-1},p=P?p*-1:p,P=1-p; 22 | if(!f||!t)return null; 23 | if(l)r=m(P*f.r+p*t.r),g=m(P*f.g+p*t.g),b=m(P*f.b+p*t.b); 24 | else r=m((P*f.r**2+p*t.r**2)**0.5),g=m((P*f.g**2+p*t.g**2)**0.5),b=m((P*f.b**2+p*t.b**2)**0.5); 25 | a=f.a,t=t.a,f=a>=0||t>=0,a=f?a<0?t:t<0?a:a*P+t*p:0; 26 | if(h)return"rgb"+(f?"a(":"(")+r+","+g+","+b+(f?","+m(a*1000)/1000:"")+")"; 27 | else return"#"+(4294967296+r*16777216+g*65536+b*256+(f?m(a*255):0)).toString(16).slice(1,f?undefined:-2) 28 | } 29 | 30 | export { pSBC }; 31 | -------------------------------------------------------------------------------- /frontend/src/components/utils/include-unicons.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Unicon from "vue-unicons/dist/vue-unicons-vue2.umd"; 3 | import { 4 | uniEstate, 5 | uniExpandArrowsAlt, 6 | uniCompressArrows, 7 | uniAngleRight, 8 | uniAngleLeft, 9 | uniAngleDown, 10 | uniInfoCircle, 11 | uniSearchPlus, 12 | uniSortAmountDown, 13 | uniMultiply, 14 | } from "vue-unicons/dist/icons"; 15 | 16 | Unicon.add([ 17 | uniEstate, 18 | uniExpandArrowsAlt, 19 | uniCompressArrows, 20 | uniAngleRight, 21 | uniAngleLeft, 22 | uniAngleDown, 23 | uniInfoCircle, 24 | uniSearchPlus, 25 | uniSortAmountDown, 26 | uniMultiply, 27 | ]); 28 | Vue.use(Unicon); 29 | -------------------------------------------------------------------------------- /frontend/src/event-hub.js: -------------------------------------------------------------------------------- 1 | // Central event hub/bus for communicating between components. 2 | 3 | import Vue from "vue"; 4 | export const eventHub = new Vue(); 5 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import store from "./store"; 4 | 5 | // For unicons 6 | import "./components/utils/include-unicons.js"; 7 | 8 | // toggle-button : https://github.com/euvl/vue-js-toggle-button 9 | import { ToggleButton } from "vue-js-toggle-button"; 10 | Vue.component("ToggleButton", ToggleButton); 11 | 12 | // markdown to html : https://github.com/meteorlxy/vue-showdown 13 | import VueShowdown from "vue-showdown"; 14 | Vue.use(VueShowdown); 15 | 16 | Vue.config.productionTip = false; 17 | 18 | new Vue({ 19 | store, 20 | render: (h) => h(App), 21 | }).$mount("#app"); 22 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex"; 2 | import Vue from "vue"; 3 | 4 | import BackendAPI from "./modules/backend-api"; 5 | 6 | // Load Vuex 7 | Vue.use(Vuex); 8 | 9 | // Create store 10 | export default new Vuex.Store({ 11 | modules: { 12 | BackendAPI, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: { content: ["./public/**/*.html", "./src/**/*.vue"] }, 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | transitionProperty: { 7 | height: "height", 8 | }, 9 | typography: { 10 | sm: { 11 | css: { 12 | code: { 13 | fontSize: "1em", 14 | }, 15 | pre: { 16 | "overflow-x": "auto", 17 | }, 18 | h2: { 19 | marginTop: "1.4em", 20 | }, 21 | }, 22 | }, 23 | }, 24 | }, 25 | }, 26 | variants: { 27 | extend: {}, 28 | }, 29 | plugins: [ 30 | require("@tailwindcss/forms")({ 31 | strategy: "class", 32 | }), 33 | require("@tailwindcss/typography"), 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | module.exports = { 3 | configureWebpack: { 4 | resolve: { 5 | alias: { 6 | src: path.resolve(__dirname, "src"), 7 | }, 8 | }, 9 | }, 10 | devServer: { 11 | host: "localhost", 12 | port: 8081, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /library/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 19.3b0 10 | hooks: 11 | - id: black 12 | exclude: user_script_* 13 | - repo: https://github.com/asottile/reorder_python_imports 14 | rev: v2.6.0 15 | hooks: 16 | - id: reorder-python-imports 17 | exclude: user_script_* 18 | -------------------------------------------------------------------------------- /library/MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include src/greppo/static * 2 | -------------------------------------------------------------------------------- /library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/library/__init__.py -------------------------------------------------------------------------------- /library/description.md: -------------------------------------------------------------------------------- 1 | # Hey there, this is Greppo... 2 | 3 | **A Python framework for building geospatial web-applications.** 4 | 5 | Greppo is an open-source Python framework that makes it easy to build applications. It provides a toolkit for to quickly integrate data, algorithms, visualizations and UI for interactivity. 6 | 7 | **Documentation**: [docs.greppo.io](https://docs.greppo.io) 8 | 9 | **Website**: https://greppo.io 10 | 11 | **Discord Community**: https://discord.gg/RNJBjgh8gz 12 | 13 | ## Installation 14 | 15 | ```shell 16 | $ pip install greppo 17 | ``` 18 | 19 | We suggest you use a virtual environment to manage your packages for this project. For more infromation and troubleshooting visit the [Installation Guide](https://docs.greppo.io). 20 | 21 | ## Support & Community 22 | 23 | Do you have questions? Ideas? Want to share your project? Join us on discord [Invite Link](https://discord.gg/RNJBjgh8gz). 24 | 25 | ## License 26 | 27 | Greppo is licensed under Apache V2. -------------------------------------------------------------------------------- /library/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | -------------------------------------------------------------------------------- /library/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | affine==2.3.0 2 | alabaster==0.7.12 3 | anyio==3.3.4 4 | appnope==0.1.2 5 | asgiref==3.4.1 6 | attrs==21.2.0 7 | Babel==2.9.1 8 | backcall==0.2.0 9 | backports.entry-points-selectable==1.1.0 10 | build==0.7.0 11 | certifi==2021.10.8 12 | cfgv==3.3.1 13 | charset-normalizer==2.0.7 14 | click==8.0.3 15 | click-plugins==1.1.1 16 | cligj==0.7.2 17 | cycler==0.11.0 18 | debugpy==1.5.1 19 | decorator==5.1.0 20 | distlib==0.3.3 21 | docutils==0.17.1 22 | entrypoints==0.3 23 | filelock==3.3.2 24 | Fiona==1.8.20 25 | geopandas==0.10.2 26 | h11==0.12.0 27 | identify==2.3.1 28 | idna==3.3 29 | imagesize==1.2.0 30 | iniconfig==1.1.1 31 | ipykernel==6.5.0 32 | ipython==7.29.0 33 | jedi==0.18.0 34 | Jinja2==3.0.2 35 | jupyter-client==7.0.6 36 | jupyter-core==4.9.1 37 | kiwisolver==1.3.2 38 | MarkupSafe==2.0.1 39 | matplotlib==3.4.3 40 | matplotlib-inline==0.1.3 41 | meta==1.0.2 42 | munch==2.5.0 43 | nest-asyncio==1.5.1 44 | nodeenv==1.6.0 45 | numpy==1.21.2 46 | packaging==21.0 47 | pandas==1.3.3 48 | parso==0.8.2 49 | pep517==0.12.0 50 | pexpect==4.8.0 51 | pickleshare==0.7.5 52 | Pillow==8.4.0 53 | platformdirs==2.4.0 54 | pluggy==1.0.0 55 | pre-commit==2.15.0 56 | prompt-toolkit==3.0.22 57 | ptyprocess==0.7.0 58 | py==1.10.0 59 | Pygments==2.10.0 60 | pyparsing==2.4.7 61 | pyproj==3.2.1 62 | pytest==6.2.5 63 | python-dateutil==2.8.2 64 | pytz==2021.3 65 | PyYAML==6.0 66 | pyzmq==22.3.0 67 | rasterio==1.2.10 68 | requests==2.26.0 69 | Shapely==1.7.1 70 | six==1.16.0 71 | sniffio==1.2.0 72 | snowballstemmer==2.1.0 73 | snuggs==1.4.7 74 | Sphinx==4.2.0 75 | sphinxcontrib-applehelp==1.0.2 76 | sphinxcontrib-devhelp==1.0.2 77 | sphinxcontrib-htmlhelp==2.0.0 78 | sphinxcontrib-jsmath==1.0.1 79 | sphinxcontrib-qthelp==1.0.3 80 | sphinxcontrib-serializinghtml==1.1.5 81 | starlette==0.16.0 82 | toml==0.10.2 83 | tomli==1.2.1 84 | tornado==6.1 85 | traitlets==5.1.1 86 | urllib3==1.26.7 87 | uvicorn==0.15.0 88 | virtualenv==20.9.0 89 | wcwidth==0.2.5 90 | -------------------------------------------------------------------------------- /library/requirements.txt: -------------------------------------------------------------------------------- 1 | affine==2.3.0 2 | anyio==3.3.4 3 | asgiref==3.4.1 4 | attrs==21.2.0 5 | backports.entry-points-selectable==1.1.1 6 | build==0.7.0 7 | certifi==2021.10.8 8 | cfgv==3.3.1 9 | click==8.0.3 10 | click-plugins==1.1.1 11 | cligj==0.7.2 12 | distlib==0.3.3 13 | filelock==3.4.0 14 | Fiona==1.8.20 15 | geopandas==0.10.2 16 | h11==0.12.0 17 | identify==2.4.0 18 | idna==3.3 19 | meta==1.0.2 20 | munch==2.5.0 21 | nodeenv==1.6.0 22 | numpy==1.21.4 23 | packaging==21.3 24 | pandas==1.3.4 25 | pep517==0.12.0 26 | Pillow==8.4.0 27 | platformdirs==2.4.0 28 | pluggy==1.0.0 29 | pre-commit==2.15.0 30 | py==1.10.0 31 | Pygments==2.10.0 32 | pyparsing==2.4.7 33 | pyproj==3.3.0 34 | python-dateutil==2.8.2 35 | pytz==2021.3 36 | PyYAML==6.0 37 | rasterio==1.2.10 38 | Shapely==1.7.1 39 | six==1.16.0 40 | sniffio==1.2.0 41 | snuggs==1.4.7 42 | starlette==0.16.0 43 | toml==0.10.2 44 | tomli==1.2.2 45 | uvicorn==0.15.0 46 | virtualenv==20.10.0 47 | websocket-client==1.2.1 48 | websockets==10.1 49 | wsproto==1.0.0 50 | -------------------------------------------------------------------------------- /library/setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = greppo 3 | version = 0.0.33 4 | author = Adithya Krishnan and Sarma Tangirala 5 | author_email = greppomail@gmail.com 6 | description = Build responsive web-apps for geospatial applications. 7 | long_description = file: Readme.md 8 | long_description_content_type = text/markdown 9 | url = https://greppo.io/ 10 | project_urls = 11 | Source = https://github.com/greppo-io/greppo 12 | Documentation = https://docs.greppo.io/ 13 | classifiers = 14 | Development Status :: 2 - Pre-Alpha 15 | Programming Language :: Python :: 3.9 16 | Operating System :: OS Independent 17 | license = "Apache 2" 18 | 19 | [options] 20 | # packages = find: 21 | packages = find_namespace: 22 | package_dir = 23 | = src 24 | include_package_data = True 25 | 26 | install_requires = 27 | click==8.0.3 28 | geopandas>=0.9.0 29 | pandas>=1.1.5 30 | numpy 31 | Shapely==1.7.1 32 | starlette==0.16.0 33 | uvicorn==0.15.0 34 | meta==1.0.2 35 | xyzservices 36 | earthengine-api 37 | 38 | python_requires = >=3.6 39 | 40 | [options.packages.find] 41 | where = src 42 | 43 | [options.entry_points] 44 | console_scripts = 45 | greppo=greppo.cli:wrap_and_run_script 46 | -------------------------------------------------------------------------------- /library/src/greppo/__init__.py: -------------------------------------------------------------------------------- 1 | from greppo import osm as osm 2 | 3 | from .greppo import GreppoApp 4 | from .greppo import GreppoAppProxy 5 | from .greppo import app 6 | 7 | from .cli import wrap_and_run_script -------------------------------------------------------------------------------- /library/src/greppo/cli.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import click 4 | 5 | from .greppo import GreppoApp 6 | from .greppo_server import GreppoServer 7 | 8 | 9 | @click.command() 10 | @click.argument("command") 11 | @click.argument("path_to_script") 12 | @click.option('--host', default="127.0.0.1", help='Host location for the app.') 13 | @click.option('--port', default="8080", help='Port location for the app.') 14 | def wrap_and_run_script(command, path_to_script, host, port): 15 | """Run a Greppo COMMAND with PATH_TO_SCRIPT_NAME 16 | 17 | PATH_TO_SCRIPT is the path to the script 18 | COMMANDS is one of [serve] 19 | """ 20 | if command == "serve": 21 | gpo = GreppoApp() 22 | server = GreppoServer(gr_app=gpo, user_script=path_to_script) 23 | 24 | server.run(host=host, port=int(port)) 25 | else: 26 | logging.error("Command {} not supported".format(command)) 27 | 28 | 29 | if __name__ == "__main__": 30 | wrap_and_run_script() 31 | -------------------------------------------------------------------------------- /library/src/greppo/greppo.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | import json 3 | import logging 4 | from typing import Any 5 | from typing import Dict 6 | from typing import List 7 | 8 | from .input_types import ComponentInfo 9 | from .input_types import GreppoInputs 10 | 11 | from .input_types import BarChart 12 | from .input_types import LineChart 13 | from .input_types import Multiselect 14 | from .input_types import Number 15 | from .input_types import Select 16 | from .input_types import Text 17 | from .input_types import Display 18 | from .input_types import DrawFeature 19 | 20 | from .layers.base_layer import BaseLayerComponent, BaseLayer 21 | from .layers.tile_layer import TileLayer, TileLayerComponent 22 | from .layers.wms_tile_layer import WMSTileLayer, WMSTileLayerComponent 23 | from .layers.vector_layer import VectorLayer, VectorLayerComponent 24 | from .layers.image_layer import ImageLayer, ImageLayerComponent 25 | from .layers.raster_layer import RasterLayerComponent 26 | from .layers.ee_layer import EarthEngineLayerComponent 27 | 28 | 29 | class GreppoApp(object): 30 | """ 31 | The main Greppo class that is the entry point for user scripts. User scripts will use this class via a module 32 | import variable `gpo`. 33 | 34 | This class provides an interface around available frontend component elements. The methods simply point to the 35 | backend representation of those frontend component elements (ie. `Number` is the backend class that a user script 36 | can access via `self.number`. 37 | """ 38 | 39 | def __init__(self, name: str = "Untitled App"): 40 | self.name: str = name 41 | self.display = Display 42 | self.select = Select 43 | self.multiselect = Multiselect 44 | self.draw_feature = DrawFeature 45 | self.bar_chart = BarChart 46 | self.line_chart = LineChart 47 | 48 | # UX component proxy methods 49 | @staticmethod 50 | def number(): 51 | """ 52 | Interactive Number value rendered on the frontend. 53 | """ 54 | return Number 55 | 56 | @staticmethod 57 | def text(): 58 | """ 59 | Interactive Text value rendered on the frontend. 60 | """ 61 | return Text 62 | 63 | 64 | class GreppoAppProxy(object): 65 | """ 66 | Proxy object that mirrors the `GreppoApp` class. Adds additional methods that user scripts don't need to know about. 67 | These methods are used by a Greppo server to obtain an output from the user script that is then rendered by the 68 | frontend. 69 | """ 70 | 71 | def __init__(self): 72 | # Map component data 73 | self.map_data: Dict = {'settings': {'zoom': 3, 74 | 'center': [0, 0], 'maxZoom': 18, 'minZoom': 0}} 75 | self.base_layers: List[BaseLayer] = [] 76 | self.tile_layers: List[TileLayer] = [] 77 | self.wms_tile_layers: List[WMSTileLayer] = [] 78 | self.vector_layers: List[VectorLayer] = [] 79 | self.image_layers: List[ImageLayer] = [] 80 | # TODO Cleanup raster temp 81 | # self.raster_image_reference: List[bytes] = [] 82 | self.registered_inputs: List[ComponentInfo] = [] 83 | 84 | # Input updates 85 | self.inputs = {} 86 | 87 | def display(self, **kwargs): 88 | display = Display(**kwargs) 89 | self.register_input(display) 90 | return display 91 | 92 | def number(self, **kwargs): 93 | number = Number(**kwargs) 94 | self.register_input(number) 95 | return number 96 | 97 | def text(self, **kwargs): 98 | text = Text(**kwargs) 99 | self.register_input(text) 100 | return text 101 | 102 | def select(self, **kwargs): 103 | select = Select(**kwargs) 104 | self.register_input(select) 105 | return select 106 | 107 | def multiselect(self, **kwargs): 108 | multiselect = Multiselect(**kwargs) 109 | self.register_input(multiselect) 110 | return multiselect 111 | 112 | def draw_feature(self, **kwargs): 113 | draw_feature = DrawFeature(**kwargs) 114 | self.register_input(draw_feature) 115 | return draw_feature 116 | 117 | def bar_chart(self, **kwargs): 118 | bar_chart = BarChart(**kwargs) 119 | self.register_input(bar_chart) 120 | return bar_chart 121 | 122 | def line_chart(self, **kwargs): 123 | line_chart = LineChart(**kwargs) 124 | self.register_input(line_chart) 125 | return line_chart 126 | 127 | def map(self, **kwargs): 128 | if 'zoom' in kwargs: 129 | self.map_data['settings']['zoom'] = kwargs.get('zoom') 130 | if 'center' in kwargs: 131 | self.map_data['settings']['center'] = kwargs.get('center') 132 | if 'max_zoom' in kwargs: 133 | self.map_data['settings']['maxZoom'] = kwargs.get('max_zoom') 134 | if 'min_zoom' in kwargs: 135 | self.map_data['settings']['minZoom'] = kwargs.get('min_zoom') 136 | 137 | def ee_layer(self, **kwargs): 138 | ee_layer_component = EarthEngineLayerComponent(**kwargs) 139 | ee_layer_dataclass = ee_layer_component.convert_to_dataclass() 140 | self.tile_layers.append(ee_layer_dataclass) 141 | 142 | def tile_layer(self, **kwargs): 143 | tile_layer_component = TileLayerComponent(**kwargs) 144 | tile_layer_dataclass = tile_layer_component.convert_to_dataclass() 145 | self.tile_layers.append(tile_layer_dataclass) 146 | 147 | def wms_tile_layer(self, **kwargs): 148 | wms_tile_layer_component = WMSTileLayerComponent(**kwargs) 149 | wms_tile_layer_dataclass = wms_tile_layer_component.convert_to_dataclass() 150 | self.wms_tile_layers.append(wms_tile_layer_dataclass) 151 | 152 | def base_layer(self,**kwargs): 153 | base_layer_component = BaseLayerComponent(**kwargs) 154 | base_layer_dataclass = base_layer_component.convert_to_dataclass() 155 | self.base_layers.append(base_layer_dataclass) 156 | 157 | def vector_layer(self, **kwargs): 158 | vector_layer_component = VectorLayerComponent(**kwargs) 159 | vector_layer_dataclass = vector_layer_component.convert_to_dataclass() 160 | self.vector_layers.append(vector_layer_dataclass) 161 | 162 | def image_layer(self, **kwargs): 163 | image_layer_component = ImageLayerComponent(**kwargs) 164 | image_layer_dataclass = image_layer_component.convert_to_dataclass() 165 | self.image_layers.append(image_layer_dataclass) 166 | 167 | def raster_layer(self, **kwargs): 168 | raster_layer_component = RasterLayerComponent(**kwargs) 169 | image_layer_dataclass = raster_layer_component.convert_to_dataclass() 170 | self.image_layers.append(image_layer_dataclass) 171 | 172 | def overlay_layer(self, **kwargs): 173 | vector_layer_component = VectorLayerComponent(**kwargs) 174 | vector_layer_dataclass = vector_layer_component.convert_to_dataclass() 175 | self.vector_layers.append(vector_layer_dataclass) 176 | 177 | def update_inputs(self, inputs: Dict[str, Any]): 178 | self.inputs = inputs 179 | 180 | def register_input(self, discovered_input: GreppoInputs): 181 | """ 182 | BarChart and LineChart are also registered with this `register_input` method. Maybe rename this method. 183 | """ 184 | component_info = discovered_input.convert_to_component_info() 185 | self.registered_inputs.append(component_info) 186 | 187 | return discovered_input 188 | 189 | def gpo_prepare_data(self): 190 | """ 191 | Take output of run script and setup the payload for the front-end to read. 192 | """ 193 | 194 | app_output = { 195 | "base_layer_data": [], 196 | "tile_layer_data": [], 197 | "wms_tile_layer_data": [], 198 | "vector_layer_data": [], 199 | "image_layer_data": [], 200 | "component_info": [], 201 | "map": {}, 202 | } 203 | app_output["map"] = self.map_data 204 | 205 | for _tile_layer in self.tile_layers: 206 | s = {} 207 | for k, v in _tile_layer.__dict__.items(): 208 | _v = v 209 | s[k] = _v 210 | app_output["tile_layer_data"].append(s) 211 | 212 | for _wms_tile_layer in self.wms_tile_layers: 213 | s = {} 214 | for k, v in _wms_tile_layer.__dict__.items(): 215 | _v = v 216 | s[k] = _v 217 | app_output["wms_tile_layer_data"].append(s) 218 | 219 | for _base_layer in self.base_layers: 220 | s = {} 221 | for k, v in _base_layer.__dict__.items(): 222 | _v = v 223 | s[k] = _v 224 | app_output["base_layer_data"].append(s) 225 | 226 | for _vector_layer in self.vector_layers: 227 | s = {} 228 | for k, v in _vector_layer.__dict__.items(): 229 | _v = v 230 | if k == "data": 231 | _v = json.loads(v.to_json()) 232 | s[k] = _v 233 | 234 | app_output["vector_layer_data"].append(s) 235 | 236 | for _image_layer in self.image_layers: 237 | s = {} 238 | for k, v in _image_layer.__dict__.items(): 239 | _v = v 240 | s[k] = _v 241 | app_output["image_layer_data"].append(s) 242 | 243 | app_output["component_info"] = [ 244 | dataclasses.asdict(i) for i in self.registered_inputs 245 | ] 246 | 247 | logging.info("Len component info: ", len(app_output["component_info"])) 248 | 249 | return app_output 250 | 251 | # def gpo_reference_data(self): 252 | # """ Only return one reference image for testing. """ 253 | # if len(self.raster_image_reference) == 0: 254 | # return None 255 | # return self.raster_image_reference[0] 256 | 257 | 258 | app = GreppoApp() 259 | -------------------------------------------------------------------------------- /library/src/greppo/greppo_server.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from distutils.sysconfig import get_python_lib 4 | from functools import partial 5 | 6 | import uvicorn 7 | from greppo import GreppoApp 8 | from starlette.applications import Starlette 9 | from starlette.middleware import Middleware 10 | from starlette.middleware.cors import CORSMiddleware 11 | from starlette.requests import Request 12 | from starlette.responses import JSONResponse 13 | from starlette.responses import Response 14 | from starlette.routing import Mount 15 | from starlette.routing import Route 16 | from starlette.routing import WebSocketRoute 17 | from starlette.staticfiles import StaticFiles 18 | 19 | from .user_script_utils import script_task 20 | 21 | 22 | async def api_endpoint(user_script: str, request: Request): 23 | input_updates = {} 24 | try: 25 | input_updates = await request.json() 26 | logging.debug("Got input update: ", input_updates) 27 | except Exception as e: 28 | logging.debug("Unable to parse request body: ", await request.body(), e) 29 | 30 | payload, _ = await script_task(script_name=user_script, input_updates=input_updates) 31 | 32 | return JSONResponse(payload) 33 | 34 | 35 | def gen_ws_api_endpoint(user_script): 36 | async def ws_api_endpoint(websocket): 37 | await websocket.accept() 38 | 39 | input_updates = {} 40 | try: 41 | input_updates = await websocket.receive_json() 42 | logging.debug("Got input update: ", input_updates) 43 | except Exception as e: 44 | logging.debug( 45 | "Unable to parse request body: ", await websocket.get_text(), e 46 | ) 47 | 48 | payload, _ = await script_task( 49 | script_name=user_script, input_updates=input_updates 50 | ) 51 | await websocket.send_json(payload) 52 | 53 | await websocket.close() 54 | 55 | return ws_api_endpoint 56 | 57 | 58 | async def raster_api_endpoint(user_script: str, request: Request): 59 | input_updates = {} 60 | try: 61 | input_updates = await request.json() 62 | logging.debug("Got input update: ", input_updates) 63 | except Exception as e: 64 | logging.debug("Unable to parse request body: ", await request.body(), e) 65 | 66 | _, payload = await script_task(script_name=user_script, input_updates=input_updates) 67 | 68 | image_bytes_data = payload.read() if payload else None 69 | 70 | return Response(content=image_bytes_data, media_type="image/png") 71 | 72 | 73 | def get_static_dir_path(): 74 | dist_path = get_python_lib() + "/greppo" 75 | if os.path.isfile(dist_path): 76 | BASE_DIR = dist_path 77 | else: 78 | BASE_DIR = os.path.dirname(__file__) 79 | 80 | return BASE_DIR + "/static/" 81 | 82 | 83 | class GreppoServer(object): 84 | def __init__(self, gr_app: GreppoApp, user_script: str): 85 | self.gr_app = gr_app 86 | self.user_script = user_script 87 | 88 | def run(self, host="0.0.0.0", port=8080): 89 | routes = [ 90 | Route( 91 | "/api", partial(api_endpoint, self.user_script), methods=["GET", "POST"] 92 | ), 93 | Route( 94 | "/raster", 95 | partial(raster_api_endpoint, self.user_script), 96 | methods=["GET", "POST"], 97 | ), 98 | WebSocketRoute("/wsapi", gen_ws_api_endpoint(self.user_script)), 99 | Mount( 100 | "/", 101 | app=StaticFiles(directory=get_static_dir_path(), html=True), 102 | name="static", 103 | ), 104 | ] 105 | 106 | middleware = [ 107 | Middleware( 108 | CORSMiddleware, 109 | allow_origins=[ 110 | "http://localhost:8080", 111 | "http://0.0.0.0:8080", 112 | "http://localhost:8081", 113 | "http://0.0.0.0:8081", 114 | ], 115 | allow_methods=["GET", "POST"], 116 | ) 117 | ] 118 | 119 | app = Starlette(debug=True, routes=routes, middleware=middleware) 120 | uvicorn.run(app, host=host, port=port) 121 | 122 | def close(self): 123 | pass 124 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from .bar_chart import BarChart 4 | from .bar_chart import BarChartComponentInfo 5 | from .draw_feature import DrawFeature 6 | from .draw_feature import DrawFeatureComponentInfo 7 | from .line_chart import LineChart 8 | from .line_chart import LineChartComponentInfo 9 | from .multiselect import Multiselect 10 | from .multiselect import MultiselectComponentInfo 11 | from .number import Number 12 | from .number import NumberComponentInfo 13 | from .text import Text 14 | from .text import TextComponentInfo 15 | from .select import Select 16 | from .select import SelectComponentInfo 17 | from .display import Display 18 | from .display import DisplayComponentInfo 19 | 20 | # TODO add interface to inputs 21 | GreppoCharts = Union[BarChart, LineChart] 22 | GreppoChartNames = [i.proxy_name() for i in GreppoCharts.__args__] 23 | 24 | GreppoInputs = Union[Display, Number, Text, Select, Multiselect, DrawFeature] 25 | GreppoInputsNames = [i.proxy_name() for i in GreppoInputs.__args__] 26 | 27 | ComponentInfo = Union[ 28 | DisplayComponentInfo, 29 | NumberComponentInfo, 30 | TextComponentInfo, 31 | SelectComponentInfo, 32 | MultiselectComponentInfo, 33 | DrawFeatureComponentInfo, 34 | BarChartComponentInfo, 35 | LineChartComponentInfo, 36 | ] 37 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/bar_chart.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any 3 | from typing import Dict 4 | from typing import List, Union 5 | 6 | num = Union[int, float] 7 | num_list = List[num] 8 | num_num_list = Union[num, num_list] 9 | str_list = List[str] 10 | str_str_list = Union[str, str_list] 11 | x_type = Union[int, float, str] 12 | 13 | @dataclass 14 | class Dataset: 15 | data: List[num_num_list] 16 | label: str 17 | backgroundColor: str 18 | 19 | 20 | @dataclass 21 | class ChartData: 22 | labels: List[Any] 23 | datasets: List[Dataset] 24 | 25 | 26 | # TODO handle multiple datasets. 27 | @dataclass 28 | class BarChart: 29 | def __init__( 30 | self, 31 | name: str, 32 | x: List[x_type], 33 | y: List[num_num_list], 34 | color: str_str_list = "#CCCCCC", 35 | label: str_str_list = '', 36 | description: str = '', 37 | input_updates: Dict[str, Any] = {}, 38 | ): 39 | self.input_name = name 40 | self.description = description 41 | self.input_updates = input_updates 42 | 43 | if isinstance(y[0], list): 44 | dataset = [] 45 | for idx in range(len(y)): 46 | if isinstance(label, list) and (len(label) == len(y)): this_label = label[idx] 47 | else: raise ValueError('`label` passed in to the bar_chart must be of same length as `y`') 48 | if isinstance(color, list) and (len(color) == len(y)): this_color = color[idx] 49 | else: raise ValueError('`color` passed in to the bar_chart must be of same length as `y`') 50 | dataset.append(Dataset(label=this_label, data=y[idx],backgroundColor=this_color)) 51 | self.chartdata = ChartData(labels=x, datasets=dataset) 52 | else: 53 | if not bool(label): 54 | _, label = name.split("_") 55 | dataset = Dataset(label=label, data=y,backgroundColor=color) 56 | self.chartdata = ChartData(labels=x, datasets=[dataset]) 57 | 58 | def get_value(self): 59 | return None 60 | 61 | def convert_to_component_info(self): 62 | _id, name = self.input_name.split("_") 63 | _type = BarChart.__name__ 64 | 65 | return BarChartComponentInfo( 66 | name=name, 67 | id=_id, 68 | type=_type, 69 | description=self.description, 70 | chartdata=self.chartdata, 71 | ) 72 | 73 | @classmethod 74 | def proxy_name(cls): 75 | return "bar_chart" 76 | 77 | def __repr__(self): 78 | return str(self.get_value()) 79 | 80 | def __str__(self): 81 | return self.__repr__() 82 | 83 | 84 | @dataclass 85 | class BarChartComponentInfo: 86 | name: str 87 | id: str 88 | type: str 89 | description: str 90 | chartdata: ChartData 91 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/display.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any 3 | from typing import Dict 4 | from typing import Union 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | 10 | @dataclass 11 | class Display: 12 | def __init__(self, name: str, value: str, input_updates: Dict[str, Any] = {}): 13 | self.input_name = name 14 | self.value = value 15 | 16 | def get_value(self): 17 | id, name = self.input_name.split("_") 18 | 19 | return self.value 20 | 21 | def convert_to_component_info(self): 22 | _id, name = self.input_name.split("_") 23 | _type = Display.__name__ 24 | value = str(self.get_value()) 25 | 26 | return DisplayComponentInfo(id=_id, name=name, type=_type, value=value) 27 | 28 | @classmethod 29 | def proxy_name(cls): 30 | return "display" 31 | 32 | def __repr__(self): 33 | return str(self.get_value()) 34 | 35 | def __str__(self): 36 | return self.__repr__() 37 | 38 | 39 | @dataclass 40 | class DisplayComponentInfo: 41 | id: str 42 | name: str 43 | type: str 44 | value: Any 45 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/draw_feature.py: -------------------------------------------------------------------------------- 1 | import json 2 | from dataclasses import dataclass 3 | from enum import Enum 4 | from typing import Any 5 | from typing import Dict 6 | from typing import List 7 | 8 | from geopandas import GeoDataFrame as gdf 9 | from shapely.geometry import LineString 10 | from shapely.geometry import Point 11 | from shapely.geometry import Polygon 12 | 13 | 14 | default_gdf = gdf( 15 | [{"id": 9999, "type": "Point", "geometry": Point(0, 0)}], crs="EPSG:4326" 16 | ) 17 | 18 | 19 | @dataclass 20 | class DrawFeature: 21 | def __init__( 22 | self, 23 | name: str, 24 | geometry: list, 25 | features: gdf = default_gdf, 26 | input_updates: Dict[str, Any] = {}, 27 | ): 28 | self.input_name = name 29 | self.geometry = geometry 30 | # TODO Check if it matches EPSG 4326/WSG84 31 | self.features = features.explode(ignore_index=True) 32 | 33 | self.input_updates = input_updates 34 | 35 | def get_value(self): 36 | id, name = self.input_name.split("_") 37 | if name in self.input_updates: 38 | return draw_feature_dict_2_gdf(self.input_updates.get(name)) 39 | 40 | return self.features 41 | 42 | def convert_to_component_info(self): 43 | _id, name = self.input_name.split("_") 44 | _type = DrawFeature.__name__ 45 | _geometry = self.geometry 46 | _features = draw_feature_gdf_2_dict(self.get_value()) 47 | 48 | return DrawFeatureComponentInfo( 49 | id=_id, name=name, type=_type, geometry=_geometry, features=_features 50 | ) 51 | 52 | @classmethod 53 | def proxy_name(cls): 54 | return "draw_feature" 55 | 56 | def __repr__(self): 57 | return str(self.get_value()) 58 | 59 | def __str__(self): 60 | return self.__repr__() 61 | 62 | 63 | def draw_feature_dict_2_gdf(features_dict): 64 | features_list = [] 65 | for feature in features_dict: 66 | _points = [] 67 | _type = feature["type"] 68 | for latlng in feature["latlngs"]: 69 | _points.append(Point(latlng["lng"], latlng["lat"])) 70 | 71 | if _type == "LineString": 72 | feature_geom = LineString(_points) 73 | 74 | if _type == "Polygon": 75 | feature_geom = Polygon(_points) 76 | 77 | if _type == "Point": 78 | feature_geom = _points[0] 79 | 80 | features_list.append({"type": _type, "geometry": feature_geom}) 81 | 82 | features_gdf = gdf(features_list, crs="EPSG:4326") 83 | 84 | return features_gdf 85 | 86 | 87 | def draw_feature_gdf_2_dict(features_gdf): 88 | features_dict = [] 89 | for index, feature in features_gdf.iterrows(): 90 | _type = feature["geometry"].geom_type 91 | latlngs = [] 92 | if _type == "Polygon": 93 | for (lng, lat) in list(feature["geometry"].exterior.coords): 94 | latlngs.append({"lat": lat, "lng": lng}) 95 | 96 | if _type == "LineString": 97 | for (lng, lat) in list(feature["geometry"].coords): 98 | latlngs.append({"lat": lat, "lng": lng}) 99 | 100 | if _type == "Point": 101 | for (lng, lat) in list(feature["geometry"].coords): 102 | latlngs.append({"lat": lat, "lng": lng}) 103 | 104 | features_dict.append({"type": _type, "latlngs": latlngs}) 105 | return features_dict 106 | 107 | 108 | @dataclass 109 | class DrawFeatureComponentInfo: 110 | id: str 111 | type: str 112 | name: str 113 | geometry: list 114 | features: dict 115 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/line_chart.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any 3 | from typing import Dict 4 | from typing import List, Union 5 | 6 | num = Union[int, float] 7 | num_list = List[num] 8 | num_num_list = Union[num, num_list] 9 | str_list = List[str] 10 | str_str_list = Union[str, str_list] 11 | x_type = Union[int, float, str] 12 | 13 | @dataclass 14 | class Dataset: 15 | data: List[num_num_list] 16 | label: str 17 | backgroundColor: str 18 | borderColor: str 19 | fill: bool = False 20 | pointRadius = 10 21 | 22 | 23 | @dataclass 24 | class ChartData: 25 | labels: List[Any] 26 | datasets: List[Dataset] 27 | 28 | 29 | # TODO handle multiple datasets. 30 | @dataclass 31 | class LineChart: 32 | def __init__( 33 | self, 34 | name: str, 35 | x: List[x_type], 36 | y: List[num_num_list], 37 | color: str_str_list = "#CCCCCC", 38 | label: str_str_list = '', 39 | description: str = '', 40 | input_updates: Dict[str, Any] = {}, 41 | ): 42 | self.input_name = name 43 | self.description = description 44 | self.input_updates = input_updates 45 | 46 | if isinstance(y[0], list): 47 | dataset = [] 48 | for idx in range(len(y)): 49 | if isinstance(label, list) and (len(label) == len(y)): this_label = label[idx] 50 | else: raise ValueError('`label` passed in to the line_chart must be of same length as `y`') 51 | if isinstance(color, list) and (len(color) == len(y)): this_color = color[idx] 52 | else: raise ValueError('`color` passed in to the line_chart must be of same length as `y`') 53 | dataset.append(Dataset(label=this_label, data=y[idx],backgroundColor=this_color, borderColor=this_color)) 54 | self.chartdata = ChartData(labels=x, datasets=dataset) 55 | else: 56 | if not bool(label): 57 | _, label = name.split("_") 58 | dataset = Dataset(label=label, data=y,backgroundColor=color, borderColor=color) 59 | self.chartdata = ChartData(labels=x, datasets=[dataset]) 60 | 61 | def get_value(self): 62 | return None 63 | 64 | def convert_to_component_info(self): 65 | _id, name = self.input_name.split("_") 66 | _type = LineChart.__name__ 67 | 68 | return LineChartComponentInfo( 69 | name=name, 70 | id=_id, 71 | type=_type, 72 | description=self.description, 73 | chartdata=self.chartdata, 74 | ) 75 | 76 | @classmethod 77 | def proxy_name(cls): 78 | return "line_chart" 79 | 80 | def __repr__(self): 81 | return str(self.get_value()) 82 | 83 | def __str__(self): 84 | return self.__repr__() 85 | 86 | 87 | @dataclass 88 | class LineChartComponentInfo: 89 | name: str 90 | id: str 91 | type: str 92 | description: str 93 | chartdata: ChartData 94 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/multiselect.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any 3 | from typing import Dict 4 | from typing import List 5 | from typing import Union 6 | 7 | 8 | SELECT_TYPES = Union[int, float, str, bool] 9 | 10 | 11 | @dataclass 12 | class Multiselect: 13 | def __init__( 14 | self, 15 | name: str, 16 | options: List[SELECT_TYPES], 17 | default: List[SELECT_TYPES], 18 | input_updates: Dict[str, Any] = {}, 19 | ): 20 | self.input_name = name 21 | self.options = options 22 | self.default = default 23 | 24 | self.input_updates = input_updates 25 | 26 | def get_value(self): 27 | id, name = self.input_name.split("_") 28 | return self.input_updates.get(name, self.default) 29 | 30 | def convert_to_component_info(self): 31 | _id, name = self.input_name.split("_") 32 | _type = Multiselect.__name__ 33 | value = self.get_value() 34 | 35 | return MultiselectComponentInfo( 36 | id=_id, name=name, type=_type, value=value, options=self.options 37 | ) 38 | 39 | @classmethod 40 | def proxy_name(cls): 41 | return "multiselect" 42 | 43 | def __repr__(self): 44 | return str(self.get_value()) 45 | 46 | def __str__(self): 47 | return self.__repr__() 48 | 49 | 50 | @dataclass 51 | class MultiselectComponentInfo: 52 | id: str 53 | name: str 54 | type: str 55 | value: Union[ 56 | int, float, str, bool 57 | ] # This can represent the default user supplied value 58 | options: List[Any] 59 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/number.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from dataclasses import dataclass 3 | from typing import Any 4 | from typing import Dict 5 | from typing import Union 6 | 7 | import numpy as np 8 | import pandas as pd 9 | 10 | 11 | @dataclass 12 | class Number: 13 | def __init__( 14 | self, name: str, value: Union[int, float], input_updates: Dict[str, Any] = {} 15 | ): 16 | self.input_name = name 17 | self.value = value 18 | 19 | self.input_updates = input_updates 20 | 21 | def get_value(self): 22 | id, name = self.input_name.split("_") 23 | return self.input_updates.get(name, self.value) 24 | 25 | def convert_to_component_info(self): 26 | _id, name = self.input_name.split("_") 27 | _type = Number.__name__ 28 | value = str(self.get_value()) 29 | 30 | return NumberComponentInfo(id=_id, name=name, type=_type, value=value) 31 | 32 | @classmethod 33 | def proxy_name(cls): 34 | return "number" 35 | 36 | def __add__(self, other): 37 | if type(other) in [int, float]: 38 | return self.get_value() + other 39 | elif type(other) is Number: 40 | return self.get_value() + other.get_value() 41 | else: 42 | raise Exception("Operation Not Supported for type: " + str(type(other))) 43 | 44 | __radd__ = __add__ 45 | 46 | def __mul__(self, other): 47 | if type(other) in [int, float]: 48 | return self.get_value() * other 49 | elif type(other) in [list, str, np.array, np.ndarray, pd.Series]: 50 | return other * self.get_value() 51 | elif type(other) is Number: 52 | return self.get_value() * other.get_value() 53 | else: 54 | raise Exception("Operation Not Supported for type: " + str(type(other))) 55 | 56 | __rmul__ = __mul__ 57 | 58 | def __repr__(self): 59 | return str(self.get_value()) 60 | 61 | def __str__(self): 62 | return self.__repr__() 63 | 64 | 65 | @dataclass 66 | class NumberComponentInfo: 67 | id: str 68 | name: str 69 | type: str 70 | value: Any 71 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/select.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any 3 | from typing import Dict 4 | from typing import List 5 | from typing import Union 6 | 7 | 8 | SELECT_TYPES = Union[int, float, str, bool] 9 | 10 | 11 | @dataclass 12 | class Select: 13 | def __init__( 14 | self, 15 | name: str, 16 | options: List[SELECT_TYPES], 17 | default: SELECT_TYPES, 18 | input_updates: Dict[str, Any] = {}, 19 | ): 20 | self.input_name = name 21 | self.options = options 22 | self.default = default 23 | 24 | self.input_updates = input_updates 25 | 26 | def get_value(self): 27 | id, name = self.input_name.split("_") 28 | return self.input_updates.get(name, self.default) 29 | 30 | def convert_to_component_info(self): 31 | _id, name = self.input_name.split("_") 32 | _type = Select.__name__ 33 | value = str(self.get_value()) 34 | 35 | return SelectComponentInfo( 36 | id=_id, name=name, type=_type, value=value, options=self.options 37 | ) 38 | 39 | @classmethod 40 | def proxy_name(cls): 41 | return "select" 42 | 43 | def __repr__(self): 44 | return str(self.get_value()) 45 | 46 | def __str__(self): 47 | return self.__repr__() 48 | 49 | 50 | @dataclass 51 | class SelectComponentInfo: 52 | id: str 53 | name: str 54 | type: str 55 | value: Union[ 56 | int, float, str, bool 57 | ] # This can represent the default user supplied value 58 | options: List[Any] 59 | -------------------------------------------------------------------------------- /library/src/greppo/input_types/text.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Any 3 | from typing import Dict 4 | from typing import Union 5 | 6 | import numpy as np 7 | import pandas as pd 8 | 9 | @dataclass 10 | class Text: 11 | def __init__(self, name: str, value: str, input_updates: Dict[str, Any] = {}): 12 | self.input_name = name 13 | self.value = value 14 | 15 | self.input_updates = input_updates 16 | 17 | def get_value(self): 18 | id, name = self.input_name.split("_") 19 | return self.input_updates.get(name, self.value) 20 | 21 | def convert_to_component_info(self): 22 | _id, name = self.input_name.split("_") 23 | _type = Text.__name__ 24 | value = str(self.get_value()) 25 | 26 | return TextComponentInfo(id=_id, name=name, type=_type, value=value) 27 | 28 | @classmethod 29 | def proxy_name(cls): 30 | return "text" 31 | 32 | def __add__(self, other): 33 | if type(other) in [str]: 34 | return self.get_value() + other 35 | elif type(other) is Text: 36 | return self.get_value() + other.get_value() 37 | else: 38 | raise Exception("Operation Not Supported for type: " + str(type(other))) 39 | 40 | __radd__ = __add__ 41 | 42 | def __mul__(self, other): 43 | if type(other) in [str]: 44 | return self.get_value() * other 45 | elif type(other) in [list, str, np.array, np.ndarray, pd.Series]: 46 | return other * self.get_value() 47 | elif type(other) is Text: 48 | return self.get_value() * other.get_value() 49 | else: 50 | raise Exception("Operation Not Supported for type: " + str(type(other))) 51 | 52 | __rmul__ = __mul__ 53 | 54 | def __repr__(self): 55 | return str(self.get_value()) 56 | 57 | def __str__(self): 58 | return self.__repr__() 59 | 60 | 61 | @dataclass 62 | class TextComponentInfo: 63 | id: str 64 | name: str 65 | type: str 66 | value: Any 67 | -------------------------------------------------------------------------------- /library/src/greppo/layers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/library/src/greppo/layers/__init__.py -------------------------------------------------------------------------------- /library/src/greppo/layers/base_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Union 3 | import xyzservices 4 | import uuid 5 | 6 | 7 | @dataclass 8 | class BaseLayerComponent: 9 | def __init__( 10 | self, 11 | provider: Union[str, xyzservices.TileProvider] = '', 12 | name: str = '', 13 | visible: bool = True, 14 | url: str = '', 15 | attribution: str = '', 16 | subdomains: Union[str, List[str]] = '', 17 | min_zoom: int = 0, 18 | max_zoom: int = 18, 19 | bounds: List[List[int]] = [], 20 | ): 21 | self.provider = provider 22 | self.name = name 23 | self.visible = visible 24 | self.url = url 25 | self.attribution = attribution 26 | self.subdomains = subdomains 27 | self.min_zoom = min_zoom 28 | self.max_zoom = max_zoom 29 | self.bounds = bounds 30 | 31 | def convert_to_dataclass(self): 32 | id = uuid.uuid4().hex 33 | tile_provider = None 34 | 35 | if self.provider and isinstance(self.provider, str): 36 | try: 37 | tile_provider = xyzservices.providers.query_name(self.provider) 38 | except ValueError: 39 | raise ValueError( 40 | f"No matching provider found for: '{self.provider}'.") 41 | 42 | if isinstance(self.provider, xyzservices.TileProvider): 43 | tile_provider = self.provider 44 | 45 | if tile_provider is not None: 46 | name = self.name if self.name else tile_provider.name.replace( 47 | ".", " - ") 48 | url = self.url if self.url else tile_provider.build_url( 49 | fill_subdomain=False) 50 | attribution = self.attribution if self.attribution else tile_provider.attribution if 'attribution' in tile_provider else tile_provider.html_attribution 51 | subdomains = self.subdomains if self.subdomains else tile_provider.subdomains if 'subdomains' in tile_provider else '' 52 | 53 | return BaseLayer(id, name=name, visible=self.visible, url=url, subdomains=subdomains, attribution=attribution) 54 | 55 | else: 56 | if not self.name: 57 | raise ValueError( 58 | "If 'provider' is not passed for 'base_layer', please pass in 'name'.") 59 | if not self.url: 60 | raise ValueError( 61 | "If 'provider' is not passed for 'base_layer', please pass in 'url'.") 62 | 63 | return BaseLayer(id, name=self.name, visible=self.visible, url=self.url, subdomains=self.subdomains, attribution=self.attribution) 64 | 65 | 66 | @dataclass() 67 | class BaseLayer: 68 | id: str 69 | name: str 70 | visible: bool 71 | url: str 72 | subdomains: Union[str, List[str]] 73 | attribution: str 74 | -------------------------------------------------------------------------------- /library/src/greppo/layers/ee_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Union, Dict 3 | from .tile_layer import TileLayer 4 | import ee 5 | import uuid 6 | 7 | 8 | @dataclass 9 | class EarthEngineLayerComponent: 10 | def __init__( 11 | self, 12 | ee_object: Union[ee.Image, ee.ImageCollection, ee.FeatureCollection, ee.Feature, ee.Geometry], 13 | name: str = '', 14 | description: str = '', 15 | visible: bool = True, 16 | vis_params: Dict = {}, 17 | opacity: float = 1.0, 18 | ): 19 | self.name = name 20 | self.description = description 21 | self.visible = visible 22 | self.opacity = opacity 23 | 24 | if ( 25 | isinstance(ee_object, ee.geometry.Geometry) 26 | or isinstance(ee_object, ee.feature.Feature) 27 | or isinstance(ee_object, ee.featurecollection.FeatureCollection) 28 | ): 29 | features = ee.FeatureCollection(ee_object) 30 | 31 | width = vis_params.get('width', 2) 32 | color = vis_params.get('color', '000000') 33 | 34 | image_fill = features.style(**{"fillColor": color}).updateMask( 35 | ee.Image.constant(0.5) 36 | ) 37 | image_outline = features.style( 38 | **{"color": color, "fillColor": "00000000", "width": width} 39 | ) 40 | 41 | self.ee_object_image = image_fill.blend(image_outline) 42 | elif isinstance(ee_object, ee.image.Image): 43 | self.ee_object_image = ee_object 44 | elif isinstance(ee_object, ee.imagecollection.ImageCollection): 45 | self.ee_object_image = ee_object.mosaic() 46 | 47 | self.vis_params = vis_params 48 | 49 | def convert_to_dataclass(self): 50 | id = uuid.uuid4().hex 51 | map_id_dict = ee.Image(self.ee_object_image).getMapId(self.vis_params) 52 | url = map_id_dict["tile_fetcher"].url_format 53 | return TileLayer(id=id, url=url, name=self.name, description=self.description, visible=self.visible, opacity=self.opacity) 54 | -------------------------------------------------------------------------------- /library/src/greppo/layers/image_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | import numpy as np 4 | import uuid 5 | import io 6 | import base64 7 | import os 8 | import json 9 | from urllib.parse import urlparse, uses_netloc, uses_params, uses_relative 10 | 11 | 12 | @dataclass 13 | class ImageLayerComponent: 14 | def __init__( 15 | self, 16 | image: str, 17 | name: str, 18 | bounds: List[List[float]], # [[lat_min, lon_min], [lat_max, lon_max]] 19 | description: str = '', 20 | visible: bool = True, 21 | opacity: float = 1.0, 22 | ): 23 | self.name = name 24 | self.description = description 25 | self.visible = visible 26 | self.opacity = opacity 27 | 28 | # image_ext = image.split(".")[-1].lower() 29 | # buffered = io.BytesIO() 30 | # image_open = Image.open(image) 31 | # image_open.save(buffered, format="JPEG") 32 | # image_string = base64.b64encode(buffered.getvalue()).decode() 33 | # self.image = f"data:image/{image_ext};base64," + image_string 34 | self.url = _image_to_url(image) 35 | 36 | assert ((len(bounds) == 2) and (len(bounds[0]) == 2) and ( 37 | len(bounds[1]) == 2)), "bounds not of format `[[lat_min, lon_min], [lat_max, lon_max]]`" 38 | self.bounds = bounds 39 | 40 | def convert_to_dataclass(self): 41 | id = uuid.uuid4().hex 42 | return ImageLayer(id=id, url=self.url, bounds=self.bounds, name=self.name, description=self.description, visible=self.visible, opacity=self.opacity) 43 | 44 | 45 | @dataclass 46 | class ImageLayer: 47 | id: str 48 | url: str 49 | name: str 50 | description: str 51 | bounds: List[List] 52 | visible: bool 53 | opacity: float 54 | 55 | 56 | def _image_to_url(image): 57 | """ 58 | Adopted from Folium 59 | https://github.com/python-visualization/folium/blob/551b2420150ab56b71dcf14c62e5f4b118caae32/folium/raster_layers.py#L187 60 | """ 61 | if isinstance(image, str) and not _is_url(image): 62 | img_ext = os.path.splitext(image)[-1][1:] 63 | assert img_ext in [ 64 | "png", 65 | "jpg", 66 | "jpeg", 67 | "tiff", 68 | ], "Image input extension should be png, jpg or jpeg for image_layer" 69 | with io.open(image, 'rb') as f: 70 | img = f.read() 71 | b64encoded = base64.b64encode(img).decode('utf-8') 72 | url = 'data:image/{};base64,{}'.format(img_ext, b64encoded) 73 | elif _is_url(image): 74 | # Round-trip to ensure a nice formatted json. 75 | url = json.loads(json.dumps(image)) 76 | else: 77 | assert False, "image not of correct format." 78 | return url.replace('\n', ' ') 79 | 80 | 81 | def _is_url(url): 82 | """ 83 | Check to see if `url` has a valid protocol. 84 | 85 | Taken from: 86 | https://github.com/python-visualization/folium/blob/551b2420150ab56b71dcf14c62e5f4b118caae32/folium/utilities.py#L148 87 | """ 88 | _VALID_URLS = set(uses_relative + uses_netloc + uses_params) 89 | _VALID_URLS.discard('') 90 | _VALID_URLS.add('data') 91 | 92 | try: 93 | return urlparse(url).scheme in _VALID_URLS 94 | except Exception: 95 | return False 96 | -------------------------------------------------------------------------------- /library/src/greppo/layers/overlay_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | from geopandas import GeoDataFrame as gdf 5 | 6 | 7 | @dataclass 8 | class OverlayLayer: 9 | id: str 10 | data: gdf 11 | name: str 12 | description: str 13 | style: dict 14 | visible: bool 15 | viewzoom: List 16 | -------------------------------------------------------------------------------- /library/src/greppo/layers/raster_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Callable 3 | import uuid 4 | import struct 5 | import zlib 6 | import base64 7 | import numpy as np 8 | from .image_layer import ImageLayer 9 | 10 | 11 | @dataclass 12 | class RasterLayerComponent: 13 | def __init__( 14 | self, 15 | data: np.ndarray, 16 | name: str, 17 | bounds: List[List[float]], # [[lat_min, lon_min], [lat_max, lon_max]] 18 | description: str = '', 19 | origin: str = 'upper', 20 | visible: bool = True, 21 | opacity: float = 1.0, 22 | colormap: Callable = None 23 | ): 24 | self.name = name 25 | self.description = description 26 | self.visible = visible 27 | self.opacity = opacity 28 | assert origin in [ 29 | "upper", "lower"], "origin should be one of `upper` or `lower`" 30 | 31 | assert 'ndarray' in data.__class__.__name__, "raster data not of right format" 32 | img = _write_png(data, origin=origin, colormap=colormap) 33 | b64encoded = base64.b64encode(img).decode('utf-8') 34 | self.url = 'data:image/png;base64,{}'.format( 35 | b64encoded).replace('\n', ' ') 36 | 37 | assert ((len(bounds) == 2) and (len(bounds[0]) == 2) and ( 38 | len(bounds[1]) == 2)), "bounds not of format `[[lat_min, lon_min], [lat_max, lon_max]]`" 39 | self.bounds = bounds 40 | 41 | def convert_to_dataclass(self): 42 | id = uuid.uuid4().hex 43 | return ImageLayer(id=id, url=self.url, bounds=self.bounds, name=self.name, description=self.description, visible=self.visible, opacity=self.opacity) 44 | 45 | 46 | def _write_png(data, origin='upper', colormap=None): 47 | """ 48 | Taken from: 49 | https://github.com/python-visualization/folium/blob/551b2420150ab56b71dcf14c62e5f4b118caae32/folium/utilities.py#L156 50 | 51 | Transform an array of data into a PNG string. 52 | 53 | Inspired from 54 | https://stackoverflow.com/questions/902761/saving-a-numpy-array-as-an-image 55 | """ 56 | 57 | if colormap is None: 58 | def colormap(x): 59 | return (x, x, x, 1) 60 | 61 | arr = np.atleast_3d(data) 62 | height, width, nblayers = arr.shape 63 | 64 | if nblayers not in [1, 3, 4]: 65 | raise ValueError('Data must be NxM (mono), ' 66 | 'NxMx3 (RGB), or NxMx4 (RGBA)') 67 | assert arr.shape == (height, width, nblayers) 68 | 69 | if nblayers == 1: 70 | arr = np.array(list(map(colormap, arr.ravel()))) 71 | nblayers = arr.shape[1] 72 | if nblayers not in [3, 4]: 73 | raise ValueError('colormap must provide colors of r' 74 | 'length 3 (RGB) or 4 (RGBA)') 75 | arr = arr.reshape((height, width, nblayers)) 76 | assert arr.shape == (height, width, nblayers) 77 | 78 | if nblayers == 3: 79 | arr = np.concatenate((arr, np.ones((height, width, 1))), axis=2) 80 | nblayers = 4 81 | assert arr.shape == (height, width, nblayers) 82 | assert nblayers == 4 83 | 84 | # Normalize to uint8 if it isn't already. 85 | if arr.dtype != 'uint8': 86 | with np.errstate(divide='ignore', invalid='ignore'): 87 | arr = arr * 255./arr.max(axis=(0, 1)).reshape((1, 1, 4)) 88 | arr[~np.isfinite(arr)] = 0 89 | arr = arr.astype('uint8') 90 | 91 | # Eventually flip the image. 92 | if origin == 'lower': 93 | arr = arr[::-1, :, :] 94 | 95 | # Transform the array to bytes. 96 | raw_data = b''.join([b'\x00' + arr[i, :, :].tobytes() 97 | for i in range(height)]) 98 | 99 | def png_pack(png_tag, data): 100 | chunk_head = png_tag + data 101 | return (struct.pack('!I', len(data)) + 102 | chunk_head + 103 | struct.pack('!I', 0xFFFFFFFF & zlib.crc32(chunk_head))) 104 | 105 | return b''.join([ 106 | b'\x89PNG\r\n\x1a\n', 107 | png_pack(b'IHDR', struct.pack('!2I5B', width, height, 8, 6, 0, 0, 0)), 108 | png_pack(b'IDAT', zlib.compress(raw_data, 9)), 109 | png_pack(b'IEND', b'')]) 110 | 111 | 112 | # src_dataset = rasterio.open(file_path) 113 | # dst_crs = "EPSG:4326" 114 | 115 | # transform, width, height = calculate_default_transform( 116 | # src_dataset.crs, 117 | # dst_crs, 118 | # src_dataset.width, 119 | # src_dataset.height, 120 | # *src_dataset.bounds 121 | # ) 122 | 123 | # dst_bands = [] 124 | # for band_n_1 in range(src_dataset.count): 125 | # src_band = rasterio.band(src_dataset, band_n_1 + 1) 126 | # dst_band = reproject(src_band, dst_crs=dst_crs) 127 | # dst_bands.append(dst_band) 128 | 129 | # if src_dataset.count != 3: 130 | # for i in range(len(dst_bands), src_dataset.count): 131 | # dst_bands.append(rasterio.band(src_dataset, 1)) 132 | 133 | # alpha = np.where(dst_bands[0][0] > 1e8, 0, 1) 134 | # alpha_band = list(copy.deepcopy(dst_bands[0])) 135 | # alpha_band[0] = alpha.astype("uint8") 136 | # dst_bands.append(tuple(alpha_band)) 137 | 138 | # png_kwargs = src_dataset.meta.copy() 139 | # png_kwargs.update( 140 | # { 141 | # "crs": dst_crs, 142 | # "width": width, 143 | # "height": height, 144 | # "driver": "PNG", 145 | # "dtype": rasterio.uint8, 146 | # "transform": transform, 147 | # "count": len(dst_bands), 148 | # } 149 | # ) 150 | 151 | # with MemoryFile() as png_memfile: 152 | # with png_memfile.open(**png_kwargs) as dst_file: 153 | # for i_1, dst_band in enumerate(dst_bands): 154 | # dst_file.write(dst_band[0][0], i_1 + 1) 155 | 156 | # dst_file.write_colormap( 157 | # i_1 + 1, {0: (255, 0, 0, 255), 255: (0, 0, 0, 255)} 158 | # ) 159 | 160 | # self.raster_image_reference.append(png_memfile.read()) 161 | 162 | # url = ( 163 | # "data:image/png;base64," + 164 | # base64.b64encode(png_memfile.read()).decode() 165 | # ) 166 | # (bounds_bottom, bounds_right) = transform * (0, 0) 167 | # (bounds_top, bounds_left) = transform * (width, height) 168 | # bounds = [[bounds_left, bounds_bottom], [bounds_right, bounds_top]] 169 | -------------------------------------------------------------------------------- /library/src/greppo/layers/tile_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | import uuid 3 | 4 | 5 | @dataclass 6 | class TileLayerComponent: 7 | def __init__( 8 | self, 9 | url: str, 10 | name: str, 11 | description: str = '', 12 | visible: bool = True, 13 | opacity: float = 1.0, 14 | ): 15 | self.url = url 16 | self.name = name 17 | self.description = description 18 | self.visible = visible 19 | self.opacity = opacity 20 | 21 | def convert_to_dataclass(self): 22 | id = uuid.uuid4().hex 23 | return TileLayer(id=id, url=self.url, name=self.name, description=self.description, visible=self.visible, opacity=self.opacity) 24 | 25 | 26 | @dataclass() 27 | class TileLayer: 28 | id: str 29 | url: str 30 | name: str 31 | description: str 32 | visible: bool 33 | opacity: float 34 | -------------------------------------------------------------------------------- /library/src/greppo/layers/vector_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Union 3 | import uuid 4 | from geopandas import GeoDataFrame as gdf 5 | from numpy import arange 6 | from ..colorbrewer import get_palette 7 | 8 | 9 | @dataclass 10 | class VectorLayerComponent: 11 | def __init__( 12 | self, 13 | data: gdf, 14 | name: str, 15 | description: str = '', 16 | style: dict = {}, 17 | visible: bool = True 18 | ): 19 | self.data = data 20 | self.name = name 21 | self.description = description 22 | self.style = style 23 | self.visible = visible 24 | 25 | if 'choropleth' in style: 26 | choropleth_style = style['choropleth'] 27 | if 'key_on' not in choropleth_style: 28 | raise ValueError( 29 | '"key_on" not specified in style: "choropleth" for vector_layer') 30 | else: 31 | key_on = choropleth_style['key_on'] 32 | if key_on in data: 33 | choropleth_on_data = data[key_on] 34 | else: 35 | raise ValueError( 36 | f'"{key_on}" is an invalid key for the passed in DataFrame') 37 | bin_min = choropleth_on_data.min() 38 | bin_max = choropleth_on_data.max() 39 | if 'bins' in choropleth_style: 40 | bins = choropleth_style['bins'] 41 | if isinstance(bins, int): 42 | if bins <= 0: 43 | ValueError( 44 | f'"{bins}" is a invalid for "bins" in style: "choropleth", "bins" must be >= 1') 45 | bin_step = (bin_min+bin_max)/bins 46 | bins = list(arange(bin_min, bin_max, bin_step)) 47 | choropleth_style['bins'] = bins 48 | elif isinstance(bins, List): 49 | if not all(bin_min <= x <= bin_max for x in bins[1:]): 50 | raise ValueError( 51 | 'Values in "bins" do not match the values in the DataFrame') 52 | if not all(x <= y for x, y in zip(bins[:-1], bins[1:])): 53 | raise ValueError('Values in "bins" are not increasing') 54 | else: 55 | bin_step = (bin_min+bin_max)/6 56 | bins = list(arange(bin_min, bin_max, bin_step)) 57 | choropleth_style['bins'] = bins 58 | bins_length = len(bins) 59 | if 'palette' in choropleth_style: 60 | if isinstance(choropleth_style['palette'], str): 61 | choropleth_style['palette'] = get_palette( 62 | bins_length, choropleth_style['palette']) 63 | elif isinstance(choropleth_style['palette'], List[str]): 64 | if bins_length == len(choropleth_style['palette']): 65 | pass 66 | # TODO Check if each item is a valid color Hex or RGB code. 67 | else: 68 | raise ValueError( 69 | '"bins" length and "palette" length must match') 70 | else: 71 | choropleth_style['palette'] = get_palette(bins_length, 'Purples') 72 | 73 | choropleth_style['bins'] = list(reversed(choropleth_style['bins'])) 74 | choropleth_style['palette'] = list(reversed(choropleth_style['palette'])) 75 | self.style['choropleth'] = choropleth_style 76 | 77 | def convert_to_dataclass(self): 78 | id = uuid.uuid4().hex 79 | gdf_bounds = self.data.total_bounds 80 | bounds = [[gdf_bounds[1], gdf_bounds[0]], 81 | [gdf_bounds[3], gdf_bounds[2]]] 82 | return VectorLayer(id=id, data=self.data, name=self.name, description=self.description, style=self.style, visible=self.visible, bounds=bounds) 83 | 84 | 85 | @dataclass 86 | class VectorLayer: 87 | id: str 88 | data: gdf 89 | name: str 90 | description: str 91 | style: dict 92 | visible: bool 93 | bounds: List[List] 94 | -------------------------------------------------------------------------------- /library/src/greppo/layers/wms_tile_layer.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List, Union 3 | import uuid 4 | 5 | 6 | @dataclass 7 | class WMSTileLayerComponent: 8 | def __init__( 9 | self, 10 | url: str, 11 | name: str, 12 | description: str = '', 13 | visible: bool = True, 14 | opacity: float = 1.0, 15 | layers: str = '', 16 | subdomains: Union[str, List[str]] = '', 17 | attribution: str = '', 18 | transparent: bool = True, 19 | format: str = 'image/jpeg', 20 | ): 21 | self.url = url 22 | self.name = name 23 | self.description = description 24 | self.visible = visible 25 | self.opacity = opacity 26 | self.layers = layers, 27 | self.subdomains = subdomains, 28 | self.attribution = attribution, 29 | self.transparent = transparent, 30 | self.format = format, 31 | 32 | def convert_to_dataclass(self): 33 | id = uuid.uuid4().hex 34 | return WMSTileLayer(id=id, url=self.url, name=self.name, description=self.description, visible=self.visible, opacity=self.opacity, layers=self.layers, subdomains=self.subdomains, attribution=self.attribution, transparent=self.transparent, format=self.format) 35 | 36 | 37 | @dataclass() 38 | class WMSTileLayer: 39 | id: str 40 | url: str 41 | name: str 42 | description: str 43 | layers: str 44 | visible: bool 45 | opacity: float 46 | subdomains: Union[str, List[str]] 47 | attribution: str 48 | transparent: bool 49 | format: str 50 | -------------------------------------------------------------------------------- /library/src/greppo/osm.py: -------------------------------------------------------------------------------- 1 | """OpenStreetMap utilities Python. 2 | 3 | Modified version of github.com/rossant/smopy . 4 | """ 5 | # ----------------------------------------------------------------------------- 6 | # Imports 7 | # ----------------------------------------------------------------------------- 8 | import logging 9 | 10 | import numpy as np 11 | 12 | # ----------------------------------------------------------------------------- 13 | # OSM functions 14 | # ----------------------------------------------------------------------------- 15 | def correct_box(box, z): 16 | """Get good box limits""" 17 | x0, y0, x1, y1 = box 18 | new_x0 = max(0, min(x0, x1)) 19 | new_x1 = min(2 ** z - 1, max(x0, x1)) 20 | new_y0 = max(0, min(y0, y1)) 21 | new_y1 = min(2 ** z - 1, max(y0, y1)) 22 | 23 | return (new_x0, new_y0, new_x1, new_y1) 24 | 25 | 26 | def get_box_size(box): 27 | """Get box size""" 28 | x0, y0, x1, y1 = box 29 | sx = abs(x1 - x0) + 1 30 | sy = abs(y1 - y0) + 1 31 | return (sx, sy) 32 | 33 | 34 | # ----------------------------------------------------------------------------- 35 | # Functions related to coordinates 36 | # ----------------------------------------------------------------------------- 37 | def deg2num(latitude, longitude, zoom, do_round=True): 38 | """Convert from latitude and longitude to tile numbers. 39 | 40 | If do_round is True, return integers. Otherwise, return floating point 41 | values. 42 | 43 | Source: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Python 44 | 45 | """ 46 | lat_rad = np.radians(latitude) 47 | n = 2.0 ** zoom 48 | if do_round: 49 | f = np.floor 50 | else: 51 | f = lambda x: x 52 | xtile = f((longitude + 180.0) / 360.0 * n) 53 | ytile = f((1.0 - np.log(np.tan(lat_rad) + (1 / np.cos(lat_rad))) / np.pi) / 2.0 * n) 54 | if do_round: 55 | if isinstance(xtile, np.ndarray): 56 | xtile = xtile.astype(np.int32) 57 | else: 58 | xtile = int(xtile) 59 | if isinstance(ytile, np.ndarray): 60 | ytile = ytile.astype(np.int32) 61 | else: 62 | ytile = int(ytile) 63 | return (xtile, ytile) 64 | 65 | 66 | def get_tile_box(box_latlon, z): 67 | """Convert a box in geographical coordinates to a box in 68 | tile coordinates (integers), at a given zoom level. 69 | 70 | box_latlon is lat0, lon0, lat1, lon1. 71 | 72 | """ 73 | lat0, lon0, lat1, lon1 = box_latlon 74 | x0, y0 = deg2num(lat0, lon0, z) 75 | x1, y1 = deg2num(lat1, lon1, z) 76 | return (x0, y0, x1, y1) 77 | 78 | 79 | def _box(*args): 80 | """Return a tuple (lat0, lon0, lat1, lon1) from a coordinate box that 81 | can be specified in multiple ways: 82 | 83 | A. box((lat0, lon0)) # nargs = 1 84 | B. box((lat0, lon0, lat1, lon1)) # nargs = 1 85 | C. box(lat0, lon0) # nargs = 2 86 | D. box((lat0, lon0), (lat1, lon1)) # nargs = 2 87 | E. box(lat0, lon0, lat1, lon1) # nargs = 4 88 | 89 | """ 90 | nargs = len(args) 91 | assert nargs in (1, 2, 4) 92 | pos1 = None 93 | 94 | # Case A. 95 | if nargs == 1: 96 | assert hasattr(args[0], "__len__") 97 | pos = args[0] 98 | assert len(pos) in (2, 4) 99 | if len(pos) == 2: 100 | pos0 = pos 101 | elif len(pos) == 4: 102 | pos0 = pos[:2] 103 | pos1 = pos[2:] 104 | 105 | elif nargs == 2: 106 | # Case C. 107 | if not hasattr(args[0], "__len__"): 108 | pos0 = args[0], args[1] 109 | # Case D. 110 | else: 111 | pos0, pos1 = args[0], args[1] 112 | 113 | # Case E. 114 | elif nargs == 4: 115 | pos0 = args[0], args[1] 116 | pos1 = args[2], args[3] 117 | 118 | if pos1 is None: 119 | pos1 = pos0 120 | 121 | return (pos0[0], pos0[1], pos1[0], pos1[1]) 122 | 123 | 124 | def extend_box(box_latlon, margin=0.1): 125 | """Extend a box in geographical coordinates with a relative margin.""" 126 | (lat0, lon0, lat1, lon1) = box_latlon 127 | lat0, lat1 = min(lat0, lat1), max(lat0, lat1) 128 | lon0, lon1 = min(lon0, lon1), max(lon0, lon1) 129 | dlat = max((lat1 - lat0) * margin, 0.0005) 130 | dlon = max((lon1 - lon0) * margin, 0.0005 / np.cos(np.radians(lat0))) 131 | return ( 132 | max(lat0 - dlat, -80), 133 | max(lon0 - dlon, -180), 134 | min(lat1 + dlat, 80), 135 | min(lon1 + dlon, 180), 136 | ) 137 | 138 | 139 | # ----------------------------------------------------------------------------- 140 | # Main Map class 141 | # ----------------------------------------------------------------------------- 142 | class Map(object): 143 | def __init__(self, *args, **kwargs): 144 | """Create and fetch the map with a given box in geographical 145 | coordinates. 146 | 147 | """ 148 | z = kwargs.get("z", 18) 149 | margin = kwargs.get("margin", 0.05) 150 | 151 | self.tilesize = kwargs.get("tilesize", 256) 152 | self.maxtiles = kwargs.get("maxtiles", 16) 153 | self.verbose = kwargs.get("verbose", False) 154 | 155 | box = _box(*args) 156 | if margin is not None: 157 | box = extend_box(box, margin) 158 | self.box = box 159 | 160 | self.z = self.get_allowed_zoom(z) 161 | if z > self.z: 162 | if self.verbose: 163 | logging.info( 164 | "Lowered zoom level to keep map size reasonable. " 165 | "(z = %d)" % self.z 166 | ) 167 | else: 168 | self.z = z 169 | 170 | def get_allowed_zoom(self, z=18): 171 | box_tile = get_tile_box(self.box, z) 172 | box = correct_box(box_tile, z) 173 | sx, sy = get_box_size(box) 174 | if sx * sy >= self.maxtiles: 175 | z = self.get_allowed_zoom(z - 1) 176 | return z 177 | -------------------------------------------------------------------------------- /library/src/greppo/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/library/src/greppo/static/favicon.png -------------------------------------------------------------------------------- /library/src/greppo/static/img/logo.824287d2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /library/src/greppo/static/img/marker.d242732c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 9 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /library/src/greppo/static/img/spritesheet.fd5728f2.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | 30 | 31 | 32 | 33 | 35 | 55 | 58 | 61 | 66 | 71 | 76 | 77 | 82 | 87 | 92 | 97 | 100 | 105 | 110 | 116 | 117 | 120 | 125 | 130 | 131 | 132 | 136 | 143 | 150 | 151 | 156 | 157 | -------------------------------------------------------------------------------- /library/src/greppo/static/index.html: -------------------------------------------------------------------------------- 1 | greppo
-------------------------------------------------------------------------------- /library/src/greppo/user_script_utils.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import io 3 | import json 4 | import logging 5 | import pathlib 6 | import secrets 7 | import sys 8 | from _ast import Assign 9 | from _ast import Attribute 10 | from _ast import Call 11 | from _ast import keyword 12 | from _ast import Load 13 | from _ast import Name 14 | from _ast import Store 15 | from ast import Str 16 | from contextlib import redirect_stdout 17 | from typing import Any, Dict 18 | 19 | from greppo import GreppoAppProxy 20 | from .input_types import GreppoInputsNames, GreppoChartNames 21 | 22 | logger = logging.getLogger('user_script_utils') 23 | 24 | 25 | class RenameGreppoAppTransformer(ast.NodeTransformer): 26 | def __init__(self, hash_prefix): 27 | super().__init__() 28 | 29 | self.hash_prefix = hash_prefix 30 | 31 | def visit_Name(self, node): 32 | if node.id == "app": 33 | node.id = self.hash_prefix + "_app" 34 | 35 | return node 36 | 37 | 38 | def append_send_data_method(code): 39 | code.body.append( 40 | Assign( 41 | targets=[Name(ctx=Store(), id="gpo_payload")], 42 | type_comment=None, 43 | value=Call( 44 | args=[], 45 | func=Attribute( 46 | attr=GreppoAppProxy.gpo_prepare_data.__name__, 47 | ctx=Load(), 48 | value=Name(ctx=Load(), id="app"), 49 | ), 50 | keywords=[], 51 | ), 52 | ) 53 | ) 54 | 55 | return code 56 | 57 | # TODO Cleanup of raster 58 | def append_raster_reference(code): 59 | code.body.append( 60 | Assign( 61 | targets=[Name(ctx=Store(), id="gpo_raster_reference_payload")], 62 | type_comment=None, 63 | value=Call( 64 | args=[], 65 | func=Attribute( 66 | attr=GreppoAppProxy.gpo_reference_data.__name__, 67 | ctx=Load(), 68 | value=Name(ctx=Load(), id="app"), 69 | ), 70 | keywords=[], 71 | ), 72 | ) 73 | ) 74 | 75 | return code 76 | 77 | 78 | class AddInputUpdatesToGpoVariableAndGetValueTransformer(ast.NodeTransformer): 79 | def __init__(self, input_updates: Dict[str, Any], hex_token_generator): 80 | super().__init__() 81 | 82 | self.input_updates = input_updates 83 | 84 | self.hex_token_generator = hex_token_generator 85 | 86 | self.greppo_input_calls = [] 87 | 88 | def visit_Call(self, node): 89 | if not hasattr(node.func, "attr"): 90 | return node 91 | 92 | if node.func.attr not in GreppoInputsNames: 93 | return node 94 | 95 | # ==== Find name value for gpo variable and set name prefix 96 | for node_kwargs in node.keywords: 97 | if node_kwargs.arg == "name": 98 | string_container = node_kwargs.value ## 3.6 uses an object called Str that has `s` instead of `value`. 99 | if type(string_container) == Str: 100 | name = string_container.s 101 | else: 102 | name = string_container.value 103 | 104 | input_name = self.hex_token_generator(nbytes=4) + "_" + name 105 | 106 | if type(string_container) == Str: 107 | node_kwargs.value.s = input_name 108 | else: 109 | node_kwargs.value.value = input_name 110 | 111 | break 112 | 113 | # ==== Add input_updates to the ctr. 114 | input_updates_kwarg = keyword( 115 | arg='input_updates', 116 | value=ast.parse(json.dumps(self.input_updates)).body[0].value 117 | ) 118 | node.keywords.append(input_updates_kwarg) 119 | 120 | # ==== Create new Call node for get_value 121 | z = Call(args=[], 122 | func=Attribute( 123 | attr='get_value', 124 | ctx=Load(), 125 | value=node 126 | ), 127 | keywords=[]) 128 | 129 | return z 130 | 131 | 132 | class AddHexPrefixForCharts(ast.NodeTransformer): 133 | def __init__(self, input_updates, hex_token_generator): 134 | super().__init__() 135 | 136 | self.input_updates = input_updates 137 | 138 | self.hex_token_generator = hex_token_generator 139 | 140 | def visit_Call(self, node): 141 | if not hasattr(node.func, "attr"): 142 | return node 143 | 144 | if node.func.attr not in GreppoChartNames: 145 | return node 146 | 147 | # ==== Find all names for gpo_inputs and set hex id 148 | for node_kwargs in node.keywords: 149 | if node_kwargs.arg == "name": 150 | string_container = node_kwargs.value ## 3.6 uses an object called Str that has `s` instead of `value`. 151 | if type(string_container) == Str: 152 | input_name = string_container.s 153 | else: 154 | input_name = string_container.value 155 | 156 | input_name = self.hex_token_generator(nbytes=4) + "_" + input_name 157 | 158 | if type(string_container) == Str: 159 | node_kwargs.value.s = input_name 160 | else: 161 | node_kwargs.value.value = input_name 162 | 163 | break 164 | 165 | input_updates_ast = ast.parse(str(self.input_updates)).body[0] 166 | if not hasattr(input_updates_ast, 'value'): 167 | raise Exception("Cannot parse input_updates: {}", self.input_updates) 168 | 169 | input_updates_keyword = keyword( 170 | arg="input_updates", value=input_updates_ast.value 171 | ) 172 | 173 | # ==== Add input updates to node 174 | updated = False 175 | for pos, k in enumerate(node.keywords): 176 | if k.arg == "input_updates": 177 | node.keywords[pos] = input_updates_keyword 178 | updated = True 179 | break 180 | if not updated: 181 | node.keywords.append(input_updates_keyword) 182 | 183 | return node 184 | 185 | 186 | def run_script(script_name, input_updates, hex_token_generator): 187 | script_dir = str(pathlib.Path(script_name).parent) 188 | 189 | with open(script_name) as f: 190 | lines = f.read() 191 | user_code = ast.parse(lines, script_name) 192 | 193 | add_input_updates_to_gpo_variable_and_get_value_t = AddInputUpdatesToGpoVariableAndGetValueTransformer( 194 | input_updates=input_updates, hex_token_generator=hex_token_generator 195 | ) 196 | add_input_updates_to_gpo_variable_and_get_value_t.visit(user_code) 197 | ast.fix_missing_locations( 198 | user_code 199 | ) 200 | 201 | transformer = AddHexPrefixForCharts( 202 | input_updates=input_updates, hex_token_generator=hex_token_generator 203 | ) 204 | transformer.visit(user_code) 205 | ast.fix_missing_locations( 206 | user_code 207 | ) 208 | 209 | user_code = append_send_data_method(user_code) 210 | # user_code = append_raster_reference(user_code) 211 | 212 | # Transform gpo for locals() injection 213 | hash_prefix = hex_token_generator(nbytes=4) 214 | gpo_transformer = RenameGreppoAppTransformer(hash_prefix=hash_prefix) 215 | gpo_transformer.visit(user_code) 216 | 217 | ast.fix_missing_locations(user_code) 218 | 219 | gpo_app = GreppoAppProxy() 220 | 221 | locals_copy = locals().copy() 222 | locals_copy['app'] = gpo_app 223 | locals_copy[hash_prefix + "_app"] = gpo_app 224 | 225 | #logger.debug('\n\n------ Code Transform ------\n') 226 | #logger.debug(ast.unparse(user_code)) 227 | #logger.debug('\n----------------------------\n\n') 228 | 229 | exec(compile(user_code, script_name, "exec"), locals_copy, locals_copy) 230 | 231 | raster_reference_payload = locals_copy.get("gpo_raster_reference_payload", None) 232 | 233 | return locals_copy["gpo_payload"], raster_reference_payload 234 | 235 | 236 | async def script_task( 237 | script_name: str, 238 | input_updates: Dict[str, Any], 239 | hex_token_generator=secrets.token_hex, 240 | ): 241 | """ 242 | async task that runs a user_script in a Greppo context (`gpo_send_data`) and generates payload for front-end 243 | consumption. 244 | """ 245 | # logger.setLevel(logging.DEBUG) 246 | 247 | logging.info("Loading Greppo App at: " + script_name) 248 | 249 | with redirect_stdout(io.StringIO()) as loop_out: 250 | payload = run_script( 251 | script_name=script_name, 252 | input_updates=input_updates, 253 | hex_token_generator=hex_token_generator, 254 | ) 255 | 256 | logger.debug("-------------") 257 | logger.debug("stdout from process") 258 | logger.debug("===") 259 | logger.debug(loop_out.getvalue()) 260 | logger.debug("===") 261 | 262 | return payload 263 | -------------------------------------------------------------------------------- /library/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/library/tests/__init__.py -------------------------------------------------------------------------------- /library/tests/app.py: -------------------------------------------------------------------------------- 1 | import ee 2 | import os 3 | import geopandas as gpd 4 | import numpy as np 5 | from greppo import app 6 | 7 | app.base_layer( 8 | name="CartoDB Light", 9 | # visible=True, 10 | url="https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}@2x.png", 11 | subdomains=None, 12 | attribution='© OpenStreetMap contributors', 13 | ) 14 | 15 | 16 | app.tile_layer( 17 | name="Open Street Map", 18 | url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", 19 | visible=False, 20 | description="A OSM tile layer", 21 | ) 22 | 23 | data_gdf = gpd.read_file("tests/data/us-states.geojson") 24 | app.overlay_layer( 25 | data=data_gdf, 26 | name="USA States", 27 | description="Boundaries of States, USA", 28 | style={ 29 | "color": "#eeeeee", 30 | "choropleth": { 31 | "key_on": "density", 32 | "bins": [1, 10, 20, 50, 100, 200, 500, 1000], 33 | }, 34 | }, 35 | visible=True, 36 | ) 37 | 38 | text0 = """ 39 | ## EarthEngine API 40 | 41 | * Create service account 42 | * Authenticate/Initialize 43 | * Perform EE operations 44 | * Get the `ee_object` 45 | * Pass the `ee_object` to the `app.ee_layer()` 46 | """ 47 | app.display(value=text0, name="text0") 48 | 49 | email = os.environ['EE_EMAIL'] 50 | key_file = os.environ['EE_KEY_FILE'] 51 | credentials = ee.ServiceAccountCredentials(email=email, key_file=key_file) 52 | ee.Initialize(credentials) 53 | 54 | dem = ee.Image("USGS/SRTMGL1_003") 55 | ee_image_object = dem.updateMask(dem.gt(0)) 56 | vis_params = { 57 | "min": 0, 58 | "max": 4000, 59 | "palette": ["006633", "E5FFCC", "662A00", "D8D8D8", "F5F5F5"], 60 | } 61 | name = "DEM" 62 | print(vis_params) 63 | app.ee_layer( 64 | ee_object=ee_image_object, vis_params=vis_params, name=name, description="EE layer" 65 | ) 66 | 67 | app.wms_tile_layer( 68 | url="http://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi", 69 | name="Weather Data", 70 | format="image/png", 71 | layers="nexrad-n0r-900913", 72 | description="Weather WMS tile layer", 73 | ) 74 | 75 | 76 | text1 = """ 77 | ## Input APIs 78 | 79 | - Number input: `app.number(value=10, name="Number input 1")` 80 | - Text input: `app.text(value="here is a text", name="Text input 1")` 81 | - Dropdown select: `app.select(name="First selector", options=["a", "b", "c"], default="a")` 82 | - Multi-select: `app.multiselect(name="Second selector", options=["Asia", "Africa", "Europe"], default=["Asia"])` 83 | """ 84 | app.display(value=text1, name="text1") 85 | 86 | number_1 = app.number(value=10, name="Number input 1") 87 | text_1 = app.text(value="here is a text", name="Text input 1") 88 | select1 = app.select(name="First selector", options=["a", "b", "c"], default="a") 89 | multiselect1 = app.multiselect( 90 | name="Second selector", options=["Asia", "Africa", "Europe"], default=["Asia"] 91 | ) 92 | 93 | 94 | text2 = """ 95 | ## The draw feature 96 | 97 | ```python 98 | draw_features = gpd.read_file("data/features.geojson") 99 | draw_feature_input = app.draw_feature( 100 | name="Draw random features", features=draw_features, geometry=["Point", "LineString", "Polygon"] 101 | ) 102 | ``` 103 | """ 104 | app.display(value=text2, name="text2") 105 | 106 | line_feature = gpd.read_file("tests/data/line.geojson") 107 | draw_feature_line = app.draw_feature( 108 | name="Draw line features", features=line_feature, geometry=["LineString"] 109 | ) 110 | 111 | point_feature = gpd.read_file("tests/data/point.geojson") 112 | draw_feature_point = app.draw_feature( 113 | name="Draw point features", features=point_feature, geometry=["Point", "LineString"] 114 | ) 115 | 116 | polygon_feature = gpd.read_file("tests/data/polygon.geojson") 117 | draw_feature_polygon = app.draw_feature( 118 | name="Draw polygon features", features=polygon_feature, geometry=["Polygon"] 119 | ) 120 | 121 | text3 = """ 122 | ## Some charts to display 123 | 124 | * Line chart 125 | * Bar chart 126 | """ 127 | app.display(value=text3, name="text3") 128 | 129 | y_2d = [] 130 | for j in range(2): 131 | y = [] 132 | for i in range(10, 0, -1): 133 | y.append(np.random.randint(0, 100)) 134 | y_2d.append(y) 135 | 136 | app.line_chart( 137 | name="some-name", 138 | description="some_chart", 139 | x=[i for i in range(10)], 140 | y=y, 141 | color="rgb(255, 99, 132)", 142 | ) 143 | 144 | app.line_chart( 145 | name="some-name", 146 | description="some_chart", 147 | x=[i for i in range(10)], 148 | y=y_2d, 149 | label=['line 1', "line 2"], 150 | color=["rgb(255, 99, 132)", "rgb(155, 99, 132)"], 151 | ) 152 | 153 | y_2d = [] 154 | for j in range(2): 155 | y = [] 156 | for i in range(10, 0, -1): 157 | y.append(np.random.randint(0, 100)) 158 | y_2d.append(y) 159 | 160 | 161 | app.bar_chart( 162 | name="some-name", 163 | description="some_chart", 164 | x=[i for i in range(10)], 165 | y=y, 166 | color="rgb(200, 50, 150)", 167 | ) 168 | 169 | app.bar_chart( 170 | name="some-name", 171 | description="some_chart", 172 | x=[i for i in range(10)], 173 | y=y_2d, 174 | label=['bar 1', "bar 2"], 175 | color=["rgb(200, 50, 150)", "rgb(100, 50, 150)"], 176 | ) 177 | -------------------------------------------------------------------------------- /library/tests/app_gee.py: -------------------------------------------------------------------------------- 1 | import ee 2 | from greppo import app 3 | 4 | """ 5 | Authenticate earthengine-api using a Service-Account-Credential 6 | 7 | ServiceAccountCredentials(email, key_file=None, key_data=None): 8 | 9 | email: The email address of the account for which to configure credentials. 10 | Ignored if key_file or key_data represents a JSON service account key. 11 | email = my-service-account@...gserviceaccount.com 12 | email = greppo-ee-test@greppo-earth-engine.iam.gserviceaccount.com 13 | 14 | key_file: The path to a file containing the private key associated with 15 | the service account. Both JSON and PEM files are supported. 16 | 17 | key_data: Raw key data to use, if key_file is not specified. 18 | """ 19 | 20 | email = os.environ['EE_EMAIL'] 21 | key_file = os.environ['EE_KEY_FILE'] 22 | credentials = ee.ServiceAccountCredentials(email=email, key_file=key_file) 23 | ee.Initialize(credentials) 24 | 25 | ##--------------------------------------------------------------------------------------## 26 | # Basic DEM of the world 27 | ##--------------------------------------------------------------------------------------## 28 | 29 | # dem = ee.Image('USGS/SRTMGL1_003') 30 | # ee_image_object = dem.updateMask(dem.gt(0)) 31 | # vis_params = { 32 | # 'min': 0, 33 | # 'max': 4000, 34 | # 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']} 35 | # name = 'DEM' 36 | # app.ee_layer(ee_object=ee_image_object, 37 | # vis_params=vis_params, name=name) 38 | 39 | 40 | ##--------------------------------------------------------------------------------------## 41 | # Compute the trend of night-time lights. 42 | ##--------------------------------------------------------------------------------------## 43 | 44 | # Adds a band containing image date as years since 1991. 45 | def create_time_band(img): 46 | year = ee.Date(img.get("system:time_start")).get("year").subtract(1991) 47 | return ee.Image(year).byte().addBands(img) 48 | 49 | 50 | # Map the time band creation helper over the night-time lights collection. 51 | # https://developers.google.com/earth-engine/datasets/catalog/NOAA_DMSP-OLS_NIGHTTIME_LIGHTS 52 | collection = ( 53 | ee.ImageCollection("NOAA/DMSP-OLS/NIGHTTIME_LIGHTS") 54 | .select("stable_lights") 55 | .map(create_time_band) 56 | ) 57 | 58 | # Compute a linear fit over the series of values at each pixel, visualizing 59 | # the y-intercept in green, and positive/negative slopes as red/blue. 60 | vis_params = {"min": 0, "max": [0.18, 20, -0.18], "bands": ["scale", "offset", "scale"]} 61 | ee_image_object = collection.reduce(ee.Reducer.linearFit()) 62 | # map_id_dict = ee.Image(ee_image_object).getMapId(vis_params) 63 | # ee_url = map_id_dict['tile_fetcher'].url_format 64 | 65 | app.ee_layer( 66 | ee_object=ee_image_object, vis_params=vis_params, name="Trendy Night Lights" 67 | ) 68 | 69 | ##--------------------------------------------------------------------------------------## 70 | # Working with feature/geometry collection. 71 | ##--------------------------------------------------------------------------------------## 72 | 73 | # dem = ee.Image('USGS/SRTMGL1_003') 74 | # ee_image_object = dem.updateMask(dem.gt(0)) 75 | # vis_params = { 76 | # 'min': 0, 77 | # 'max': 4000, 78 | # 'palette': ['006633', 'E5FFCC', '662A00', 'D8D8D8', 'F5F5F5']} 79 | # name = 'DEM' 80 | # app.ee_layer(ee_object=ee_image_object, 81 | # vis_params=vis_params, name=name) 82 | 83 | # xy = ee.Geometry.Point([86.9250, 27.9881]) 84 | # app.ee_layer(ee_object=xy, vis_params={"color": "red"}, name="Mount Everest", description="The data") 85 | -------------------------------------------------------------------------------- /library/tests/data/features.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "LineString", 9 | "coordinates": [ 10 | [-27.59765625, 57.844750992891], 11 | [-15.468749999999998, 59.57885104663186], 12 | [-4.306640625, 56.70450561416937], 13 | [-12.83203125, 51.01375465718821], 14 | [-27.0703125, 54.41892996865827] 15 | ] 16 | } 17 | }, 18 | { 19 | "type": "Feature", 20 | "properties": {}, 21 | "geometry": { 22 | "type": "LineString", 23 | "coordinates": [ 24 | [-10.1953125, 61.14323525084058], 25 | [-5.80078125, 62.512317938386914], 26 | [4.74609375, 59.88893689676585], 27 | [6.064453125, 56.80087831233043] 28 | ] 29 | } 30 | }, 31 | { 32 | "type": "Feature", 33 | "properties": {}, 34 | "geometry": { 35 | "type": "Polygon", 36 | "coordinates": [ 37 | [ 38 | [-40.078125, 45.1510532655634], 39 | [-38.67187499999999, 40.17887331434696], 40 | [-34.189453125, 39.70718665682654], 41 | [-29.53125, 44.96479793033101], 42 | [-32.6953125, 48.69096039092549], 43 | [-35.77148437499999, 44.653024159812], 44 | [-40.078125, 45.1510532655634] 45 | ] 46 | ] 47 | } 48 | }, 49 | { 50 | "type": "Feature", 51 | "properties": {}, 52 | "geometry": { 53 | "type": "Polygon", 54 | "coordinates": [ 55 | [ 56 | [-10.107421874999998, 44.715513732021336], 57 | [-20.390625, 40.04443758460856], 58 | [-17.402343749999996, 36.66841891894786], 59 | [-13.974609375, 37.3002752813443], 60 | [-10.107421874999998, 44.715513732021336] 61 | ] 62 | ] 63 | } 64 | }, 65 | { 66 | "type": "Feature", 67 | "properties": {}, 68 | "geometry": { 69 | "type": "Point", 70 | "coordinates": [10.546875, 45.9511496866914] 71 | } 72 | }, 73 | { 74 | "type": "Feature", 75 | "properties": {}, 76 | "geometry": { 77 | "type": "Point", 78 | "coordinates": [7.119140625, 40.713955826286046] 79 | } 80 | }, 81 | { 82 | "type": "Feature", 83 | "properties": {}, 84 | "geometry": { 85 | "type": "Point", 86 | "coordinates": [-6.416015625, 48.574789910928864] 87 | } 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /library/tests/data/line.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "LineString", 9 | "coordinates": [ 10 | [-1.7578125, 73.22669969306126], 11 | [-33.046875, 62.75472592723178], 12 | [-9.4921875, 47.754097979680026], 13 | [20.7421875, 53.74871079689897], 14 | [31.289062500000004, 71.85622888185527], 15 | [40.78125, 72.91963546581484] 16 | ] 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /library/tests/data/point.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Point", 9 | "coordinates": [-13.798828125, 50.401515322782366] 10 | } 11 | }, 12 | { 13 | "type": "Feature", 14 | "properties": {}, 15 | "geometry": { 16 | "type": "Point", 17 | "coordinates": [-5.2734375, 48.574789910928864] 18 | } 19 | }, 20 | { 21 | "type": "Feature", 22 | "properties": {}, 23 | "geometry": { 24 | "type": "Point", 25 | "coordinates": [-10.8984375, 46.558860303117164] 26 | } 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /library/tests/data/polygon.geojson: -------------------------------------------------------------------------------- 1 | { 2 | "type": "FeatureCollection", 3 | "features": [ 4 | { 5 | "type": "Feature", 6 | "properties": {}, 7 | "geometry": { 8 | "type": "Polygon", 9 | "coordinates": [ 10 | [ 11 | [-15.1171875, 59.88893689676585], 12 | [-15.468749999999998, 46.800059446787316], 13 | [2.109375, 43.32517767999296], 14 | [21.796875, 54.97761367069628], 15 | [13.7109375, 64.01449619484472], 16 | [-15.1171875, 59.88893689676585] 17 | ] 18 | ] 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /library/tests/data/sfo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/library/tests/data/sfo.jpg -------------------------------------------------------------------------------- /library/tests/test.py: -------------------------------------------------------------------------------- 1 | from greppo import app 2 | 3 | app.map(max_zoom= 18, center=[25, 25], zoom=10) 4 | 5 | app.base_layer(provider='CartoDB Positron', visible=True) 6 | 7 | text_1 = app.text(name='Text input', value='Some text...') 8 | 9 | app.display(name='text-1-display', value=f'Value of text input: {text_1}') -------------------------------------------------------------------------------- /library/tests/unit_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greppo-io/greppo/5b3b58bec5d42341398a2730c60785484ad909c1/library/tests/unit_tests/__init__.py -------------------------------------------------------------------------------- /library/tests/unit_tests/test_greppo_app.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestGreppoApp(unittest.TestCase): 5 | pass 6 | -------------------------------------------------------------------------------- /library/tests/unit_tests/test_user_script_utils.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import asyncio 3 | import pathlib 4 | import unittest 5 | 6 | from greppo.input_types import GreppoInputsNames 7 | from meta.asttools import cmp_ast 8 | from meta.asttools import print_ast 9 | from greppo.user_script_utils import append_send_data_method, ReplaceGpoVariableWithValueTransformer 10 | from greppo.user_script_utils import RenameGreppoAppTransformer 11 | from greppo.user_script_utils import script_task 12 | 13 | 14 | def hex_token_generator(nbytes): 15 | if nbytes == 4: 16 | return "somehex1" 17 | return 0 18 | 19 | 20 | class TestUserScriptUtils(unittest.TestCase): 21 | def test_transformer_for_number_input(self): 22 | transformer = ReplaceGpoVariableWithValueTransformer( 23 | input_updates={}, hex_token_generator=hex_token_generator, 24 | ) 25 | user_code = ast.parse( 26 | 'number_1 = gpo.number(value=10, name="Number input 1")', "" 27 | ) 28 | 29 | transformer.visit(user_code) 30 | 31 | expected_user_code = ast.parse( 32 | "number_1 = 10", 33 | "", 34 | ) 35 | 36 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 37 | 38 | def test_transformer_for_number_input_with_input_update(self): 39 | transformer = ReplaceGpoVariableWithValueTransformer( 40 | input_updates={"Number input 1": 11}, hex_token_generator=hex_token_generator, 41 | ) 42 | user_code = ast.parse( 43 | 'number_1 = gpo.number(value=10, name="Number input 1")', "" 44 | ) 45 | 46 | transformer.visit(user_code) 47 | 48 | expected_user_code = ast.parse( 49 | "number_1 = 11", 50 | "", 51 | ) 52 | 53 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 54 | 55 | def test_transformer_for_multiselect_no_input_update(self): 56 | transformer = ReplaceGpoVariableWithValueTransformer( 57 | input_updates={}, hex_token_generator=hex_token_generator, 58 | ) 59 | user_code = ast.parse( 60 | 'multiselect1=gpo.multiselect(name="Second selector", options=[1, "True", "France"], default=["a"])' 61 | ) 62 | 63 | transformer.visit(user_code) 64 | 65 | expected_user_code = ast.parse( 66 | 'multiselect1 = ["a"]', 67 | "", 68 | ) 69 | 70 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 71 | 72 | def test_transformer_for_multiselect_with_input_update(self): 73 | transformer = ReplaceGpoVariableWithValueTransformer( 74 | input_updates={"Second selector": ["b"]}, hex_token_generator=hex_token_generator, 75 | ) 76 | user_code = ast.parse( 77 | 'multiselect1=gpo.multiselect(name="Second selector", options=[1, "True", "France"], default=["a"])' 78 | ) 79 | 80 | transformer.visit(user_code) 81 | 82 | expected_user_code = ast.parse( 83 | 'multiselect1 = ["b"]', 84 | "", 85 | ) 86 | 87 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 88 | 89 | def test_transformer_for_multiselect_with_input_update_empty_value(self): 90 | # TODO this should be fixed, we cannot have an empty value for multiselect input. 91 | transformer = ReplaceGpoVariableWithValueTransformer( 92 | input_updates={"Second selector": ""}, hex_token_generator=hex_token_generator, 93 | ) 94 | user_code = ast.parse( 95 | 'multiselect1=gpo.multiselect(name="Second selector", options=[1, "True", "France"], default=["a"])' 96 | ) 97 | 98 | transformer.visit(user_code) 99 | 100 | expected_user_code = ast.parse( 101 | 'multiselect1 = ""', 102 | "", 103 | ) 104 | 105 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 106 | 107 | def test_transformer_for_all_inputs_no_arg_check(self): 108 | transformer = ReplaceGpoVariableWithValueTransformer( 109 | input_updates={}, hex_token_generator=hex_token_generator, 110 | ) 111 | 112 | input_names = GreppoInputsNames 113 | for input_name in input_names: 114 | with self.subTest(): 115 | user_code = ast.parse("select1 = gpo.{}()".format(input_name), "") 116 | 117 | transformer.visit(user_code) 118 | 119 | expected_user_code = ast.parse( 120 | "select1 = null", 121 | "", 122 | ) 123 | 124 | print_ast(user_code) 125 | print_ast(expected_user_code) 126 | 127 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 128 | 129 | def test_transformer_for_gpo_name(self): 130 | hash_prefix = hex_token_generator(nbytes=4) 131 | transformer = RenameGreppoAppTransformer(hash_prefix=hash_prefix) 132 | 133 | user_code = ast.parse( 134 | "select1 = app.select(name='somehex1_First selector', " 135 | "options=['a', 'b', 'c'], default='a', input_updates={})", 136 | "", 137 | ) 138 | 139 | transformer.visit(user_code) 140 | 141 | expected_user_code = ast.parse( 142 | "select1 = somehex1_app.select(name='somehex1_First selector', " 143 | "options=['a', 'b', 'c'], default='a', input_updates={})", 144 | "", 145 | ) 146 | 147 | self.assertTrue(cmp_ast(user_code, expected_user_code)) 148 | 149 | def test_append_send_data_method(self): 150 | user_code = append_send_data_method(ast.parse('')) 151 | ast.fix_missing_locations(user_code) 152 | 153 | expected_code = ast.parse('gpo_payload = app.gpo_prepare_data()') 154 | 155 | self.assertTrue(cmp_ast(user_code, expected_code)) 156 | 157 | 158 | class TestRunUserScript(unittest.TestCase): 159 | def test_user_script_exception(self): 160 | dir_path = pathlib.Path(__file__).parent.resolve() 161 | user_script_path = dir_path.joinpath("user_script_1.py") 162 | 163 | with self.assertRaises(SyntaxError) as context: 164 | asyncio.run( 165 | script_task(script_name=str(user_script_path), input_updates={}) 166 | ) 167 | 168 | self.assertEqual("unexpected EOF while parsing", context.exception.msg) 169 | self.assertEqual(3, context.exception.lineno) 170 | 171 | def test_user_script_works(self): 172 | dir_path = pathlib.Path(__file__).parent.resolve() 173 | user_script_path = dir_path.joinpath("user_script_4.py") 174 | 175 | payload = asyncio.run( 176 | script_task( 177 | script_name=str(user_script_path), 178 | input_updates={}, 179 | hex_token_generator=hex_token_generator, 180 | ) 181 | ) 182 | 183 | expected_payload = { 184 | "base_layer_info": [], 185 | "overlay_layer_data": [], 186 | "component_info": [ 187 | { 188 | "id": "somehex1", 189 | "name": "Filter building", 190 | "type": "Multiselect", 191 | "options": ["apartments", "retail", "house"], 192 | "value": ["house"], 193 | }, 194 | ], 195 | "raster_layer_data": [], 196 | }, None 197 | 198 | print(payload) 199 | 200 | self.assertEqual(payload, expected_payload) 201 | 202 | def test_user_script_with_base_and_overlay_layer(self): 203 | # TODO 204 | pass 205 | 206 | 207 | if __name__ == "__main__": 208 | unittest.main() 209 | -------------------------------------------------------------------------------- /library/tests/unit_tests/user_script_1.py: -------------------------------------------------------------------------------- 1 | # This is a user script that does not compile 2 | 3 | print( 4 | -------------------------------------------------------------------------------- /library/tests/unit_tests/user_script_2.py: -------------------------------------------------------------------------------- 1 | from greppo import app 2 | 3 | x = app.number(name="x", value=3) 4 | number_1 = app.number(value=1, name="Number input 1") 5 | 6 | import numpy as np 7 | 8 | print("number 1 from the script:", number_1) 9 | 10 | print(np.ones(10) * number_1) 11 | -------------------------------------------------------------------------------- /library/tests/unit_tests/user_script_3.py: -------------------------------------------------------------------------------- 1 | from greppo import app 2 | 3 | 4 | multiselect1 = app.multiselect( 5 | name="Second selector", options=[1, "True", "France"], default=["a"] 6 | ) 7 | -------------------------------------------------------------------------------- /library/tests/unit_tests/user_script_4.py: -------------------------------------------------------------------------------- 1 | import geopandas as gpd 2 | from greppo import app 3 | 4 | filter_select = app.multiselect(name="Filter building", options=["apartments", "retail", "house"], default=["house"]) 5 | -------------------------------------------------------------------------------- /library/tests/v30.py: -------------------------------------------------------------------------------- 1 | from greppo import app 2 | import numpy as np 3 | import rasterio 4 | from rasterio.warp import calculate_default_transform 5 | from rasterio.warp import reproject 6 | 7 | app.base_layer( 8 | name="CartoDB Light", 9 | visible=True, 10 | url="https://cartodb-basemaps-a.global.ssl.fastly.net/light_all/{z}/{x}/{y}@2x.png", 11 | subdomains=None, 12 | attribution='© OpenStreetMap contributors', 13 | ) 14 | 15 | # app.image_layer( 16 | # image='tests/data/sfo.jpg', 17 | # bounds=[ 18 | # [14.760840184106792, 77.97900023926854], 19 | # [14.763995704693206, 77.98389492733145], 20 | # ], 21 | # name='This name', 22 | # description='Some description', 23 | # visible=True, 24 | # ) 25 | 26 | # file_path = 'tests/data/rvrnbrt.TIF' 27 | # src_dataset = rasterio.open(file_path) 28 | # dst_crs = "EPSG:4326" 29 | 30 | # transform, width, height = calculate_default_transform( 31 | # src_dataset.crs, 32 | # dst_crs, 33 | # src_dataset.width, 34 | # src_dataset.height, 35 | # *src_dataset.bounds 36 | # ) 37 | 38 | # dst_bands = [] 39 | # for band_n_1 in range(src_dataset.count): 40 | # src_band = rasterio.band(src_dataset, band_n_1 + 1) 41 | # dst_band = reproject(src_band, dst_crs=dst_crs) 42 | # dst_bands.append(dst_band) 43 | 44 | # (bounds_bottom, bounds_right) = transform * (0, 0) 45 | # (bounds_top, bounds_left) = transform * (width, height) 46 | # bounds = [[bounds_left, bounds_bottom], [bounds_right, bounds_top]] 47 | 48 | # app.raster_layer( 49 | # data=dst_bands[0][0][0], 50 | # bounds=bounds, 51 | # name='This name', 52 | # description='Some description', 53 | # visible=True, 54 | # ) 55 | 56 | band = np.zeros((61, 61)) 57 | band[0, :] = 1.0 58 | band[60, :] = 1.0 59 | band[:, 0] = 1.0 60 | band[:, 60] = 1.0 61 | 62 | app.raster_layer( 63 | data=band, 64 | bounds=[[0, -60], [60, 60]], 65 | colormap=lambda x: (1, 0, 0, x), 66 | name='Rectangle', 67 | description='A large red rectangle on a map.', 68 | visible=True, 69 | ) 70 | 71 | --------------------------------------------------------------------------------