├── .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 | [](https://discord.gg/RNJBjgh8gz) 
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 |
4 |
--------------------------------------------------------------------------------
/assets/docs.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/globe.svg:
--------------------------------------------------------------------------------
1 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/marker.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/frontend/src/assets/tailwind.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
6 |
--------------------------------------------------------------------------------
/frontend/src/components/BaseContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/frontend/src/components/CenterContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
14 |
15 |
16 |
17 |
25 |
Evaluating !
26 |
27 |
28 |
29 |
30 |
31 |
35 |
36 |
47 |
52 |
81 |
82 |
83 |
84 |
105 |
106 |
107 |
108 |
109 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/frontend/src/components/LeftContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
77 |
78 |
79 |
80 |
123 |
124 |
134 |
--------------------------------------------------------------------------------
/frontend/src/components/RightContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
19 |
20 |
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 |
2 |
3 |
{{ data.name }}
4 |
{{ data.description }}
5 |
11 |
17 |
23 |
29 |
35 |
41 |
47 |
53 |
59 |
60 |
61 |
62 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/frontend/src/components/chart-components/ChartTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
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 |
2 |
3 |
13 |
14 |
15 |
16 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/frontend/src/components/display-components/MarkdownDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/frontend/src/components/functional-components/AppInfoModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
17 |
18 |
21 |
{{ getAppInfo.title }}
22 |
A geospatial app
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
56 |
57 |
67 |
--------------------------------------------------------------------------------
/frontend/src/components/functional-components/ErrorModal.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
13 |
{{ getErrorInfo.title }}
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
37 |
38 |
48 |
--------------------------------------------------------------------------------
/frontend/src/components/input-components/InputControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
13 |
19 |
25 |
26 |
27 |
28 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/frontend/src/components/input-components/InputTest.vue:
--------------------------------------------------------------------------------
1 |
2 |
55 |
56 |
57 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/frontend/src/components/input-components/MultiSelectInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/frontend/src/components/input-components/NumberInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
37 |
38 |
49 |
--------------------------------------------------------------------------------
/frontend/src/components/input-components/SelectInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/frontend/src/components/input-components/TextInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
37 |
38 |
41 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/BaseLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
14 |
26 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/BaseLayerControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/DrawFeatureControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
Draw feature control
11 |
15 |
20 |
21 |
22 |
26 |
40 |
Layer: {{ modelValue }}
41 |
44 |
45 | No features drawn.
46 |
47 |
52 |
53 | {{ feature.type }}
54 | with id: {{ feature.id }}
55 |
56 |
61 | {{ latlng }}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/ImageLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/LayerControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
Layer control
12 |
16 |
21 |
22 |
23 |
27 |
36 |
40 |
41 |
42 |
43 | Overlay layer control
44 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/MapContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
18 |
19 |
20 |
25 |
26 |
27 |
32 |
33 |
34 |
39 |
40 |
41 |
42 |
48 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
152 |
153 |
177 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/OverlayLayerControl.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
{{ layerData.name }}
10 |
11 |
12 |
25 |
26 |
31 |
32 |
33 |
{{ layerData.description }}
34 |
35 |
36 |
37 |
77 |
78 |
83 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/TileLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/VectorLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/frontend/src/components/map-components/WMSTileLayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
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 |
13 |
--------------------------------------------------------------------------------
/library/src/greppo/static/img/spritesheet.fd5728f2.svg:
--------------------------------------------------------------------------------
1 |
2 |
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 |
--------------------------------------------------------------------------------