├── .dockerignore ├── .github └── workflows │ ├── build-deploy-docs.yml │ ├── ci.yml │ └── docker.yml ├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── .gitignore ├── Makefile ├── NetherlandsRDNewQuad.json ├── custom-grid-lv95.json ├── helloworld.db ├── helloworld.qgs ├── liechtenstein.mbtiles ├── maplibre.html ├── mapserver.conf ├── mbtiles_mvt_fl-style.json ├── mvtbench.gpkg ├── mvtbench.pmtiles ├── mvtbench_t_rex.toml ├── natural-earth-countries-style.json ├── ne.map ├── ne.qgs ├── ne_extracts.gpkg ├── ne_extracts.qgz ├── ol-ogc-tiles.html ├── railway-test.gpkg └── usergrid.html ├── bbox-asset-server ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── endpoints.rs │ ├── lib.rs │ ├── main.rs │ ├── qgis_plugins.rs │ ├── runtime_templates.rs │ ├── service.rs │ ├── static │ └── plugins.xsl │ └── templates │ └── plugins.xml ├── bbox-core ├── Cargo.toml ├── README.md ├── fixtures │ ├── dc.webp │ ├── world.jpg │ └── world.png ├── src │ ├── api.rs │ ├── auth │ │ ├── mod.rs │ │ └── oidc.rs │ ├── cli.rs │ ├── config.rs │ ├── empty │ │ └── .gitkeep │ ├── endpoints.rs │ ├── file_search.rs │ ├── formats.rs │ ├── lib.rs │ ├── logger.rs │ ├── metrics.rs │ ├── ogcapi.rs │ ├── openapi.yaml │ ├── pg_ds.rs │ ├── service.rs │ ├── service_utils.rs │ ├── static_assets.rs │ ├── static_files.rs │ ├── templates.rs │ ├── tile_response.rs │ └── tls.rs ├── static │ └── core │ │ └── favicon.ico └── templates │ ├── base.html │ └── collections.html ├── bbox-feature-server ├── .gitignore ├── Cargo.toml ├── README.md ├── src │ ├── config.rs │ ├── datasource │ │ ├── gpkg.rs │ │ ├── mod.rs │ │ └── postgis.rs │ ├── endpoints.rs │ ├── error.rs │ ├── filter_params.rs │ ├── inventory.rs │ ├── lib.rs │ ├── main.rs │ ├── openapi.yaml │ └── service.rs └── templates │ ├── collection.html │ ├── feature.html │ ├── features.html │ └── queryables.html ├── bbox-frontend ├── Cargo.toml ├── Makefile ├── README.md ├── src │ ├── endpoints.rs │ ├── lib.rs │ └── qwc2_config.rs ├── static │ ├── frontend │ │ └── bbox.css │ ├── maplibre │ │ ├── maplibre-gl.css │ │ └── maplibre-gl.js │ ├── ol │ │ ├── ol.min.css │ │ └── ol.min.js │ ├── proj │ │ └── proj4.min.js │ ├── qwc2 │ │ ├── assets │ │ │ ├── css │ │ │ │ └── qwc2.css │ │ │ ├── forms │ │ │ │ └── form.ui │ │ │ ├── img │ │ │ │ ├── app_icon.png │ │ │ │ ├── app_icon_114.png │ │ │ │ ├── app_icon_144.png │ │ │ │ ├── app_icon_72.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── logo-mobile.svg │ │ │ │ ├── logo.svg │ │ │ │ └── mapthumbs │ │ │ │ │ ├── default.jpg │ │ │ │ │ └── mapnik.jpg │ │ │ └── templates │ │ │ │ └── legendprint.html │ │ ├── config.json │ │ ├── index.html │ │ ├── js │ │ │ └── QWC2App.js │ │ └── translations │ │ │ ├── data.cs-CZ │ │ │ ├── data.de-CH │ │ │ ├── data.de-DE │ │ │ ├── data.en-US │ │ │ ├── data.es-ES │ │ │ ├── data.fr-FR │ │ │ ├── data.it-IT │ │ │ ├── data.pl-PL │ │ │ ├── data.pt-BR │ │ │ ├── data.ro-RO │ │ │ ├── data.ru-RU │ │ │ ├── data.sv-SE │ │ │ └── data.tr-TR │ ├── redoc │ │ └── redoc.standalone.js │ └── swagger │ │ ├── swagger-ui-bundle.js │ │ └── swagger-ui.css ├── templates │ ├── index.html │ ├── redoc.html │ ├── scalar.html │ └── swaggerui.html └── ui │ ├── .gitignore │ ├── Makefile │ ├── package.json │ ├── src │ └── bbox.css │ └── tailwind.config.js ├── bbox-map-server ├── Cargo.toml ├── README.md ├── bench │ ├── .gitignore │ ├── Makefile │ ├── bbox-bench.toml │ ├── httpbench.lua │ ├── paths.txt │ ├── paths_baseline.txt │ └── results │ │ ├── Makefile │ │ ├── http-quick.g │ │ └── http.g ├── mock-fcgi-wms │ ├── Cargo.toml │ ├── README.md │ └── src │ │ └── main.rs ├── qgis │ └── plugins │ │ ├── ClearCapabilities │ │ ├── __init__.py │ │ ├── clear_capabilities.py │ │ └── metadata.txt │ │ └── Instrumentation │ │ ├── Instrumentation.py │ │ ├── __init__.py │ │ └── metadata.txt └── src │ ├── config.rs │ ├── dispatcher │ ├── mod.rs │ ├── rand.rs │ ├── round_robin.rs │ └── wms_optimized.rs │ ├── endpoints.rs │ ├── fcgi_process.rs │ ├── inventory.rs │ ├── lib.rs │ ├── main.rs │ ├── metrics.rs │ ├── openapi.yaml │ ├── service.rs │ ├── wms_capabilities.rs │ └── wms_fcgi_backend.rs ├── bbox-processes-server ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── dagster.rs │ ├── endpoints.rs │ ├── error.rs │ ├── lib.rs │ ├── main.rs │ ├── models.rs │ ├── openapi.yaml │ └── service.rs ├── bbox-routing-server ├── Cargo.toml ├── README.md └── src │ ├── config.rs │ ├── ds.rs │ ├── endpoints.rs │ ├── engine.rs │ ├── error.rs │ ├── lib.rs │ ├── main.rs │ ├── openapi.yaml │ └── service.rs ├── bbox-server ├── Cargo.toml ├── README.md └── src │ └── main.rs ├── bbox-tile-server ├── .gitignore ├── Cargo.toml ├── README.md ├── bbox-mvtbench.toml ├── justfile ├── performance.md └── src │ ├── cli.rs │ ├── config.rs │ ├── config_t_rex.rs │ ├── datasource │ ├── mbtiles.rs │ ├── mod.rs │ ├── mvt.rs │ ├── pmtiles.rs │ ├── postgis.rs │ ├── postgis_queries.rs │ ├── wms_fcgi.rs │ └── wms_http.rs │ ├── endpoints.rs │ ├── filter_params.rs │ ├── lib.rs │ ├── main.rs │ ├── mbtiles_ds.rs │ ├── openapi.yaml │ ├── seed.rs │ ├── service.rs │ └── store │ ├── files.rs │ ├── mbtiles.rs │ ├── mod.rs │ ├── pmtiles.rs │ ├── s3.rs │ └── s3putfiles.rs ├── bbox.toml ├── docker ├── Dockerfile ├── Dockerfile-mapserver ├── Dockerfile-qgis-server └── bbox │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── bbox-full.toml │ ├── create-multiple-postgresql-databases.sh │ ├── docker-compose.yml │ ├── init-windmill-as-superuser.sql │ ├── instrumentation │ ├── grafana │ │ ├── dashboards │ │ │ └── system │ │ │ │ ├── bbox.json │ │ │ │ └── prom.json │ │ ├── grafana.ini │ │ └── provisioning │ │ │ ├── dashboards │ │ │ └── dashboards.yml │ │ │ ├── datasources │ │ │ └── prometheus.yml │ │ │ └── notifiers │ │ │ └── telegram.yml │ └── prometheus.yml │ ├── nginx │ ├── authentik.conf.template │ ├── default.conf.template │ └── windmill.conf.template │ └── template.env ├── justfile ├── plugins └── instantprint.zip ├── templates ├── maplibre-asset-style.html └── maplibre.html └── website ├── .gitignore ├── assets └── css │ └── custom.css ├── config.toml ├── content ├── _index.md ├── bbox-services.png └── docs │ ├── _index.md │ ├── api-endpoints.md │ ├── asset-server │ ├── _index.md │ └── configuration.md │ ├── core │ ├── _index.md │ └── configuration.md │ ├── feature-server │ ├── _index.md │ ├── configuration.md │ └── endpoints.md │ ├── installation.md │ ├── instrumentation.md │ ├── map-server │ ├── _index.md │ ├── configuration.md │ └── endpoints.md │ ├── processes-server │ ├── _index.md │ ├── configuration.md │ └── endpoints.md │ ├── routing-server │ ├── _index.md │ ├── configuration.md │ └── endpoints.md │ ├── running.md │ └── tile-server │ ├── _index.md │ ├── configuration.md │ ├── endpoints.md │ ├── guides.md │ ├── installation.md │ ├── running.md │ ├── seeding.md │ └── styling.md ├── i18n └── en.yaml ├── justfile ├── layouts └── partials │ └── custom │ └── head-end.html └── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon-dark.svg ├── favicon.ico ├── favicon.svg └── site.webmanifest /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /target 3 | /.gitlab-ci.yml 4 | /.github 5 | /docker 6 | README.md 7 | /bbox.toml 8 | /assets 9 | /bbox.sublime-* 10 | /bbox-tile-server/s3data 11 | -------------------------------------------------------------------------------- /.github/workflows/build-deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | build-docs: 13 | name: Build Docs 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: true 19 | 20 | - name: Install just 21 | uses: taiki-e/install-action@v2 22 | with: { tool: just } 23 | 24 | - name: Install nightly toolchain 25 | uses: dtolnay/rust-toolchain@master 26 | with: 27 | toolchain: nightly-2024-09-23 28 | 29 | - name: Install protoc 30 | uses: arduino/setup-protoc@v3 31 | with: 32 | repo-token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Install Python build dependencies 35 | run: python -m pip install jmespath 36 | 37 | - name: Install Hugo 38 | uses: peaceiris/actions-hugo@v3 39 | with: 40 | hugo-version: "0.111.3" 41 | # extended: true 42 | 43 | - name: Generate reference documentation 44 | run: cd website && just refdoc 45 | 46 | - name: Build site 47 | run: cd website && just build 48 | 49 | - name: Deploy 50 | uses: peaceiris/actions-gh-pages@v3 51 | if: github.ref == 'refs/heads/main' 52 | with: 53 | github_token: ${{ secrets.GITHUB_TOKEN }} 54 | publish_dir: ./website/public 55 | cname: www.bbox.earth 56 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker images 2 | 3 | on: 4 | # push: 5 | # branches: [main] 6 | release: 7 | types: [published] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | 17 | - uses: actions/checkout@v4 18 | 19 | - name: Get version tag 20 | id: get_tag 21 | run: | 22 | if [ ${{ startsWith(github.ref, 'refs/tags/') }} = true ]; then 23 | echo "tag=latest,${GITHUB_REF:10}" >>$GITHUB_OUTPUT 24 | else 25 | echo "tag=latest" >>$GITHUB_OUTPUT 26 | fi 27 | 28 | - name: Build and Publish bbox-server-qgis 29 | uses: elgohr/Publish-Docker-Github-Action@v5 30 | with: 31 | name: sourcepole/bbox-server-qgis 32 | username: ${{ secrets.DOCKER_HUB_USER }} 33 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 34 | tags: "${{ steps.get_tag.outputs.tag }}" 35 | dockerfile: docker/Dockerfile-qgis-server 36 | 37 | - name: Build and Publish bbox-map-server-qgis 38 | uses: elgohr/Publish-Docker-Github-Action@v5 39 | env: 40 | BUILD_DIR: bbox-map-server 41 | with: 42 | name: sourcepole/bbox-map-server-qgis 43 | username: ${{ secrets.DOCKER_HUB_USER }} 44 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 45 | buildargs: BUILD_DIR 46 | tags: "${{ steps.get_tag.outputs.tag }}" 47 | dockerfile: docker/Dockerfile-qgis-server 48 | 49 | - name: Build and Publish bbox-tile-server 50 | uses: elgohr/Publish-Docker-Github-Action@v5 51 | env: 52 | BUILD_DIR: bbox-tile-server 53 | with: 54 | name: sourcepole/bbox-tile-server 55 | username: ${{ secrets.DOCKER_HUB_USER }} 56 | password: ${{ secrets.DOCKER_HUB_PASSWORD }} 57 | buildargs: BUILD_DIR 58 | tags: "${{ steps.get_tag.outputs.tag }}" 59 | dockerfile: docker/Dockerfile 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /*.pem 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "website/themes/hextra"] 2 | path = website/themes/hextra 3 | url = https://github.com/pka/hextra 4 | branch = backport-debian 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "bbox-asset-server", 4 | "bbox-core", 5 | "bbox-feature-server", 6 | "bbox-frontend", 7 | "bbox-map-server", 8 | "bbox-map-server/mock-fcgi-wms", 9 | "bbox-processes-server", 10 | "bbox-routing-server", 11 | "bbox-server", 12 | "bbox-tile-server", 13 | ] 14 | default-members = ["bbox-server"] 15 | resolver = "2" 16 | 17 | [workspace.package] 18 | edition = "2021" 19 | rust-version = "1.65" 20 | repository = "https://github.com/bbox-services/bbox" 21 | homepage = "https://www.bbox.earth" 22 | license = "MIT OR Apache-2.0" 23 | 24 | [workspace.dependencies] 25 | actix-web = { version = "4.0.0", features = ["rustls"] } 26 | async-stream = "0.3.4" 27 | async-trait = "0.1.65" 28 | awc = "3.0.0" # features = ["rustls"] 29 | clap = { version = "4.2.1", features = ["derive"] } 30 | chrono = "0.4" 31 | futures = "0.3.21" 32 | geozero = { version = "0.11.0" } 33 | log = "0.4.17" 34 | minijinja = { version = "2.2.0", features = ["loader"] } 35 | num_cpus = "1.13.1" 36 | once_cell = "1.12.0" 37 | opentelemetry = { version = "0.18", default-features = false, features = [ 38 | "trace", 39 | "metrics", 40 | "rt-tokio", 41 | ] } 42 | prometheus = { version = "0.13", default-features = false } 43 | reqwest = { version = "0.11.11", default-features = false, features = [ 44 | "rustls-tls", 45 | ] } 46 | rust-embed = { version = "6.8.1", features = ["compression"] } 47 | serde = { version = "1.0", features = ["derive"] } 48 | serde_json = "1.0.57" 49 | serde_urlencoded = "0.7.1" 50 | sqlx = { version = "0.7.0", default-features = false, features = [ 51 | "runtime-tokio-rustls", 52 | "sqlite", 53 | "postgres", 54 | "chrono", 55 | ] } 56 | tempfile = "3.8.1" 57 | thiserror = "1.0.31" 58 | tokio = { version = "1.19.2" } 59 | 60 | [patch.crates-io] 61 | #tile-grid = { path = "../tile-grid" } 62 | #geozero = { path = "../geozero/geozero" } 63 | #fast_paths = { path = "../fast_paths" } 64 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Pirmin Kalberer 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BBOX services 2 | 3 | Composable spatial services. 4 | 5 | [![Documentation](https://img.shields.io/badge/docs-Book-informational)](https://www.bbox.earth/) 6 | [![CI build](https://github.com/bbox-services/bbox/workflows/CI/badge.svg)](https://github.com/bbox-services/bbox/actions) 7 | [![Docker](https://img.shields.io/docker/v/sourcepole/bbox-server-qgis?label=Docker%20image&sort=semver)](https://hub.docker.com/r/sourcepole/bbox-server-qgis) 8 | 9 | ``` 10 | ___ ___ _____ __ 11 | | _ ) _ )/ _ \ \/ / 12 | | _ \ _ \ (_) > < 13 | |___/___/\___/_/\_\ 14 | ``` 15 | 16 | Components: 17 | * [BBOX Feature server](bbox-feature-server): OGC API Features service 18 | * [BBOX Map server](bbox-map-server): OGC API Map service 19 | * [BBOX Tile server](bbox-tile-server): OGC API Tile service 20 | * [BBOX Asset server](bbox-asset-server): Serving static and templated files 21 | * [BBOX Processes server](bbox-processes-server): OGC API Processes service 22 | * [BBOX Routing server](bbox-routing-server): OGC API Routing service (experimental) 23 | 24 | Features: 25 | * Built-in high performance HTTP server 26 | * QWC2 Map viewer 27 | * Instrumentation: Prometheus and Jaeger tracing 28 | * Healths endpoints for Docker and Kubernetes hosting 29 | 30 | 31 | See [Documentation](https://www.bbox.earth/) for detailed information. 32 | 33 | ## Installation 34 | 35 | See [Documentation](https://www.bbox.earth/docs/installation/) for instructions. 36 | 37 | ## Docker 38 | 39 | docker run -p 8080:8080 sourcepole/bbox-server-qgis 40 | 41 | Serve tiles from file: 42 | 43 | docker run -p 8080:8080 -v $PWD/assets:/assets:ro sourcepole/bbox-server-qgis bbox-server serve /assets/liechtenstein.mbtiles 44 | 45 | Run with configuration file: 46 | 47 | docker run -p 8080:8080 -v $PWD/bbox.toml:/var/www/bbox.toml:ro -v $PWD/assets:/assets:ro sourcepole/bbox-server-qgis 48 | 49 | ## License 50 | 51 | Licensed under either of 52 | 53 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 54 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 55 | 56 | at your option. 57 | 58 | ### Contribution 59 | 60 | Unless you explicitly state otherwise, any contribution intentionally submitted 61 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 62 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- 1 | /download 2 | /maputnik 3 | /*.bin 4 | -------------------------------------------------------------------------------- /assets/Makefile: -------------------------------------------------------------------------------- 1 | # Download and prapare data 2 | 3 | all: ne_extracts.gpkg 4 | 5 | download/natural_earth_vector.gpkg.zip: 6 | mkdir -p download 7 | wget -O $@ https://naciscdn.org/naturalearth/packages/natural_earth_vector.gpkg.zip 8 | 9 | download/packages/natural_earth_vector.gpkg: download/natural_earth_vector.gpkg.zip 10 | unzip -d download $< 11 | touch $@ 12 | 13 | ne_extracts.gpkg: download/packages/natural_earth_vector.gpkg 14 | ogr2ogr -f GPKG -select scalerank,featurecla,name -nlt PROMOTE_TO_MULTI $@ $< ne_10m_rivers_lake_centerlines 15 | ogr2ogr -update -select scalerank,featurecla,name -nlt PROMOTE_TO_MULTI $@ $< ne_10m_lakes 16 | ogr2ogr -update -select scalerank,labelrank,featurecla,name $@ $< ne_10m_populated_places 17 | 18 | # For reverting mvtbench.gpkg add the following .git/hooks/pre-commit file: 19 | # #!/bin/sh 20 | # 21 | # if [ -f data/mvtbench.gpkg-wal ]; then 22 | # echo Revert mvtbench.gpkg 23 | # git checkout 2c96bb5 data/mvtbench.gpkg 24 | # rm data/mvtbench.gpkg-* 25 | # fi 26 | 27 | 28 | # Setup PostgreSQL database 29 | 30 | DBNAME = bbox_tests 31 | 32 | createdb: 33 | psql postgres -c "DROP DATABASE IF EXISTS $(DBNAME)" 34 | psql postgres -c "CREATE DATABASE $(DBNAME)" 35 | psql $(DBNAME) -c "CREATE EXTENSION postgis" 36 | psql $(DBNAME) -c "CREATE SCHEMA ne" 37 | 38 | loaddata: 39 | ogr2ogr -f PostgreSQL PG:dbname=$(DBNAME) -lco SCHEMA=ne ne_extracts.gpkg 40 | 41 | 42 | # Misc targets 43 | 44 | mvtbench: 45 | # docker run -p 127.0.0.1:5439:5432 -d --name mvtbenchdb --rm sourcepole/mvtbenchdb 46 | cd ../bbox-tile-server && cargo run -- --t-rex-config=../assets/mvtbench_t_rex.toml serve 47 | # curl -o /tmp/tile.mvt http://localhost:8080/xyz/ne_countries/0/0/0.pbf 48 | 49 | t_rex_multi: 50 | # docker run -p 127.0.0.1:5439:5432 -d --name trextestdb --rm sourcepole/trextestdb 51 | cd ../bbox-tile-server && cargo run -- --t-rex-config=../../t-rex/examples/multiple-ds.toml serve 52 | 53 | t_rex_grid: 54 | cd ../bbox-tile-server && cargo run -- --t-rex-config=../../t-rex/examples/utmgrid.toml serve 55 | -------------------------------------------------------------------------------- /assets/helloworld.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/helloworld.db -------------------------------------------------------------------------------- /assets/liechtenstein.mbtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/liechtenstein.mbtiles -------------------------------------------------------------------------------- /assets/maplibre.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MapLibre Viewer 6 | 7 | 8 | 9 | 10 | 11 |
12 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /assets/mapserver.conf: -------------------------------------------------------------------------------- 1 | # 2 | # MapServer 8.0 Config File 3 | # 4 | CONFIG 5 | 6 | # 7 | # Environment variables (see https://mapserver.org/environment_variables.html) 8 | # 9 | ENV 10 | # 11 | # Limit Mapfile Access 12 | # 13 | # MS_MAP_NO_PATH "1" 14 | MS_MAP_PATTERN "" ## required when referencing mapfiles by path 15 | # MS_MAP_BAD_PATTERN "[/\\]{2}|[/\\]?\\.+[/\\]|," 16 | 17 | # 18 | # Global Log/Debug Setup 19 | # 20 | # MS_DEBUGLEVEL "5" 21 | # MS_ERRORFILE "/opt/mapserver/logs/mapserver.log" 22 | 23 | # 24 | # Default Map 25 | # 26 | # MS_MAPFILE "/opt/mapserver/test/test.map" 27 | 28 | # 29 | # Proj Library 30 | # 31 | # PROJ_LIB "/usr/local/share/proj" 32 | 33 | # 34 | # Request Control 35 | # 36 | # disable POST requests (allowed by default, any value will do) 37 | # MS_NO_POST "1" 38 | 39 | # 40 | # Response Control 41 | # 42 | # suppress version information in reponses, recommended for production deployments 43 | # MS_NO_VERSION "1" 44 | 45 | # 46 | # Other Options 47 | # 48 | # MS_ENCRYPTION_KEY 49 | # MS_USE_GLOBAL_FT_CACHE 50 | # MS_PDF_CREATION_DATE 51 | # MS_MAPFILE_PATTERN "\.map$" 52 | # MS_XMLMAPFILE_XSLT 53 | # MS_MODE 54 | # MS_OPENLAYERS_JS_URL 55 | # MS_TEMPPATH 56 | # MS_MAX_OPEN_FILES 57 | 58 | # 59 | # OGC API 60 | # 61 | # OGCAPI_HTML_TEMPLATE_DIRECTORY "/usr/local/mapserver/share/ogcapi/templates/html-bootstrap4/" 62 | END 63 | 64 | # 65 | # Map Aliases 66 | # 67 | MAPS 68 | # TEST_MAPFILE "/opt/mapserver/test/test.map" 69 | END 70 | 71 | END 72 | -------------------------------------------------------------------------------- /assets/mvtbench.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/mvtbench.gpkg -------------------------------------------------------------------------------- /assets/mvtbench.pmtiles: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/mvtbench.pmtiles -------------------------------------------------------------------------------- /assets/mvtbench_t_rex.toml: -------------------------------------------------------------------------------- 1 | # t-rex configuration for mvtbench 2 | 3 | [service.mvt] 4 | viewer = true 5 | 6 | [[datasource]] 7 | dbconn = "postgresql://mvtbench:mvtbench@127.0.0.1:5439/mvtbench" 8 | name = "pg" 9 | default = true 10 | 11 | [grid] 12 | predefined = "web_mercator" 13 | 14 | [[tileset]] 15 | name = "ne_countries" 16 | attribution = "Natural Earth v4" 17 | extent = [-179.97277, -83.05457, 179.99366, 83.23559] 18 | minzoom = 0 19 | maxzoom = 6 20 | 21 | [[tileset.layer]] 22 | name = "country" 23 | geometry_field = "wkb_geometry" 24 | geometry_type = "MULTIPOLYGON" 25 | srid = 3857 26 | buffer_size = 3 27 | simplify = true 28 | [[tileset.layer.query]] 29 | sql = """SELECT wkb_geometry, adm0_a3, mapcolor7 FROM ne_10m_admin_0_countries WHERE min_zoom::integer <= !zoom! AND wkb_geometry && !bbox!""" 30 | 31 | [[tileset.layer]] 32 | name = "country-name" 33 | geometry_field = "wkb_geometry" 34 | geometry_type = "POINT" 35 | srid = 3857 36 | buffer_size = 0 37 | [[tileset.layer.query]] 38 | sql = """SELECT wkb_geometry, abbrev, name FROM ne_10m_admin_0_country_points""" 39 | 40 | [[tileset.layer]] 41 | name = "geo-lines" 42 | geometry_field = "wkb_geometry" 43 | geometry_type = "MULTILINESTRING" 44 | srid = 3857 45 | buffer_size = 0 46 | simplify = false 47 | [[tileset.layer.query]] 48 | # ne_50m_geographic_lines 49 | minzoom = 1 50 | maxzoom = 4 51 | sql = """SELECT wkb_geometry, name FROM ne_50m_geographic_lines""" 52 | [[tileset.layer.query]] 53 | # ne_10m_geographic_lines 54 | minzoom = 5 55 | sql = """SELECT wkb_geometry, name FROM ne_10m_geographic_lines""" 56 | 57 | [[tileset.layer]] 58 | name = "land-border-country" 59 | geometry_field = "wkb_geometry" 60 | geometry_type = "MULTILINESTRING" 61 | fid_field = "ogc_fid" 62 | srid = 3857 63 | buffer_size = 0 64 | simplify = true 65 | [[tileset.layer.query]] 66 | # ne_10m_admin_0_boundary_lines_land 67 | sql = """SELECT wkb_geometry FROM ne_10m_admin_0_boundary_lines_land WHERE min_zoom::integer <= !zoom! AND wkb_geometry && !bbox!""" 68 | 69 | [[tileset.layer]] 70 | name = "state" 71 | geometry_field = "wkb_geometry" 72 | geometry_type = "MULTILINESTRING" 73 | srid = 3857 74 | buffer_size = 0 75 | simplify = true 76 | [[tileset.layer.query]] 77 | sql = """SELECT wkb_geometry, adm0_a3 FROM ne_10m_admin_1_states_provinces_lines WHERE min_zoom::integer <= !zoom! AND wkb_geometry && !bbox!""" 78 | 79 | [cache.file] 80 | base = "/tmp/tiles" 81 | baseurl = "http://example.com/tiles" 82 | 83 | [webserver] 84 | bind = "0.0.0.0" 85 | port = 6767 86 | threads = 4 87 | #cache_control_max_age = 43200 88 | 89 | [[webserver.static]] 90 | path = "/map" 91 | dir = "../maps/" 92 | -------------------------------------------------------------------------------- /assets/ne.map: -------------------------------------------------------------------------------- 1 | MAP 2 | NAME 'ne_countries' 3 | SIZE 500 500 4 | MAXSIZE 4096 5 | PROJECTION 6 | "+init=epsg:3857" 7 | END 8 | OUTPUTFORMAT 9 | NAME "png8" 10 | DRIVER AGG/PNG8 11 | MIMETYPE "image/png; mode=8bit" 12 | IMAGEMODE RGB 13 | EXTENSION "png" 14 | FORMATOPTION "QUANTIZE_FORCE=on" 15 | FORMATOPTION "QUANTIZE_COLORS=256" 16 | FORMATOPTION "GAMMA=0.75" 17 | TRANSPARENT ON 18 | END 19 | 20 | OUTPUTFORMAT 21 | NAME "mvt" 22 | DRIVER MVT 23 | #FORMATOPTION "EXTENT=512" # default is 4096 24 | FORMATOPTION "EDGE_BUFFER=20" 25 | END 26 | 27 | WEB 28 | METADATA 29 | wms_enable_request "*" 30 | MVT_SIZE '512' 31 | WMS_TITLE 'Natural Earth MVT' 32 | WMS_ONLINERESOURCE 'http://localhost/cgi-bin/mapserv?' 33 | WMS_SRS 'epsg:3857 epsg:4326' 34 | END 35 | END 36 | 37 | LAYER 38 | NAME "country" 39 | TYPE POLYGON 40 | STATUS ON 41 | CONNECTIONTYPE OGR 42 | CONNECTION "mvtbench.gpkg" 43 | DATA "ne_10m_admin_0_countries" 44 | #DATA "wkb_geometry from (SELECT ogc_fid, name, adm0_a3, abbrev, mapcolor7, wkb_geometry FROM ne_10m_admin_0_countries) as temp using unique ogc_fid using SRID=3857" 45 | EXTENT -20037508.342789 -34679773.785951 20037508.342789 18428920.012950 46 | #DUMP true 47 | METADATA 48 | "wms_title" "country" 49 | "wms_srs" "epsg:4326 epsg:3857 epsg:900913" 50 | "wms_feature_info_mime_type" "text/html" 51 | "gml_include_items" "adm0_a3,mapcolor7" 52 | "gml_mapcolor7_type" "integer" 53 | END 54 | PROJECTION 55 | "init=epsg:3857" 56 | END 57 | CLASS 58 | STYLE 59 | OUTLINECOLOR 200 50 100 60 | WIDTH 0.4 61 | END 62 | END 63 | END 64 | 65 | 66 | END # Map end 67 | -------------------------------------------------------------------------------- /assets/ne_extracts.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/ne_extracts.gpkg -------------------------------------------------------------------------------- /assets/ne_extracts.qgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/ne_extracts.qgz -------------------------------------------------------------------------------- /assets/ol-ogc-tiles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | OpenLayers OGC tiles 8 | 9 | 10 | 11 | 12 | 13 | 14 |

OpenLayers OGC tiles

15 | 16 |
17 | 18 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /assets/railway-test.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/assets/railway-test.gpkg -------------------------------------------------------------------------------- /bbox-asset-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-asset-server" 3 | version = "0.6.2" 4 | authors = ["Pirmin Kalberer "] 5 | readme = "README.md" 6 | description = "Serving static and templated files" 7 | categories = ["science::geo"] 8 | 9 | edition.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | homepage.workspace = true 14 | 15 | [dependencies] 16 | actix-files = "0.6.0" 17 | actix-web = { workspace = true } 18 | async-trait = { workspace = true } 19 | bbox-core = { path = "../bbox-core", version = "0.6.2" } 20 | clap = { workspace = true } 21 | configparser = "3.0.0" 22 | log = { workspace = true } 23 | minijinja = { workspace = true } 24 | once_cell = { workspace = true } 25 | rust-embed = { workspace = true } 26 | serde = { workspace = true } 27 | tempfile = { workspace = true } 28 | zip = { version = "2.2.0", default-features = false, features = ["deflate"] } 29 | 30 | [[bin]] 31 | name = "bbox-asset-server" 32 | path = "src/main.rs" 33 | -------------------------------------------------------------------------------- /bbox-asset-server/README.md: -------------------------------------------------------------------------------- 1 | # BBOX Asset server 2 | 3 | Serving static and templated files. 4 | 5 | Features: 6 | - [x] Configurable base directories and endpoints 7 | - [x] Serve fonts and other assets for Tile services 8 | - [ ] Serve MVT sprites from SVG images 9 | - [x] QGIS plugin repository 10 | - [ ] Templates with inputs from path, arguments and configuration 11 | 12 | 13 | ## Configuration 14 | 15 | See [Documentation](https://www.bbox.earth/docs/asset-server/configuration/) for examples. 16 | -------------------------------------------------------------------------------- /bbox-asset-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::qgis_plugins::QgisPluginRepoCfg; 2 | use crate::runtime_templates::TemplateDirCfg; 3 | use bbox_core::config::{from_config_opt_or_exit, ConfigError}; 4 | use bbox_core::service::ServiceConfig; 5 | use clap::ArgMatches; 6 | use serde::Deserialize; 7 | 8 | #[derive(Deserialize, Default, Debug)] 9 | #[serde(default, deny_unknown_fields)] 10 | pub struct AssetServiceCfg { 11 | #[serde(rename = "static")] 12 | pub static_: Vec, 13 | pub template: Vec, 14 | pub repo: Vec, 15 | } 16 | 17 | #[derive(Deserialize, Debug)] 18 | #[serde(deny_unknown_fields)] 19 | pub struct StaticDirCfg { 20 | /// endpoint path for publishing 21 | pub path: String, 22 | /// file directory 23 | pub dir: String, 24 | } 25 | 26 | impl AssetServiceCfg { 27 | pub(crate) fn from_config() -> Self { 28 | from_config_opt_or_exit("assets").unwrap_or_default() 29 | } 30 | } 31 | 32 | impl ServiceConfig for AssetServiceCfg { 33 | fn initialize(_args: &ArgMatches) -> Result { 34 | Ok(AssetServiceCfg::from_config()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /bbox-asset-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod endpoints; 3 | pub mod qgis_plugins; 4 | pub mod runtime_templates; 5 | pub mod service; 6 | 7 | pub use service::*; 8 | -------------------------------------------------------------------------------- /bbox-asset-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use bbox_asset_server::service::AssetService; 2 | use bbox_core::service::run_service; 3 | 4 | fn main() { 5 | run_service::().unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /bbox-asset-server/src/runtime_templates.rs: -------------------------------------------------------------------------------- 1 | use minijinja::{path_loader, Environment}; 2 | use serde::Deserialize; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Deserialize, Debug)] 6 | #[serde(deny_unknown_fields)] 7 | pub struct TemplateDirCfg { 8 | /// endpoint path for publishing 9 | pub path: String, 10 | /// template file directory 11 | pub dir: String, 12 | } 13 | 14 | #[derive(Default)] 15 | pub struct RuntimeTemplates { 16 | envs: HashMap>, 17 | } 18 | 19 | impl RuntimeTemplates { 20 | pub fn add(&mut self, dir: &str, path: &str) { 21 | let mut env = Environment::new(); 22 | env.set_loader(path_loader(dir)); 23 | self.envs.insert(path.to_string(), env); 24 | } 25 | pub fn get(&self, path: &str) -> Option<&Environment<'static>> { 26 | self.envs.get(path) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /bbox-asset-server/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::config::AssetServiceCfg; 2 | use crate::qgis_plugins::plugin_files; 3 | use async_trait::async_trait; 4 | use bbox_core::cli::{NoArgs, NoCommands}; 5 | use bbox_core::config::{app_dir, CoreServiceCfg}; 6 | use bbox_core::metrics::{no_metrics, NoMetrics}; 7 | use bbox_core::service::OgcApiService; 8 | use log::{info, warn}; 9 | use std::collections::HashMap; 10 | use std::path::{Path, PathBuf}; 11 | 12 | pub type PluginIndex = HashMap>; 13 | 14 | #[derive(Clone)] 15 | pub struct AssetService { 16 | pub plugins_index: PluginIndex, 17 | } 18 | 19 | #[async_trait] 20 | impl OgcApiService for AssetService { 21 | type Config = AssetServiceCfg; 22 | type CliCommands = NoCommands; 23 | type CliArgs = NoArgs; 24 | type Metrics = NoMetrics; 25 | 26 | async fn create(service_cfg: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self { 27 | let mut plugins_index = PluginIndex::new(); 28 | for repo in &service_cfg.repo { 29 | let dir = app_dir(&repo.dir).to_string_lossy().to_string(); 30 | if Path::new(&dir).is_dir() { 31 | info!("Scanning QGIS plugin repository directory '{dir}'"); 32 | let plugins = plugin_files(&dir); 33 | plugins_index.insert(format!("{}/plugins.xml", repo.path), plugins); 34 | } else { 35 | warn!("QGIS plugin repository file directory '{dir}' not found"); 36 | } 37 | } 38 | // static and template dir config is processed in register_endpoints 39 | AssetService { plugins_index } 40 | } 41 | fn metrics(&self) -> &'static Self::Metrics { 42 | no_metrics() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /bbox-asset-server/src/static/plugins.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | BBOX QGIS Plugin Repository 9 | 10 | 11 | 53 | 54 | 55 | 56 | 57 |

BBOX QGIS Plugin Repository

58 | 59 |
60 |
61 | 62 | 63 | 64 | 65 | : 66 | 67 |
68 |
69 | 70 |
71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | QGIS version: 81 | 82 |
83 | Download: 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 |
92 | Author: 93 | 94 |
95 | 96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 |
104 | 105 |
106 | -------------------------------------------------------------------------------- /bbox-asset-server/src/templates/plugins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% for plugin in plugins %} 5 | 6 | {{ plugin.description }} 7 | {{ plugin.version }} 8 | {{ plugin.qgis_minimum_version }} 9 | {{ plugin.qgis_maximum_version }} 10 | {% if plugin.homepage %}{{ plugin.homepage | safe }}{% endif %} 11 | {{ plugin.file_name }} 12 | {{ plugin.author }} 13 | {{url | safe}}/{{ plugin.file_name }} 14 | BBOX 15 | 1970-01-01 16 | 1970-01-01 17 | {{ plugin.experimental | default(False) }} 18 | 19 | {% endfor %} 20 | 21 | -------------------------------------------------------------------------------- /bbox-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-core" 3 | version = "0.6.2" 4 | authors = ["Pirmin Kalberer "] 5 | readme = "README.md" 6 | description = "Common functionality for BBOX services" 7 | categories = ["science::geo"] 8 | 9 | edition.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | homepage.workspace = true 14 | 15 | [features] 16 | html = [] 17 | oidc = ["openidconnect", "reqwest"] 18 | 19 | [dependencies] 20 | actix-cors = "0.7.0" 21 | actix-session = { version = "0.10.0", features = ["cookie-session"] } 22 | actix-web = { workspace = true } 23 | actix-web-opentelemetry = { version = "0.13", features = ["metrics-prometheus"] } 24 | async-stream = { workspace = true } 25 | async-trait = { workspace = true } 26 | clap = { workspace = true } 27 | env_logger = "0.11.5" 28 | figment = { version = "0.10.6", features = ["env", "toml"] } 29 | flate2 = "1.0.28" 30 | futures-core = "0.3.28" 31 | fxhash = "0.2.1" 32 | ignore = "0.4" 33 | log = { workspace = true } 34 | mime = "0.3.16" 35 | mime_guess = "2.0.3" 36 | minijinja = { workspace = true } 37 | num_cpus = { workspace = true } 38 | once_cell = "1.8.0" 39 | openidconnect = { version = "3.2.0", optional = true } 40 | opentelemetry = { workspace = true } 41 | opentelemetry-jaeger = { version = "0.17", features = ["rt-tokio"] } 42 | opentelemetry-prometheus = { version = "0.11" } 43 | prometheus = { workspace = true } 44 | reqwest = { workspace = true, optional = true } 45 | rust-embed = { workspace = true } 46 | rustls = "0.20.8" # Same as actix-tls -> tokio-rustls 47 | rustls-pemfile = "1.0.2" 48 | serde = { workspace = true } 49 | serde_json = { workspace = true } 50 | serde_yaml = "0.9.34" 51 | sqlx = { workspace = true } 52 | thiserror = { workspace = true } 53 | 54 | [dev-dependencies] 55 | 56 | 57 | [lib] 58 | path = "src/lib.rs" 59 | -------------------------------------------------------------------------------- /bbox-core/README.md: -------------------------------------------------------------------------------- 1 | # bbox-core 2 | 3 | `bbox-core` is a sub-crate that provides common functionality for the BBOX services. 4 | 5 | Not intended for external usage 6 | -------------------------------------------------------------------------------- /bbox-core/fixtures/dc.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-core/fixtures/dc.webp -------------------------------------------------------------------------------- /bbox-core/fixtures/world.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-core/fixtures/world.jpg -------------------------------------------------------------------------------- /bbox-core/fixtures/world.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-core/fixtures/world.png -------------------------------------------------------------------------------- /bbox-core/src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "oidc")] 2 | pub mod oidc; 3 | 4 | pub struct Identity { 5 | pub username: String, 6 | pub groups: Vec, 7 | } 8 | 9 | #[cfg(not(feature = "oidc"))] 10 | pub mod oidc { 11 | use super::Identity; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | type AuthError = std::io::Error; 15 | 16 | #[derive(Deserialize, Serialize, Default, Clone, Debug)] 17 | pub struct OidcAuthCfg; 18 | 19 | #[derive(Default, Clone)] 20 | pub struct OidcClient { 21 | pub authorize_url: String, 22 | } 23 | 24 | impl OidcClient { 25 | pub async fn from_config(_cfg: &OidcAuthCfg) -> Self { 26 | Self::default() 27 | } 28 | } 29 | 30 | #[derive(Deserialize, Serialize)] 31 | pub struct AuthRequest; 32 | 33 | impl AuthRequest { 34 | pub async fn auth(&self, _oidc: &OidcClient) -> Result { 35 | unimplemented!() 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /bbox-core/src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{config_error_exit, Loglevel}; 2 | use crate::service::OgcApiService; 3 | use clap::{ArgMatches, Args, Command, CommandFactory, FromArgMatches, Parser, Subcommand}; 4 | use log::warn; 5 | use std::env; 6 | use std::path::{Path, PathBuf}; 7 | 8 | #[derive(Args, Debug)] 9 | pub struct GlobalArgs { 10 | /// Config file (Default: bbox.toml) 11 | #[arg(short, long, value_name = "FILE")] 12 | pub config: Option, 13 | /// Log level (Default: info) 14 | #[arg(long)] 15 | pub loglevel: Option, 16 | } 17 | 18 | #[derive(Parser, Debug)] 19 | pub enum CommonCommands { 20 | /// Run service 21 | Serve(ServeArgs), 22 | } 23 | 24 | #[derive(Args, Debug)] 25 | pub struct ServeArgs { 26 | /// Serve service from file or URL 27 | pub file_or_url: Option, 28 | } 29 | 30 | /* t-rex serve: 31 | OPTIONS: 32 | --bind Bind web server to this address (0.0.0.0 for all) 33 | -c, --config Load from custom config file 34 | --loglevel Log level (Default: info) 35 | --port Bind web server to this port 36 | */ 37 | 38 | #[derive(Parser, Debug)] 39 | pub enum NoCommands {} 40 | 41 | #[derive(Args, Debug)] 42 | pub struct NoArgs; 43 | 44 | /// Combined cli commands and args for composed service 45 | #[derive(Clone)] 46 | pub struct CliArgs { 47 | cli: Command, 48 | } 49 | 50 | impl CliArgs { 51 | pub fn register_args(&mut self) { 52 | let mut cli = C::augment_subcommands(self.cli.clone()); 53 | if std::any::type_name::() != "bbox_core::cli::NoArgs" { 54 | cli = A::augment_args(cli); 55 | } 56 | self.cli = cli; 57 | } 58 | /// Register service commands and args 59 | pub fn register_service_args(&mut self) { 60 | self.register_args::(); 61 | } 62 | pub fn cli_matches(&self) -> ArgMatches { 63 | // cli.about("BBOX tile server") 64 | self.cli.clone().get_matches() 65 | } 66 | pub fn apply_global_args(&self) { 67 | let Ok(args) = GlobalArgs::from_arg_matches(&self.cli_matches()) else { 68 | warn!("GlobalArgs::from_arg_matches error"); 69 | return; 70 | }; 71 | if let Some(config) = args.config { 72 | if Path::new(&config).exists() { 73 | env::set_var("BBOX_CONFIG", config); 74 | } else { 75 | config_error_exit(format!("Config file {config:?} not found.")); 76 | } 77 | } 78 | } 79 | } 80 | 81 | impl Default for CliArgs { 82 | fn default() -> Self { 83 | Self { 84 | cli: NoCommands::command(), 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /bbox-core/src/empty/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-core/src/empty/.gitkeep -------------------------------------------------------------------------------- /bbox-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod auth; 3 | pub mod cli; 4 | pub mod config; 5 | pub mod endpoints; 6 | pub mod file_search; 7 | mod formats; 8 | pub mod logger; 9 | pub mod metrics; 10 | pub mod ogcapi; 11 | pub mod pg_ds; 12 | pub mod service; 13 | mod service_utils; 14 | pub mod static_assets; 15 | pub mod static_files; 16 | pub mod templates; 17 | mod tile_response; 18 | pub mod tls; 19 | 20 | pub use formats::*; 21 | pub use service_utils::*; 22 | pub use tile_response::*; 23 | -------------------------------------------------------------------------------- /bbox-core/src/logger.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Loglevel; 2 | use std::env; 3 | 4 | pub fn init(level: Option) { 5 | if let Some(level) = level { 6 | let levelstr = match level { 7 | Loglevel::Error => "error", 8 | Loglevel::Warn => "warn", 9 | Loglevel::Info => "info", 10 | Loglevel::Debug => "debug,tokio=info", 11 | Loglevel::Trace => "trace", 12 | }; 13 | env::set_var("RUST_LOG", levelstr); 14 | } else if env::var("RUST_LOG").is_err() { 15 | env::set_var( 16 | "RUST_LOG", 17 | "info,bbox_map_server=debug,bbox_feature_server=debug,bbox_frontend=debug,sqlx=warn", 18 | ); 19 | } 20 | env_logger::init(); 21 | } 22 | -------------------------------------------------------------------------------- /bbox-core/src/metrics.rs: -------------------------------------------------------------------------------- 1 | use crate::config::MetricsCfg; 2 | use once_cell::sync::OnceCell; 3 | use opentelemetry::{ 4 | global, 5 | sdk::{ 6 | export::metrics::aggregation, 7 | metrics::{controllers, processors, selectors}, 8 | propagation::TraceContextPropagator, 9 | }, 10 | }; 11 | use opentelemetry_prometheus::PrometheusExporter; 12 | 13 | fn init_tracer(config: &MetricsCfg) { 14 | if let Some(cfg) = &config.jaeger { 15 | global::set_text_map_propagator(TraceContextPropagator::new()); // default header: traceparent 16 | opentelemetry_jaeger::new_agent_pipeline() 17 | .with_endpoint(cfg.agent_endpoint.clone()) 18 | .with_service_name("bbox") 19 | .install_batch(opentelemetry::runtime::Tokio) 20 | .expect("Failed to initialize tracer"); 21 | } 22 | } 23 | 24 | pub(crate) fn init_metrics_exporter() -> Option { 25 | let metrics_cfg = MetricsCfg::from_config()?; 26 | 27 | init_tracer(&metrics_cfg); 28 | 29 | metrics_cfg.prometheus.as_ref()?; 30 | 31 | // Prometheus request metrics handler 32 | let controller = controllers::basic( 33 | processors::factory( 34 | selectors::simple::histogram([1.0, 2.0, 5.0, 10.0, 20.0, 50.0]), 35 | aggregation::cumulative_temporality_selector(), 36 | ) 37 | .with_memory(true), 38 | ) 39 | .build(); 40 | let exporter = opentelemetry_prometheus::exporter(controller).init(); 41 | // let metrics_handler = PrometheusMetricsHandler::new(exporter); 42 | 43 | // Run actix server, metrics are now available at http://localhost:8080/metrics 44 | // HttpServer::new(move || { 45 | // App::new() 46 | // .wrap(RequestTracing::new()) 47 | // .wrap(request_metrics.clone()) 48 | // .route("/metrics", web::get().to(metrics_handler.clone())) 49 | // }) 50 | 51 | Some(exporter) 52 | } 53 | 54 | #[derive(Default)] 55 | pub struct NoMetrics; 56 | 57 | pub fn no_metrics() -> &'static NoMetrics { 58 | static METRICS: OnceCell = OnceCell::new(); 59 | METRICS.get_or_init(NoMetrics::default) 60 | } 61 | -------------------------------------------------------------------------------- /bbox-core/src/pg_ds.rs: -------------------------------------------------------------------------------- 1 | use crate::config::DsPostgisCfg; 2 | use log::info; 3 | use sqlx::postgres::{PgPool, PgPoolOptions}; 4 | use thiserror::Error; 5 | 6 | #[derive(Error, Debug)] 7 | pub enum Error { 8 | #[error(transparent)] 9 | DbError(#[from] sqlx::Error), 10 | } 11 | 12 | pub type Result = std::result::Result; 13 | 14 | #[derive(Clone, Debug)] 15 | pub struct PgDatasource { 16 | pub pool: PgPool, 17 | } 18 | 19 | impl PgDatasource { 20 | pub async fn from_config(ds: &DsPostgisCfg, envvar: Option) -> Result { 21 | Self::new_pool(&envvar.unwrap_or(ds.url.clone())).await 22 | } 23 | pub async fn new_pool(url: &str) -> Result { 24 | info!("Connecting to {url}"); 25 | let pool = PgPoolOptions::new() 26 | .min_connections(0) 27 | .max_connections(8) 28 | .connect(url) 29 | .await?; 30 | Ok(PgDatasource { pool }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /bbox-core/src/service_utils.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | /// Store named objects (e.g. datasources) 4 | pub struct NamedObjectStore { 5 | /// Lookup name -> Object 6 | lookup: HashMap, 7 | /// Name of default object. 8 | default: Option, 9 | } 10 | 11 | impl Default for NamedObjectStore { 12 | fn default() -> Self { 13 | NamedObjectStore { 14 | lookup: HashMap::new(), 15 | default: None, 16 | } 17 | } 18 | } 19 | 20 | impl NamedObjectStore { 21 | /// Add named object. 22 | /// 23 | /// Notice: duplicate names overwrite existing entries. 24 | pub fn add(&mut self, name: &str, ds: T) { 25 | self.lookup.insert(name.to_string(), ds); 26 | // First one added is default, when not explicitely set. 27 | self.default.get_or_insert(name.to_string()); 28 | } 29 | 30 | pub fn get(&self, name: &str) -> Option<&T> { 31 | self.lookup.get(name) 32 | } 33 | 34 | pub fn get_mut(&mut self, name: &str) -> Option<&mut T> { 35 | self.lookup.get_mut(name) 36 | } 37 | 38 | pub fn get_default(&self) -> Option<&T> { 39 | let no_default = "".to_string(); 40 | let name = self.default.as_ref().unwrap_or(&no_default); 41 | self.get(name) 42 | } 43 | 44 | pub fn get_default_mut(&mut self) -> Option<&mut T> { 45 | let no_default = "".to_string(); 46 | let name = self.default.as_ref().unwrap_or(&no_default).clone(); 47 | self.get_mut(&name) 48 | } 49 | 50 | pub fn get_or_default(&self, name: Option<&str>) -> Option<&T> { 51 | if let Some(name) = name { 52 | self.get(name) 53 | } else { 54 | self.get_default() 55 | } 56 | } 57 | 58 | pub fn get_or_default_mut(&mut self, name: Option<&str>) -> Option<&mut T> { 59 | if let Some(name) = name { 60 | self.get_mut(name) 61 | } else { 62 | self.get_default_mut() 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bbox-core/src/static_assets.rs: -------------------------------------------------------------------------------- 1 | use crate::static_files::EmbedFile; 2 | use actix_web::{web, Error}; 3 | use rust_embed::RustEmbed; 4 | use std::path::PathBuf; 5 | 6 | #[derive(RustEmbed)] 7 | #[folder = "static/core/"] 8 | struct CoreStatics; 9 | 10 | pub(crate) async fn favicon() -> Result { 11 | Ok(EmbedFile::open::(PathBuf::from( 12 | "favicon.ico", 13 | ))?) 14 | } 15 | 16 | pub fn register_endpoints(_cfg: &mut web::ServiceConfig) {} 17 | -------------------------------------------------------------------------------- /bbox-core/src/templates.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{http::header, web, FromRequest, HttpRequest, HttpResponse}; 2 | use minijinja::{path_loader, Environment, Error, State}; 3 | use rust_embed::RustEmbed; 4 | use serde::Serialize; 5 | 6 | #[derive(RustEmbed)] 7 | #[folder = "src/empty/"] 8 | pub struct NoTemplates; 9 | 10 | #[cfg(feature = "html")] 11 | #[derive(RustEmbed)] 12 | #[folder = "templates/"] 13 | struct BaseTemplates; 14 | 15 | #[cfg(not(feature = "html"))] 16 | type BaseTemplates = NoTemplates; 17 | 18 | fn truncate(_state: &State, value: String, new_len: usize) -> Result { 19 | let mut s = value; 20 | s.truncate(new_len); 21 | Ok(s) 22 | } 23 | 24 | pub fn create_env(path: &str, _extensions: &[&str]) -> Environment<'static> { 25 | let mut env = create_base_env(); 26 | env.set_loader(path_loader(path)); 27 | env 28 | } 29 | 30 | pub fn create_env_embedded() -> Environment<'static> { 31 | let mut env = create_base_env(); 32 | for f in E::iter() { 33 | add_embedded_template::(&mut env, &f); 34 | } 35 | env 36 | } 37 | 38 | fn create_base_env() -> Environment<'static> { 39 | let mut env = Environment::new(); 40 | env.add_filter("truncate", truncate); 41 | for f in BaseTemplates::iter() { 42 | add_embedded_template::(&mut env, &f); 43 | } 44 | env 45 | } 46 | 47 | fn add_embedded_template(env: &mut Environment<'static>, fname: &str) { 48 | let templ = String::from_utf8(E::get(fname).unwrap().data.to_vec()).unwrap(); 49 | env.add_template_owned(fname.to_string(), templ).unwrap(); 50 | } 51 | 52 | /// Return rendered template 53 | pub async fn render_endpoint( 54 | env: &Environment<'static>, 55 | template: &str, 56 | ctx: S, 57 | ) -> actix_web::Result { 58 | let template = env.get_template(template).expect("couln't load template"); 59 | let page = template.render(ctx).expect("template render failed"); 60 | Ok(HttpResponse::Ok().content_type("text/html").body(page)) 61 | } 62 | 63 | pub async fn html_accepted(req: &HttpRequest) -> bool { 64 | if cfg!(not(feature = "html")) { 65 | return false; 66 | } 67 | 68 | if req.path().ends_with(".json") { 69 | return false; 70 | } 71 | web::Header::::extract(req) 72 | .await 73 | .map(|accept| &accept.preference().to_string() == "text/html") 74 | .unwrap_or(false) 75 | } 76 | -------------------------------------------------------------------------------- /bbox-core/src/tls.rs: -------------------------------------------------------------------------------- 1 | use crate::config::{app_dir, config_error_exit, error_exit}; 2 | use rustls::{Certificate, PrivateKey, ServerConfig}; 3 | use rustls_pemfile::{certs, pkcs8_private_keys}; 4 | use std::{fs::File, io::BufReader}; 5 | 6 | // For self-signed certificates we recommend to use [`mkcert`]. 7 | // To use local CA, you should run: 8 | // 9 | // ```sh 10 | // mkcert -install 11 | // ``` 12 | // 13 | // If you want to generate your own cert/private key file, then run: 14 | // 15 | // ```sh 16 | // mkcert localhost 127.0.0.1 17 | // ``` 18 | // 19 | // [`mkcert`]: https://github.com/FiloSottile/mkcert 20 | 21 | pub fn load_rustls_config(tls_cert: &str, tls_key: &str) -> rustls::ServerConfig { 22 | // init server config builder with safe defaults 23 | let config = ServerConfig::builder() 24 | .with_safe_defaults() 25 | .with_no_client_auth(); 26 | 27 | // load TLS key/cert files 28 | let cert_file = &mut BufReader::new(File::open(app_dir(tls_cert)).unwrap_or_else(error_exit)); 29 | let key_file = &mut BufReader::new(File::open(app_dir(tls_key)).unwrap_or_else(error_exit)); 30 | 31 | // convert files to key/cert objects 32 | let cert_chain = certs(cert_file) 33 | .unwrap_or_else(error_exit) 34 | .into_iter() 35 | .map(Certificate) 36 | .collect(); 37 | let mut keys: Vec = pkcs8_private_keys(key_file) 38 | .unwrap_or_else(error_exit) 39 | .into_iter() 40 | .map(PrivateKey) 41 | .collect(); 42 | 43 | // exit if no keys could be parsed 44 | if keys.is_empty() { 45 | config_error_exit("Could not locate PKCS 8 private keys."); 46 | } 47 | 48 | config 49 | .with_single_cert(cert_chain, keys.remove(0)) 50 | .unwrap_or_else(error_exit) 51 | } 52 | -------------------------------------------------------------------------------- /bbox-core/static/core/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-core/static/core/favicon.ico -------------------------------------------------------------------------------- /bbox-core/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BBOX - {% block title %}Home{% endblock %} 7 | 8 | 9 | 10 | {% block head %} {% endblock %} 11 | 12 | 13 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /bbox-core/templates/collections.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Collections{% endblock %} 4 | {% block content_title %}Collections{% endblock %} 5 | 6 | {% block content %} 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% for collection in collections.collections %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 |
CollectionDescription
{{ collection.title }}{{ collection.description }}
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /bbox-feature-server/.gitignore: -------------------------------------------------------------------------------- 1 | /codegen 2 | -------------------------------------------------------------------------------- /bbox-feature-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-feature-server" 3 | version = "0.6.2" 4 | readme = "README.md" 5 | description = "BBOX OGC API Features Service" 6 | categories = ["science::geo"] 7 | authors = ["Pirmin Kalberer "] 8 | 9 | edition.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | homepage.workspace = true 14 | 15 | [features] 16 | html = ["bbox-core/html"] 17 | 18 | [dependencies] 19 | actix-web = { workspace = true } 20 | async-trait = { workspace = true } 21 | bbox-core = { path = "../bbox-core", version = "0.6.2" } 22 | chrono = { workspace = true } 23 | clap = { workspace = true } 24 | dyn-clone = "1.0.6" 25 | futures = { workspace = true } 26 | geozero = { workspace = true, features = [ "with-gpkg", "with-postgis-sqlx" ] } 27 | log = { workspace = true } 28 | minijinja = { workspace = true } 29 | once_cell = { workspace = true } 30 | rust-embed = { workspace = true } 31 | serde = { workspace = true } 32 | serde_json = { workspace = true } 33 | serde_urlencoded = { workspace = true } 34 | sqlx = { workspace = true } 35 | thiserror = { workspace = true } 36 | 37 | [dev-dependencies] 38 | tokio = { workspace = true, features = ["full"] } 39 | test-log = "0.2.14" 40 | 41 | [[bin]] 42 | name = "bbox-feature-server" 43 | path = "src/main.rs" 44 | -------------------------------------------------------------------------------- /bbox-feature-server/README.md: -------------------------------------------------------------------------------- 1 | # BBOX OGC API Features Service 2 | 3 | Asynchronous OGC API Features server implementation. 4 | 5 | Features: 6 | - [x] OGC API - Features - Part 1: Core 1.0 7 | - [ ] OGC API - Features - Part 2: Coordinate Reference Systems by Reference 1.0 8 | - [x] OpenAPI endpoint 9 | - [x] Builtin storage backends: PostGIS, GeoPackage 10 | - [x] Output formats: GeoJSON 11 | 12 | 13 | ## Configuration 14 | 15 | See [Documentation](https://www.bbox.earth/docs/feature-server/configuration/) for examples. 16 | 17 | ## Usage 18 | 19 | Run feature server with bbox.toml configuration: 20 | 21 | cargo run serve 22 | 23 | or with a custom configuration: 24 | 25 | cargo run -- --config=bbox-pg.toml serve 26 | 27 | Inspect collections: 28 | 29 | x-www-browser http://127.0.0.1:8080/collections 30 | 31 | Feature requests: 32 | 33 | curl -s http://127.0.0.1:8080/collections/populated_places/items | jq . 34 | 35 | curl -s http://127.0.0.1:8080/collections/populated_places_names/items/2 | jq . 36 | -------------------------------------------------------------------------------- /bbox-feature-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::config::{from_config_root_or_exit, ConfigError, DsPostgisCfg, NamedDatasourceCfg}; 2 | use bbox_core::service::ServiceConfig; 3 | use clap::ArgMatches; 4 | use serde::Deserialize; 5 | 6 | #[derive(Deserialize, Default, Debug)] 7 | #[serde(default)] 8 | pub struct FeatureServiceCfg { 9 | #[serde(rename = "datasource")] 10 | pub datasources: Vec, 11 | #[serde(rename = "collections")] 12 | pub auto_collections: CollectionsCfg, 13 | #[serde(rename = "collection")] 14 | pub collections: Vec, 15 | } 16 | 17 | /// Collections with auto-detection 18 | #[derive(Deserialize, Default, Debug)] 19 | #[serde(default, deny_unknown_fields)] 20 | pub struct CollectionsCfg { 21 | pub directory: Vec, 22 | pub postgis: Vec, 23 | } 24 | 25 | #[derive(Deserialize, Debug)] 26 | #[serde(deny_unknown_fields)] 27 | pub struct DsFiledirCfg { 28 | pub dir: String, 29 | } 30 | 31 | #[derive(Deserialize, Debug)] 32 | #[serde(deny_unknown_fields)] 33 | pub struct ConfiguredCollectionCfg { 34 | pub name: String, 35 | pub title: Option, 36 | pub description: Option, 37 | // extent: Option 38 | #[serde(flatten)] 39 | pub source: CollectionSourceCfg, 40 | } 41 | 42 | /// Collections with configuration 43 | #[derive(Deserialize, Debug)] 44 | #[serde(deny_unknown_fields)] 45 | pub enum CollectionSourceCfg { 46 | #[serde(rename = "postgis")] 47 | Postgis(PostgisCollectionCfg), 48 | #[serde(rename = "gpkg")] 49 | Gpkg(GpkgCollectionCfg), 50 | } 51 | 52 | #[derive(Deserialize, Default, Clone, Debug)] 53 | #[serde(deny_unknown_fields)] 54 | pub struct PostgisCollectionCfg { 55 | /// Name of datasource.postgis config (Default: first with matching type) 56 | pub datasource: Option, 57 | // maybe we should allow direct DS URLs? 58 | // pub url: Option, 59 | pub table_schema: Option, 60 | pub table_name: Option, 61 | /// Custom SQL query 62 | pub sql: Option, 63 | pub fid_field: Option, 64 | pub geometry_field: Option, 65 | //pub field_list: Option>, 66 | /// Field used for temporal filter expressions 67 | pub temporal_field: Option, 68 | /// Field used for temporal end filter expressions 69 | pub temporal_end_field: Option, 70 | /// Fields which can be used in filter expressions 71 | #[serde(default)] 72 | pub queryable_fields: Vec, 73 | } 74 | 75 | #[derive(Deserialize, Default, Debug)] 76 | #[serde(deny_unknown_fields)] 77 | pub struct GpkgCollectionCfg { 78 | /// Name of datasource.gpkg config (Default: first with matching type) 79 | pub datasource: Option, 80 | pub table_name: Option, 81 | /// Custom SQL query 82 | pub sql: Option, 83 | pub fid_field: Option, 84 | pub geometry_field: Option, 85 | //pub field_list: Option>, 86 | } 87 | 88 | impl ServiceConfig for FeatureServiceCfg { 89 | fn initialize(_cli: &ArgMatches) -> Result { 90 | let cfg: FeatureServiceCfg = from_config_root_or_exit(); 91 | Ok(cfg) 92 | } 93 | } 94 | 95 | impl CollectionsCfg { 96 | #[allow(dead_code)] 97 | pub fn from_path(path: &str) -> Self { 98 | let mut cfg = CollectionsCfg::default(); 99 | cfg.directory.push(DsFiledirCfg { 100 | dir: path.to_string(), 101 | }); 102 | cfg 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /bbox-feature-server/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error and Result types. 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | #[allow(clippy::enum_variant_names)] 6 | pub enum Error { 7 | #[error("Geometry format error")] 8 | GeometryFormatError, 9 | #[error("datasource setup error - {0}")] 10 | DatasourceSetupError(String), 11 | #[error("datasource `{0}` not found")] 12 | DatasourceNotFound(String), 13 | // Database errors 14 | #[error(transparent)] 15 | DbError(#[from] sqlx::Error), 16 | #[error("Query parameters error")] 17 | QueryParams, 18 | } 19 | 20 | pub type Result = std::result::Result; 21 | -------------------------------------------------------------------------------- /bbox-feature-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod datasource; 3 | mod endpoints; 4 | mod error; 5 | mod filter_params; 6 | mod inventory; 7 | pub mod service; 8 | 9 | pub use service::*; 10 | -------------------------------------------------------------------------------- /bbox-feature-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::service::run_service; 2 | use bbox_feature_server::service::FeatureService; 3 | 4 | fn main() { 5 | run_service::().unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /bbox-feature-server/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::config::FeatureServiceCfg; 2 | use crate::datasource::Datasources; 3 | use crate::inventory::Inventory; 4 | use async_trait::async_trait; 5 | use bbox_core::cli::{NoArgs, NoCommands}; 6 | use bbox_core::config::{error_exit, CoreServiceCfg}; 7 | use bbox_core::metrics::{no_metrics, NoMetrics}; 8 | use bbox_core::ogcapi::{ApiLink, CoreCollection}; 9 | use bbox_core::service::OgcApiService; 10 | 11 | #[derive(Clone)] 12 | pub struct FeatureService { 13 | pub inventory: Inventory, 14 | } 15 | #[async_trait] 16 | impl OgcApiService for FeatureService { 17 | type Config = FeatureServiceCfg; 18 | type CliCommands = NoCommands; 19 | type CliArgs = NoArgs; 20 | type Metrics = NoMetrics; 21 | 22 | async fn create(config: &Self::Config, core_cfg: &CoreServiceCfg) -> Self { 23 | let mut sources = Datasources::create(&config.datasources) 24 | .await 25 | .unwrap_or_else(error_exit); 26 | 27 | let mut inventory = 28 | Inventory::scan(&config.auto_collections, core_cfg.public_server_url()).await; 29 | for cfg in &config.collections { 30 | let collection = sources 31 | .setup_collection(cfg, inventory.href_prefix()) 32 | .await 33 | .unwrap_or_else(error_exit); 34 | inventory.add_collection(collection); 35 | } 36 | FeatureService { inventory } 37 | } 38 | fn conformance_classes(&self) -> Vec { 39 | let mut classes = vec![ 40 | "http://www.opengis.net/spec/ogcapi-common-2/1.0/conf/collections".to_string(), 41 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core".to_string(), 42 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson".to_string(), 43 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30".to_string(), 44 | // "http://www.opengis.net/spec/ogcapi-features-2/1.0/conf/crs".to_string(), 45 | ]; 46 | if cfg!(feature = "html") { 47 | classes.extend(vec![ 48 | "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/html".to_string(), 49 | ]); 50 | } 51 | classes 52 | } 53 | fn landing_page_links(&self, api_base: &str) -> Vec { 54 | vec![ApiLink { 55 | href: format!("{api_base}/collections"), 56 | rel: Some("data".to_string()), 57 | type_: Some("application/json".to_string()), 58 | title: Some("Information about the feature collections".to_string()), 59 | hreflang: None, 60 | length: None, 61 | }] 62 | } 63 | fn collections(&self) -> Vec { 64 | self.inventory.collections() 65 | } 66 | fn openapi_yaml(&self) -> Option<&str> { 67 | Some(include_str!("openapi.yaml")) 68 | } 69 | fn metrics(&self) -> &'static Self::Metrics { 70 | no_metrics() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /bbox-feature-server/templates/collection.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Collection{% endblock %} 4 | {% block content_title %}{{ collection.title }}{% endblock %} 5 | 6 | {% block content %} 7 |
8 | JSON
9 | 10 | {{ collection.description }} 11 | 12 | {% if collection.extent %} 13 | {% if collection.extent.spatial %}
{{collection.extent.spatial}} {% endif %} 14 | {% if collection.extent.temporal %}
{{collection.extent.temporal}}{% endif %} 15 | {% endif %} 16 |
17 | 18 | 24 | 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /bbox-feature-server/templates/feature.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Feature{% endblock %} 4 | {% block content_title %}{{ collection.title }} Item{% endblock %} 5 | 6 | {% block content %} 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for prop in feature.properties %} 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 | 30 |
propertyvalue
id{{ feature.id }}
{{prop}}{{feature.properties[prop]}}
31 | 32 | 38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /bbox-feature-server/templates/features.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Features{% endblock %} 4 | {% block content_title %}{{ collection.title }}{% endblock %} 5 | 6 | {% block content %} 7 | 10 | 11 | 12 | 13 | 14 | {% if features.features %} 15 | {% set first_feature = features.features | first %} 16 | 17 | {% for prop in first_feature.properties %} 18 | 19 | {% endfor %} 20 | {% endif %} 21 | 22 | 23 | 24 | {% for feature in features.features %} 25 | 26 | 27 | {% for prop in first_feature.properties %} 28 | 29 | {% endfor %} 30 | 31 | {% endfor %} 32 | 33 |
id{{prop}}
{{ feature.id }}{{feature.properties[prop]}}
34 | 35 | 41 | 42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /bbox-feature-server/templates/queryables.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Queryables{% endblock %} 4 | {% block content_title %}{{ queryables.title }} Item{% endblock %} 5 | 6 | {% block content %} 7 | 10 | 11 |

Queryables

12 |
    13 | {% for prop in queryables.properties %} 14 |
  • {{prop}} ({{queryables.properties[prop].type}})
  • 15 | {% endfor %} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /bbox-frontend/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-frontend" 3 | version = "0.6.2" 4 | readme = "README.md" 5 | description = "HTML frontend for BBOX services" 6 | categories = ["science::geo"] 7 | authors = ["Pirmin Kalberer "] 8 | 9 | edition.workspace = true 10 | license.workspace = true 11 | repository.workspace = true 12 | rust-version.workspace = true 13 | homepage.workspace = true 14 | 15 | [features] 16 | default = ["redoc", "openlayers", "maplibre", "proj"] 17 | all = ["default", "qwc2", "swaggerui"] 18 | map-server = ["bbox-map-server/inventory"] 19 | qwc2 = ["map-server"] 20 | redoc = [] 21 | swaggerui = [] 22 | openlayers = [] 23 | maplibre = [] 24 | proj = [] 25 | 26 | [dependencies] 27 | actix-web = { workspace = true } 28 | bbox-core = { path = "../bbox-core", version = "0.6.2", features=["html"] } 29 | bbox-map-server = { path = "../bbox-map-server", version = "0.6.2", optional = true } 30 | log = { workspace = true } 31 | minijinja = { workspace = true } 32 | once_cell = { workspace = true } 33 | rand = "0.8.5" 34 | rust-embed = { workspace = true } 35 | serde = { workspace = true } 36 | serde_json = { workspace = true } 37 | 38 | [dev-dependencies] 39 | 40 | [lib] 41 | path = "src/lib.rs" 42 | -------------------------------------------------------------------------------- /bbox-frontend/Makefile: -------------------------------------------------------------------------------- 1 | # Download embedded JS/CSS 2 | 3 | redoc_version = 2.0.0 4 | swagger_version = 3.24.2 5 | ol_version = 8.1.0 6 | maplibre_version = 3.5.2 7 | proj_version = 2.8.1 8 | 9 | all: static/redoc/redoc.standalone.js \ 10 | static/swagger/swagger-ui-bundle.js static/swagger/swagger-ui.css \ 11 | static/maplibre/maplibre-gl.js static/maplibre/maplibre-gl.css \ 12 | static/ol/ol.min.js static/ol/ol.min.css \ 13 | static/proj/proj4.min.js 14 | 15 | static/redoc/redoc.standalone.js: 16 | # License: https://cdn.jsdelivr.net/npm/redoc@2.0.0/bundles/redoc.standalone.js.LICENSE.txt 17 | wget -O $@ https://cdn.jsdelivr.net/npm/redoc@$(redoc_version)/bundles/redoc.standalone.js 18 | 19 | static/swagger/swagger-ui-bundle.js: 20 | wget -O $@ https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/$(swagger_version)/swagger-ui-bundle.js 21 | 22 | static/swagger/swagger-ui.css: 23 | wget -O $@ https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/$(swagger_version)/swagger-ui.css 24 | 25 | static/maplibre/maplibre-gl.js: 26 | wget -O $@ https://unpkg.com/maplibre-gl@$(maplibre_version)/dist/maplibre-gl.js 27 | 28 | static/maplibre/maplibre-gl.css: 29 | wget -O $@ https://unpkg.com/maplibre-gl@$(maplibre_version)/dist/maplibre-gl.css 30 | 31 | static/ol/ol.min.js: 32 | wget -O $@ https://cdn.jsdelivr.net/npm/ol@$(ol_version)/dist/ol.min.js 33 | 34 | static/ol/ol.min.css: 35 | wget -O $@ https://cdn.jsdelivr.net/npm/ol@$(ol_version)/ol.min.css 36 | 37 | static/proj/proj4.min.js: 38 | wget -O $@ https://cdn.jsdelivr.net/npm/proj4@$(proj_version)/dist/proj4.min.js 39 | 40 | 41 | QWC2_DIR = ../../../gis/qwc2-wmsapi 42 | qwc2_files = index.html themes.json config.json \ 43 | assets/forms/form.ui assets/css/qwc2.css assets/img/mapthumbs/default.jpg \ 44 | assets/img/mapthumbs/mapnik.jpg assets/img/app_icon_72.png assets/img/favicon.ico \ 45 | assets/img/app_icon.png assets/img/app_icon_144.png assets/img/app_icon_114.png \ 46 | assets/img/logo-mobile.svg assets/img/logo.svg assets/templates/legendprint.html 47 | 48 | qwc2: 49 | mkdir -p static/qwc2/translations static/qwc2/assets/forms static/qwc2/assets/css static/qwc2/assets/img/mapthumbs static/qwc2/assets/templates static/qwc2/js 50 | cp $(QWC2_DIR)/translations/data* static/qwc2/translations/ 51 | cp $(QWC2_DIR)/dist/QWC2App.js static/qwc2/js/ 52 | for f in $(qwc2_files); do cp $(QWC2_DIR)/$$f static/qwc2/$$f; done 53 | -------------------------------------------------------------------------------- /bbox-frontend/README.md: -------------------------------------------------------------------------------- 1 | # BBOX frontend 2 | 3 | HTML frontend for BBOX services. 4 | 5 | Features: 6 | - [ ] HTML landing page 7 | - [ ] Fonts and basic assets for MVT maps 8 | - [x] Redoc OpenAPI UI 9 | - [x] Swagger OpenAPI UI 10 | - Map viewers: 11 | - [ ] QWC2 map viewer 12 | - [ ] OpenLayers map viewer 13 | - [ ] MapLibre map viewer 14 | -------------------------------------------------------------------------------- /bbox-frontend/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod endpoints; 2 | #[cfg(feature = "map-server")] 3 | mod qwc2_config; 4 | 5 | #[cfg(feature = "map-server")] 6 | pub use crate::qwc2_config::themes_json; 7 | 8 | #[cfg(feature = "map-server")] 9 | pub use bbox_map_server::inventory::{Inventory as MapInventory, WmsService}; 10 | 11 | #[cfg(not(feature = "map-server"))] 12 | mod dummy_inventory { 13 | #[derive(serde::Serialize)] 14 | pub struct WmsService; 15 | 16 | #[derive(Default)] 17 | pub struct MapInventory { 18 | pub wms_services: Vec, 19 | } 20 | 21 | impl MapInventory { 22 | pub fn base_url(&self) -> &str { 23 | "/" 24 | } 25 | } 26 | 27 | pub async fn themes_json(_: &Vec, _: String, _: Option<&str>) -> String { 28 | unimplemented!() 29 | } 30 | } 31 | 32 | #[cfg(not(feature = "map-server"))] 33 | pub use dummy_inventory::*; 34 | -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/css/qwc2.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/css/qwc2.css -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/app_icon.png -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/app_icon_114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/app_icon_114.png -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/app_icon_144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/app_icon_144.png -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/app_icon_72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/app_icon_72.png -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/favicon.ico -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/mapthumbs/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/mapthumbs/default.jpg -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/img/mapthumbs/mapnik.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/bbox-frontend/static/qwc2/assets/img/mapthumbs/mapnik.jpg -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/assets/templates/legendprint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Legend 5 | 6 | 7 |

Map legend

8 |
9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /bbox-frontend/static/qwc2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | QGIS Web Client 2 9 | 10 | 11 | 12 | 13 | 14 | 15 | 52 | 55 | 56 | 57 |
58 |
59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /bbox-frontend/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Home{% endblock %} 4 | {% block content_title %}Home{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |
10 |

Map catalog

11 |
    12 | {% for entry in wms_services %} 13 |
  • {{ entry.wms_path }}: Capabilities 14 | 15 |
  • 16 | {% endfor %} 17 |
18 |
19 |
20 |
21 |
22 |

Map request examples

23 | 28 |
29 |
30 |
31 |
32 |

OGC API endpoints

33 | 38 |
39 |
40 |
41 |
42 |

Links

43 | 46 |
47 |
48 |
49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /bbox-frontend/templates/redoc.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}OpenAPI ReDoc UI{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /bbox-frontend/templates/scalar.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}OpenAPI Acalar UI{% endblock %} 4 | 5 | {% block content %} 6 | 9 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /bbox-frontend/templates/swaggerui.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}OpenAPI Swagger UI{% endblock %} 4 | 5 | {% block head %} 6 | 7 | 8 | 21 | {% endblock %} 22 | 23 | {% block onload %}render(){% endblock %} 24 | 25 | {% block content %} 26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /bbox-frontend/ui/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /bbox-frontend/ui/Makefile: -------------------------------------------------------------------------------- 1 | all: css 2 | 3 | css: 4 | npx tailwindcss -o ../static/frontend/bbox.css 5 | 6 | watch: 7 | npx tailwindcss -o ../static/frontend/bbox.css --watch 8 | 9 | dist: 10 | npx tailwindcss -o ../static/frontend/bbox.css --minify 11 | -------------------------------------------------------------------------------- /bbox-frontend/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "tailwindcss": "^3.3.2", 4 | "@tailwindcss/typography": "^0.5.9", 5 | "daisyui": "^3.1.9" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /bbox-frontend/ui/src/bbox.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /bbox-frontend/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/*.css', 5 | '../../bbox-core/templates/*.html', 6 | '../../bbox-feature-server/templates/*.html', 7 | '../templates/*.html' 8 | ], 9 | theme: { 10 | extend: {}, 11 | }, 12 | plugins: [require("@tailwindcss/typography"), require("daisyui")], 13 | daisyui: { 14 | themes: ["cupcake"], 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /bbox-map-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-map-server" 3 | version = "0.6.2" 4 | readme = "README.md" 5 | description = "BBOX map server" 6 | keywords = ["maps", "wms", "qgis", "mapserver"] 7 | categories = ["science::geo"] 8 | authors = ["Pirmin Kalberer "] 9 | 10 | edition.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | homepage.workspace = true 15 | 16 | [features] 17 | inventory = [] 18 | 19 | [dependencies] 20 | actix-web = { workspace = true } 21 | async-process = "1.0.0" 22 | async-trait = { workspace = true } 23 | awc = { workspace = true } 24 | bbox-core = { path = "../bbox-core", version = "0.6.2" } 25 | bufstream = "0.1.4" 26 | clap = { workspace = true } 27 | deadpool = { version = "0.9.2", default-features = false, features = ["managed", "rt_tokio_1"] } 28 | fastcgi-client = { version = "0.5.0", default-features = false } 29 | log = { workspace = true } 30 | num_cpus = { workspace = true } 31 | once_cell = { workspace = true } 32 | opentelemetry = { workspace = true } 33 | prometheus = { workspace = true } 34 | rand = "0.7.3" 35 | serde = { workspace = true } 36 | serde-xml-rs = "0.6.0" 37 | serde_json = { workspace = true } 38 | tempfile = { workspace = true } 39 | thiserror = { workspace = true } 40 | tokio = { version = "1", features = ["time"] } 41 | 42 | [dev-dependencies] 43 | 44 | [[bin]] 45 | name = "bbox-map-server" 46 | path = "src/main.rs" 47 | -------------------------------------------------------------------------------- /bbox-map-server/README.md: -------------------------------------------------------------------------------- 1 | # BBOX map server 2 | 3 | Asynchronous map server with FCGI backend. 4 | 5 | Features: 6 | - [x] OGC WMS 1.3 Server 7 | - [ ] OGC Map API Server 8 | - [x] FCGI backends: 9 | - [X] QGIS Server 10 | - [X] UNN Mapserver 11 | - [ ] Instrumentation data for WMS backends 12 | - [ ] Intelligent process dispatching (slow query detection) 13 | 14 | 15 | ## Configuration 16 | 17 | See [Documentation](https://www.bbox.earth/docs/map-server/configuration/) for examples. 18 | 19 | ## Usage 20 | 21 | cd .. 22 | cargo run 23 | 24 | Configuration: 25 | * `BBOX_MAPSERVER__NUM_FCGI_PROCESSES`: Number of FCGI processes (default: number of CPU cores) 26 | 27 | 28 | WMS request examples: 29 | 30 | curl -s 'http://127.0.0.1:8080/qgis/ne?SERVICE=WMS&REQUEST=GetCapabilities' 31 | 32 | curl -o /tmp/map.png 'http://127.0.0.1:8080/qgis/ne?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-20037508.34278924391,-5966981.031407224014,19750246.20310878009,17477263.06060761213&CRS=EPSG:900913&WIDTH=1399&HEIGHT=824&LAYERS=country&STYLES=&FORMAT=image/png;%20mode%3D8bit' 33 | 34 | curl -o /tmp/legend.png 'http://127.0.0.1:8080/qgis/ne?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphic&LAYER=country&FORMAT=image/png&STYLE=default&TRANSPARENT=true' 35 | 36 | curl -s 'http://127.0.0.1:8080/qgis/helloworld?SERVICE=WMS&REQUEST=GetProjectSettings' 37 | 38 | curl -o /tmp/print.pdf 'http://127.0.0.1:8080/qgis/helloworld' -X POST \ 39 | -d 'SERVICE=WMS&VERSION=1.3.0&REQUEST=GetPrint&FORMAT=pdf' \ 40 | -d 'TEMPLATE=Composer 1&DPI=300&CRS=EPSG:4326' \ 41 | -d 'map0:LAYERS=Country,Hello&map0:extent=-92.8913,-185.227,121.09,191.872' 42 | 43 | UMN Mapserver: 44 | 45 | curl -s 'http://127.0.0.1:8080/wms/map/ne?SERVICE=WMS&REQUEST=GetCapabilities' 46 | 47 | curl -o /tmp/map.png 'http://127.0.0.1:8080/wms/map/ne?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=40.83354209954528358,0.542981257600549938,49.84069885574058389,15.5221558872974672&CRS=epsg:4326&WIDTH=1372&HEIGHT=825&LAYERS=country&STYLES=&FORMAT=image%2Fpng%3B%20mode%3D8bit' 48 | 49 | 50 | WFS request examples: 51 | 52 | curl -s 'http://127.0.0.1:8080/qgis/ne?SERVICE=WFS&REQUEST=GetCapabilities' 53 | 54 | curl -s 'http://127.0.0.1:8080/qgis/ne?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=country&SRSNAME=EPSG:3857&BBOX=1059483.34824404888786376,5959680.16110791172832251,1061700.73825845750980079,5962445.67000228632241488,EPSG:3857' 55 | 56 | curl -s --data @wfsadd.xml 'http://127.0.0.1:8080/qgis/ne?SERVICE=WFS&REQUEST=Transaction' 57 | 58 | ## Development 59 | 60 | Documentation of main libriaries: 61 | * Actix: https://actix.rs/ 62 | * Async Process: https://docs.rs/async-process/ 63 | * QGIS Server plugins: https://docs.qgis.org/3.28/en/docs/server_manual/plugins.html 64 | 65 | Fast CGI: 66 | * Fast CGI: https://fastcgi-archives.github.io/FastCGI_Specification.html 67 | * CGI: https://tools.ietf.org/html/rfc3875 68 | -------------------------------------------------------------------------------- /bbox-map-server/bench/.gitignore: -------------------------------------------------------------------------------- 1 | *.csv 2 | *.jpg 3 | -------------------------------------------------------------------------------- /bbox-map-server/bench/Makefile: -------------------------------------------------------------------------------- 1 | # See mock-fcgi-wms/README.md for running server 2 | 3 | result_prefix = ./results/results_ 4 | path_file = paths.txt 5 | wrk_cmd = docker run --rm --user=$$UID --net=host -e SW="$(sw)" -e CSV_NAME=$(csv) -e CONNECTIONS=[CONN] \ 6 | -v $$PWD:/bench -v $$PWD/$(path_file):/paths.txt:ro \ 7 | williamyeh/wrk -H 'Accept-Encoding: gzip' -H 'Connection: keep-alive' 8 | duration = 10s 9 | 10 | quick_bench_apache: 11 | make bench_http duration=5s csv=http_quick.csv sw=apache-baseline path_file=paths_baseline.txt host=http://127.0.0.1 12 | make bench_http duration=5s csv=http_quick.csv sw=apache path_file=paths.txt host=http://127.0.0.1 13 | 14 | quick_bench: 15 | make bench_http duration=5s csv=http_quick.csv sw=bbox-baseline path_file=paths_baseline.txt host=http://127.0.0.1:8080 16 | sleep 10 17 | make bench_http duration=5s csv=http_quick.csv sw=bbox path_file=paths.txt host=http://127.0.0.1:8080 18 | 19 | bench_apache: 20 | make bench_http csv=http.csv sw=apache-baseline path_file=paths_baseline.txt host=http://127.0.0.1 21 | make bench_http csv=http.csv sw=apache path_file=paths.txt host=http://127.0.0.1 22 | 23 | bench: 24 | make bench_http csv=http.csv sw=bbox-baseline path_file=paths_baseline.txt host=http://127.0.0.1:8080 25 | sleep 20 26 | make bench_http csv=http.csv sw=bbox path_file=paths.txt host=http://127.0.0.1:8080 27 | 28 | bench_http: 29 | @# From first entry only title is displayed on plot 30 | $(wrk_cmd:[CONN]=1) --latency -d 1s -c 1 -t 1 -s /bench/httpbench.lua $(host) 31 | sleep 1 32 | $(wrk_cmd:[CONN]=1) --latency -d $(duration) -c 1 -t 1 -s /bench/httpbench.lua $(host) 33 | sleep 1 34 | $(wrk_cmd:[CONN]=4) --latency -d $(duration) -c 4 -t 4 -s /bench/httpbench.lua $(host) 35 | sleep 1 36 | $(wrk_cmd:[CONN]=32) --latency -d $(duration) -c 32 -t 4 -s /bench/httpbench.lua $(host) 37 | sleep $(duration) 38 | $(wrk_cmd:[CONN]=64) --latency -d $(duration) -c 64 -t 4 -s /bench/httpbench.lua $(host) 39 | sleep $(duration) 40 | $(wrk_cmd:[CONN]=128) --latency -d $(duration) -c 128 -t 4 -s /bench/httpbench.lua $(host) 41 | sleep $(duration) 42 | sleep $(duration) 43 | $(wrk_cmd:[CONN]=256) --latency -d $(duration) -c 256 -t 4 -s /bench/httpbench.lua $(host) 44 | @echo >>$(result_prefix)${csv} 45 | @echo >>$(result_prefix)${csv} 46 | @echo Statistics written to $(result_prefix)${csv} 47 | 48 | trace: 49 | make trace_bench duration=5s csv=http_quick.csv sw=bbox-baseline path_file=paths_baseline.txt host=http://127.0.0.1:8080 50 | sleep 3 51 | make trace_bench duration=5s csv=http_quick.csv sw=apache-baseline path_file=paths_baseline.txt host=http://127.0.0.1 52 | 53 | trace_bench: 54 | $(wrk_cmd:[CONN]=32) --latency -d $(duration) -c 32 -t 4 -s /bench/httpbench.lua $(host) 55 | @echo >>$(result_prefix)${csv} 56 | @echo >>$(result_prefix)${csv} 57 | -------------------------------------------------------------------------------- /bbox-map-server/bench/bbox-bench.toml: -------------------------------------------------------------------------------- 1 | [webserver] 2 | server_addr = "0.0.0.0:8080" 3 | worker_threads = 32 4 | 5 | [mapserver] 6 | num_fcgi_processes = 32 7 | search_projects = false 8 | 9 | [mapserver.mock_backend] 10 | # Enable FCGI mockup backend (for testing) 11 | # Environment variable prefix: BBOX_MAPSERVER__MOCK_BACKEND__ 12 | path = "/wms/mock" 13 | 14 | [metrics.prometheus] 15 | path = "/metrics" 16 | 17 | [metrics.jaeger] 18 | agent_endpoint = "localhost:6831" 19 | -------------------------------------------------------------------------------- /bbox-map-server/bench/paths_baseline.txt: -------------------------------------------------------------------------------- 1 | /wms/mock/helloworld?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-67.593,-176.248,83.621,182.893&CRS=EPSG:4326&WIDTH=515&HEIGHT=217&LAYERS=Country,Hello&STYLES=,&FORMAT=image/png;%20mode=8bit&DPI=96&TRANSPARENT=TRUE 2 | -------------------------------------------------------------------------------- /bbox-map-server/bench/results/Makefile: -------------------------------------------------------------------------------- 1 | all: http-quick.jpg http.jpg 2 | 3 | quick: http-quick.jpg 4 | 5 | http.jpg: http.g results_http.csv 6 | gnuplot http.g 7 | 8 | http-quick.jpg: http-quick.g results_http_quick.csv 9 | gnuplot http-quick.g 10 | 11 | clean: 12 | rm -f *.csv 13 | touch results_http_quick.csv results_http.csv 14 | -------------------------------------------------------------------------------- /bbox-map-server/bench/results/http-quick.g: -------------------------------------------------------------------------------- 1 | set terminal jpeg 2 | set output "http-quick.jpg" 3 | 4 | # Where to place the legend/key 5 | set key left top 6 | # Draw gridlines oriented on the y axis 7 | set grid y 8 | set ylabel "Requests/s" 9 | set logscale x 10 | set xlabel "Connections" 11 | set xtics (1,4,32,64,128,256) 12 | 13 | # Use CSV delimiter instead of spaces (default) 14 | set datafile separator ',' 15 | 16 | set title "WMS requests" 17 | plot for [i=0:*] "results_http_quick.csv" index i using 3:18 \ 18 | with linespoints title columnheader(2) 19 | 20 | set output "http-quick-errors.jpg" 21 | set title "Request errors" 22 | set ylabel "Read errors" 23 | set y2tics 24 | set y2label "Status errors" 25 | 26 | plot for [i=0:*] "results_http_quick.csv" index i \ 27 | using 3:20 with linespoints title columnheader(2), \ 28 | for [i=0:*] "results_http_quick.csv" index i \ 29 | using 3:22 with linespoints title "status" 30 | -------------------------------------------------------------------------------- /bbox-map-server/bench/results/http.g: -------------------------------------------------------------------------------- 1 | set terminal jpeg 2 | set output "http.jpg" 3 | 4 | # Where to place the legend/key 5 | set key left top 6 | # Draw gridlines oriented on the y axis 7 | set grid y 8 | set ylabel "Requests/s" 9 | set logscale x 10 | set xlabel "Connections" 11 | set xtics (1,4,32,64,128,256) 12 | 13 | # Use CSV delimiter instead of spaces (default) 14 | set datafile separator ',' 15 | 16 | # graph title 17 | set title "WMS Requests" 18 | plot for [i=0:*] "results_http.csv" index i using 3:18 \ 19 | with linespoints title columnheader(2) 20 | -------------------------------------------------------------------------------- /bbox-map-server/mock-fcgi-wms/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mock-fcgi-wms" 3 | version = "0.1.0" 4 | authors = ["Pirmin Kalberer "] 5 | 6 | edition.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | rust-version.workspace = true 10 | homepage.workspace = true 11 | 12 | [dependencies] 13 | fastcgi = "1.0.0" 14 | opentelemetry = { version = "0.17", default-features = false, features = ["trace"] } 15 | opentelemetry-jaeger = { version = "0.16", features = ["collector_client"] } 16 | 17 | [[bin]] 18 | name = "mock-fcgi-wms" 19 | path = "src/main.rs" 20 | -------------------------------------------------------------------------------- /bbox-map-server/mock-fcgi-wms/README.md: -------------------------------------------------------------------------------- 1 | # FCGI WMS test server 2 | 3 | 4 | ## Standalone usage 5 | 6 | cargo build 7 | 8 | spawn-fcgi ../../target/debug/mock-fcgi-wms -n -s /tmp/mock-fcgi 9 | cgi-fcgi -bind -connect /tmp/mock-fcgi 10 | 11 | spawn-fcgi ../../target/debug/mock-fcgi-wms -n -p 8099 12 | QUERY_STRING='' cgi-fcgi -bind -connect 127.0.0.1:8099 13 | 14 | 15 | ## Use with BBOX server 16 | 17 | cargo install --path . 18 | 19 | Run map server: 20 | 21 | cd .. 22 | RUST_LOG=info cargo run --release -- --config=./bench/bbox-bench.toml serve 23 | 24 | Test request (50ms): 25 | 26 | curl 'http://localhost:8080/wms/mock/helloworld?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-67.593,-176.248,83.621,182.893&CRS=EPSG:4326&WIDTH=515&HEIGHT=217&LAYERS=Country,Hello&STYLES=,&FORMAT=image/png;%20mode=8bit&DPI=96&TRANSPARENT=TRUE' 27 | 28 | Slow request (1s): 29 | 30 | curl 'http://localhost:8080/wms/mock/slow?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-67.593,-176.248,83.621,182.893&CRS=EPSG:4326&WIDTH=515&HEIGHT=217&LAYERS=Country,Hello&STYLES=,&FORMAT=image/png;%20mode=8bit&DPI=96&TRANSPARENT=TRUE' 31 | 32 | Crash request: 33 | 34 | curl 'http://localhost:8080/wms/mock/crash?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-67.593,-176.248,83.621,182.893&CRS=EPSG:4326&WIDTH=515&HEIGHT=217&LAYERS=Country,Hello&STYLES=,&FORMAT=image/png;%20mode=8bit&DPI=96&TRANSPARENT=TRUE' 35 | 36 | Request with sleep time parameter: 37 | 38 | curl 'http://localhost:8080/wms/mock/helloworld?t=500' 39 | 40 | Start instrumentation services: 41 | 42 | cd ../../docker/bbox 43 | docker compose up -d jaeger prometheus grafana 44 | 45 | Jaeger tracing: 46 | 47 | open http://localhost:16686/ 48 | 49 | Prometheus metrics: 50 | 51 | open http://localhost:9090/ 52 | -------------------------------------------------------------------------------- /bbox-map-server/mock-fcgi-wms/src/main.rs: -------------------------------------------------------------------------------- 1 | use opentelemetry::trace::{Span, TraceContextExt, Tracer}; 2 | use opentelemetry::{global, Context, Key}; 3 | use std::collections::HashMap; 4 | use std::io::Write; 5 | use std::path::Path; 6 | use std::{thread, time}; 7 | 8 | const PID_KEY: Key = Key::from_static_str("sourcepole.com/pid"); 9 | 10 | fn main() { 11 | global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new()); 12 | let tracer = opentelemetry_jaeger::new_pipeline() 13 | .with_service_name("fcgi") 14 | .install_simple() 15 | .expect("opentelemetry_jaeger::new_pipeline"); 16 | let pid = std::process::id(); 17 | let mut process_span = tracer.start("process"); 18 | process_span.set_attribute(PID_KEY.i64(pid as i64)); 19 | let process_span_cx = Context::current_with_span(process_span); 20 | fastcgi::run(move |mut req| { 21 | let mut req_span = tracer 22 | .span_builder("request") 23 | .start_with_context(&tracer, &process_span_cx); 24 | req_span.set_attribute(PID_KEY.i64(pid as i64)); 25 | let project = req 26 | .param("REQUEST_URI") 27 | .map(|p| { 28 | let p = p.split('?').next().expect("remove query part"); 29 | Path::new(&p) 30 | .file_stem() 31 | .expect("file_stem missing") 32 | .to_str() 33 | .expect("Invalid UTF-8") 34 | .to_string() 35 | }) 36 | .unwrap_or("".to_string()); 37 | 38 | let query = req.param("QUERY_STRING").unwrap_or("".to_string()); 39 | let mut query_map = HashMap::new(); 40 | for param in query.split('&') { 41 | let param_vec: Vec<&str> = param.split('=').collect(); 42 | query_map.insert(param_vec[0], *param_vec.get(1).unwrap_or(&"")); 43 | } 44 | 45 | let t = query_map 46 | .get("t") 47 | .map(|v| v.parse::().expect("time parameter invalid")); 48 | let response = match project.as_str() { 49 | "helloworld" => { 50 | thread::sleep(time::Duration::from_millis(t.unwrap_or(50))); 51 | format!("Hello, world! (pid={pid})") 52 | } 53 | "slow" => { 54 | thread::sleep(time::Duration::from_millis(t.unwrap_or(1000))); 55 | format!("Good morning! (pid={pid})") 56 | } 57 | "crash" => std::process::exit(0), 58 | _ => format!("Unknown project. Use e.g. 'helloworld', 'slow', 'crash'. (pid={pid})",), 59 | }; 60 | write!( 61 | &mut req.stdout(), 62 | "Content-Type: text/plain\n\n{}", 63 | &response 64 | ) 65 | .unwrap_or(()); 66 | req_span.end(); 67 | }); 68 | // global::shut_down_provider(); // sending remaining spans 69 | } 70 | -------------------------------------------------------------------------------- /bbox-map-server/qgis/plugins/ClearCapabilities/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This script initializes the plugin, making it known to QGIS. 4 | """ 5 | 6 | 7 | def serverClassFactory(server_iface): 8 | from . clear_capabilities import ClearCapabilities 9 | return ClearCapabilities(server_iface) 10 | -------------------------------------------------------------------------------- /bbox-map-server/qgis/plugins/ClearCapabilities/clear_capabilities.py: -------------------------------------------------------------------------------- 1 | from qgis.core import Qgis, QgsMessageLog, QgsProject 2 | from qgis.server import QgsServerFilter, QgsConfigCache, QgsServerSettings 3 | from qgis.PyQt.QtCore import QFileInfo 4 | import shutil 5 | import os 6 | 7 | 8 | class ClearCapabilitiesFilter(QgsServerFilter): 9 | """ QGIS Server ClearCapabilitiesFilter plugin. """ 10 | 11 | def __init__(self, server_iface): 12 | super(ClearCapabilitiesFilter, self).__init__(server_iface) 13 | self.projects = {} 14 | 15 | def requestReady(self): 16 | handler = self.serverInterface().requestHandler() 17 | params = handler.parameterMap() 18 | if params.get("CLEARCACHE") and params.get("MAP", ""): 19 | self.clearWmsCache() 20 | self.clearCache(params.get("MAP", "")) 21 | elif (params.get("SERVICE", "").upper() in ["WMS", "WMTS", "WFS"] 22 | and params.get("REQUEST", "").upper() in [ 23 | "GETPROJECTSETTINGS", "GETCAPABILITIES"] 24 | and params.get("MAP", "")): 25 | self.clearCacheIfModified(params.get("MAP", "")) 26 | 27 | def clearCacheIfModified(self, project): 28 | """ Checks the project timestamps and clears cache on update """ 29 | fi = QFileInfo(project) 30 | 31 | if fi.exists(): 32 | lm = fi.lastModified() 33 | 34 | if self.projects.get(project, lm) < lm: 35 | self.clearCache(project) 36 | QgsMessageLog.logMessage( 37 | "Cached cleared after update: {} [{}]".format( 38 | project, lm.toString()), 39 | "ClearCapabilities", Qgis.Warning) 40 | 41 | self.projects[project] = lm 42 | 43 | def clearWmsCache(self): 44 | settings = QgsServerSettings() 45 | settings.load() 46 | shutil.rmtree(os.path.join(settings.cacheDirectory(), 'data8'), 47 | ignore_errors=True) 48 | # QgsProject.instance().removeAllMapLayers() 49 | 50 | def clearCache(self, project): 51 | # QgsConfigCache.instance().removeEntry(project) 52 | # cache = QgsCapabilitiesCache() 53 | # cache.removeCapabilitiesDocument(project) 54 | self.serverInterface().removeConfigCacheEntry(project) 55 | 56 | QgsMessageLog.logMessage( 57 | "Cached cleared : {}".format(project), 58 | "ClearCapabilities", Qgis.Warning) 59 | 60 | 61 | class ClearCapabilities: 62 | """ Clear Capabilities plugin: this gets loaded by the server at 63 | start and creates the CLEARCACHE request. 64 | """ 65 | 66 | def __init__(self, server_iface): 67 | """Register the filter""" 68 | clear_capabilities = ClearCapabilitiesFilter(server_iface) 69 | server_iface.registerFilter(clear_capabilities) 70 | -------------------------------------------------------------------------------- /bbox-map-server/qgis/plugins/ClearCapabilities/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=ClearCapabilities 3 | server=True 4 | -------------------------------------------------------------------------------- /bbox-map-server/qgis/plugins/Instrumentation/__init__.py: -------------------------------------------------------------------------------- 1 | def serverClassFactory(serverIface): 2 | from .Instrumentation import Instrumentation 3 | return Instrumentation(serverIface) 4 | -------------------------------------------------------------------------------- /bbox-map-server/qgis/plugins/Instrumentation/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=Instrumentation 3 | qgisMinimumVersion=3.4 4 | description=Instrumentation plugin for QGIS server 5 | about=Instrumentation plugin for QGIS server 6 | version=0.1.0 7 | author=Pirmin Kalberer 8 | email=pka@sourcepole.ch 9 | 10 | server=True 11 | 12 | changelog= 13 | 14 | 15 | tags=server 16 | 17 | category=server 18 | 19 | experimental=True 20 | -------------------------------------------------------------------------------- /bbox-map-server/src/dispatcher/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::fcgi_process::FcgiClientPool; 2 | use std::sync::Mutex; 3 | 4 | mod rand; 5 | mod round_robin; 6 | mod wms_optimized; 7 | 8 | #[allow(dead_code)] 9 | enum DispatchMode { 10 | Rand, 11 | RoundRobin, 12 | WmsOptimized, 13 | } 14 | 15 | pub struct DispatchConfig { 16 | mode: DispatchMode, 17 | } 18 | 19 | impl DispatchConfig { 20 | pub fn new() -> Self { 21 | Self { 22 | mode: DispatchMode::WmsOptimized, 23 | } 24 | } 25 | } 26 | 27 | pub trait RequestDispatcher { 28 | #[allow(clippy::ptr_arg)] 29 | fn new(config: &DispatchConfig, pools: &Vec) -> Self; 30 | fn select(&mut self, query_str: &str) -> usize; 31 | } 32 | 33 | pub enum Dispatcher { 34 | Rand(Mutex), 35 | RoundRobin(Mutex), 36 | WmsOptimized(Mutex), 37 | } 38 | 39 | impl Dispatcher { 40 | pub fn new(config: &DispatchConfig, pools: &Vec) -> Self { 41 | match config.mode { 42 | DispatchMode::Rand => { 43 | Dispatcher::Rand(Mutex::new(rand::Dispatcher::new(config, pools))) 44 | } 45 | DispatchMode::RoundRobin => { 46 | Dispatcher::RoundRobin(Mutex::new(round_robin::Dispatcher::new(config, pools))) 47 | } 48 | DispatchMode::WmsOptimized => { 49 | Dispatcher::WmsOptimized(Mutex::new(wms_optimized::Dispatcher::new(config, pools))) 50 | } 51 | } 52 | } 53 | pub fn select(&self, query_str: &str) -> usize { 54 | match self { 55 | Dispatcher::Rand(dispatcher) => dispatcher.lock().unwrap().select(query_str), 56 | Dispatcher::RoundRobin(dispatcher) => dispatcher.lock().unwrap().select(query_str), 57 | Dispatcher::WmsOptimized(dispatcher) => dispatcher.lock().unwrap().select(query_str), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /bbox-map-server/src/dispatcher/rand.rs: -------------------------------------------------------------------------------- 1 | use crate::dispatcher::{DispatchConfig, RequestDispatcher}; 2 | use crate::fcgi_process::FcgiClientPool; 3 | use rand::Rng; 4 | 5 | pub struct Dispatcher { 6 | pool_size: usize, 7 | } 8 | 9 | impl RequestDispatcher for Dispatcher { 10 | fn new(_config: &DispatchConfig, pools: &Vec) -> Self { 11 | Self { 12 | pool_size: pools.len(), 13 | } 14 | } 15 | fn select(&mut self, _query_str: &str) -> usize { 16 | rand::thread_rng().gen_range(0, self.pool_size) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /bbox-map-server/src/dispatcher/round_robin.rs: -------------------------------------------------------------------------------- 1 | use crate::dispatcher::{DispatchConfig, RequestDispatcher}; 2 | use crate::fcgi_process::FcgiClientPool; 3 | 4 | pub struct Dispatcher { 5 | pool_size: usize, 6 | /// last selected pool 7 | pool_no: usize, 8 | } 9 | 10 | impl RequestDispatcher for Dispatcher { 11 | fn new(_config: &DispatchConfig, pools: &Vec) -> Self { 12 | Self { 13 | pool_size: pools.len(), 14 | pool_no: 0, 15 | } 16 | } 17 | fn select(&mut self, _query_str: &str) -> usize { 18 | self.pool_no = (self.pool_no + 1) % self.pool_size; 19 | self.pool_no 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /bbox-map-server/src/inventory.rs: -------------------------------------------------------------------------------- 1 | use crate::wms_capabilities::*; 2 | use serde::Serialize; 3 | use serde_xml_rs::from_reader; 4 | 5 | #[derive(Serialize, Clone, Default, Debug)] 6 | pub struct Inventory { 7 | pub wms_services: Vec, 8 | base_url: String, 9 | } 10 | 11 | #[derive(Serialize, Clone, Debug)] 12 | pub struct WmsService { 13 | pub id: String, 14 | /// WMS base path like `/qgis/ne` 15 | pub wms_path: String, 16 | pub cap_type: CapType, 17 | } 18 | 19 | #[derive(Serialize, Clone, PartialEq, Debug)] 20 | pub enum CapType { 21 | Ogc, 22 | Qgis, 23 | } 24 | 25 | impl Inventory { 26 | pub fn new(wms_services: Vec, public_server_url: Option) -> Self { 27 | let base_url = format!( 28 | "{}/", 29 | public_server_url 30 | .as_deref() 31 | .unwrap_or("") 32 | .trim_end_matches('/') 33 | ); 34 | Inventory { 35 | wms_services, 36 | base_url, 37 | } 38 | } 39 | 40 | pub fn base_url(&self) -> &str { 41 | &self.base_url 42 | } 43 | } 44 | 45 | impl WmsService { 46 | fn _project(&self) -> &str { 47 | self.wms_path 48 | .split('/') 49 | .next_back() 50 | .expect("invalid wms_path") 51 | } 52 | #[allow(dead_code)] 53 | fn cap_request(&self) -> &str { 54 | match self.cap_type { 55 | CapType::Ogc => "GetCapabilities", 56 | CapType::Qgis => "GetProjectSettings", 57 | } 58 | } 59 | #[allow(dead_code)] 60 | pub fn url(&self, base_url: &str) -> String { 61 | format!("{}{}", base_url, self.wms_path) 62 | } 63 | #[allow(dead_code)] 64 | pub async fn capabilities(&self, base_url: &str) -> WmsCapabilities { 65 | let client = awc::Client::default(); 66 | let mut response = client 67 | .get(format!( 68 | "{}?SERVICE=WMS&VERSION=1.3.0&REQUEST={}", 69 | &self.url(base_url), 70 | self.cap_request() 71 | )) 72 | .send() 73 | .await 74 | .expect("GetCapabilities"); 75 | 76 | let body = response.body().await.unwrap(); 77 | let cap: WmsCapabilities = from_reader(body.as_ref()).unwrap(); 78 | cap 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /bbox-map-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod dispatcher; 3 | pub mod endpoints; 4 | pub mod fcgi_process; 5 | pub mod inventory; 6 | pub mod metrics; 7 | pub mod service; 8 | pub mod wms_capabilities; 9 | mod wms_fcgi_backend; 10 | 11 | pub use service::*; 12 | -------------------------------------------------------------------------------- /bbox-map-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::service::run_service; 2 | use bbox_map_server::service::MapService; 3 | 4 | fn main() { 5 | run_service::().unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /bbox-processes-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-processes-server" 3 | version = "0.6.2" 4 | readme = "README.md" 5 | description = "BBOX OGC API Processes Service" 6 | keywords = ["processes", "workflows"] 7 | categories = ["science::geo"] 8 | authors = ["Pirmin Kalberer "] 9 | 10 | edition.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | homepage.workspace = true 15 | 16 | [dependencies] 17 | actix-files = "0.6.0" 18 | actix-web = { workspace = true } 19 | async-trait = { workspace = true } 20 | awc = { workspace = true } 21 | bbox-core = { path = "../bbox-core", version = "0.6.2" } 22 | chrono = { workspace = true, features = ["serde"] } 23 | clap = { workspace = true } 24 | log = { workspace = true } 25 | serde = { workspace = true } 26 | serde_json = { workspace = true } 27 | swagger = { version = "6.1", features = ["serdejson"] } 28 | thiserror = { workspace = true } 29 | tokio = { version = "1", features = ["time"] } 30 | 31 | [[bin]] 32 | name = "bbox-processes-server" 33 | path = "src/main.rs" 34 | -------------------------------------------------------------------------------- /bbox-processes-server/README.md: -------------------------------------------------------------------------------- 1 | # BBOX Processes Service 2 | 3 | The OGC API - Processes standard specifies an interface for executing computational tasks. 4 | 5 | Overview: https://ogcapi.ogc.org/processes/ 6 | 7 | Features: 8 | - [ ] OGC API - Processes - Part 1: Core 9 | - [x] Support synchronous and asynchronous process execution 10 | - [x] OpenAPI endpoint 11 | - [x] Multiple backend engines 12 | - [x] [Dagster](https://dagster.io/) 13 | - [ ] [Windmill](https://www.windmill.dev/) 14 | 15 | 16 | ## Configuration 17 | 18 | See [Documentation](https://www.bbox.earth/docs/processes-server/configuration/) for examples. 19 | 20 | ## Usage 21 | 22 | ### Request examples 23 | 24 | List processes: 25 | 26 | curl 'http://localhost:8080/processes' 27 | 28 | Execute process: 29 | 30 | curl --header "Content-Type: application/json" \ 31 | --request POST \ 32 | --data '{"inputs": {"ops": {"pos_info_query": {"inputs": {"pos_x": 2607545, "pos_y": 1171421}}}}}' \ 33 | http://localhost:8080/processes/pos_info/execution 34 | 35 | Execute process asynchronous: 36 | 37 | curl --header "Content-Type: application/json" \ 38 | --header "Prefer: respond-async" \ 39 | --request POST \ 40 | --data '{"inputs": {"ops": {"export_fpds2": {"inputs": {"fixpunkte": ["12575280", "12575100"], "in_bearbeitung": false }}}}}' \ 41 | http://localhost:8080/processes/export_fpds2_to_csv/execution 42 | 43 | JOBID=386f6c55-d718-4160-b4df-afc5ad5c7a73 44 | 45 | Get job status: 46 | 47 | curl http://localhost:8080/jobs/$JOBID 48 | 49 | Return result of a job: 50 | 51 | curl http://localhost:8080/jobs/$JOBID/results 52 | -------------------------------------------------------------------------------- /bbox-processes-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::config::{config_error_exit, ConfigError}; 2 | use bbox_core::service::ServiceConfig; 3 | use clap::ArgMatches; 4 | use serde::Deserialize; 5 | 6 | #[derive(Deserialize, Default, Debug)] 7 | #[serde(default, deny_unknown_fields)] 8 | pub struct ProcessesServiceCfg { 9 | pub dagster_backend: Option, 10 | } 11 | 12 | /// Dagster backend configuration 13 | #[derive(Deserialize, Clone, Debug)] 14 | #[serde(deny_unknown_fields)] 15 | pub struct DagsterBackendCfg { 16 | /// GraphQL URL (e.g. `http://localhost:3000/graphql`) 17 | pub graphql_url: String, 18 | /// Dagster repository (e.g. `fpds2_processing_repository`) 19 | pub repository_name: String, 20 | /// Dagster repository location (e.g. `fpds2_processing.repos`) 21 | pub repository_location_name: String, 22 | /// Backend request timeout (ms) (Default: 10s) 23 | pub request_timeout: Option, 24 | } 25 | 26 | impl ServiceConfig for ProcessesServiceCfg { 27 | fn initialize(_cli: &ArgMatches) -> Result { 28 | let cfg = ProcessesServiceCfg::from_config(); 29 | Ok(cfg) 30 | } 31 | } 32 | 33 | impl ProcessesServiceCfg { 34 | pub fn from_config() -> Self { 35 | let config = bbox_core::config::app_config(); 36 | if config.find_value("processes").is_ok() { 37 | let cfg: Self = config 38 | .extract_inner("processes") 39 | .map_err(config_error_exit) 40 | .unwrap(); 41 | if !cfg.has_backend() { 42 | config_error_exit("Processing backend configuration missing"); 43 | } 44 | cfg 45 | } else { 46 | Default::default() 47 | } 48 | } 49 | pub fn has_backend(&self) -> bool { 50 | self.dagster_backend.is_some() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bbox-processes-server/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error and Result types. 2 | use crate::models; 3 | use thiserror::Error; 4 | 5 | #[derive(Error, Debug)] 6 | #[allow(clippy::enum_variant_names)] 7 | pub enum Error { 8 | // Backend errors 9 | #[error(transparent)] 10 | BackendSendError(#[from] awc::error::SendRequestError), 11 | #[error(transparent)] 12 | BackendResponseError(#[from] awc::error::JsonPayloadError), 13 | #[error(transparent)] 14 | BackendJsonError(#[from] serde_json::Error), 15 | #[error("Backend execution error - {0}")] 16 | BackendExecutionError(String), 17 | // General 18 | #[error("I/O error")] 19 | IoError(#[from] std::io::Error), 20 | // OGC Error responses 21 | #[error("Resource not found - `{0}`")] 22 | NotFound(String), 23 | } 24 | 25 | pub type Result = std::result::Result; 26 | 27 | // Convert to OGC exception response 28 | impl From for models::Exception { 29 | fn from(error: Error) -> Self { 30 | match error { 31 | Error::NotFound(type_) => models::Exception::new(type_), 32 | e => models::Exception { 33 | type_: "https://datatracker.ietf.org/doc/rfc7807/".to_string(), // TODO: Application error URL 34 | title: None, 35 | detail: Some(e.to_string()), 36 | status: None, 37 | instance: None, 38 | }, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bbox-processes-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod dagster; 3 | mod endpoints; 4 | mod error; 5 | mod models; 6 | pub mod service; 7 | 8 | pub use service::*; 9 | -------------------------------------------------------------------------------- /bbox-processes-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::service::run_service; 2 | use bbox_processes_server::service::ProcessesService; 3 | 4 | fn main() { 5 | run_service::().unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /bbox-processes-server/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::config::ProcessesServiceCfg; 2 | use crate::dagster::DagsterBackend; 3 | use async_trait::async_trait; 4 | use bbox_core::cli::{NoArgs, NoCommands}; 5 | use bbox_core::config::CoreServiceCfg; 6 | use bbox_core::metrics::{no_metrics, NoMetrics}; 7 | use bbox_core::ogcapi::ApiLink; 8 | use bbox_core::service::OgcApiService; 9 | 10 | use log::info; 11 | 12 | #[derive(Clone, Default)] 13 | pub struct ProcessesService { 14 | pub backend: Option, 15 | pub enabled: bool, 16 | } 17 | 18 | #[async_trait] 19 | impl OgcApiService for ProcessesService { 20 | type Config = ProcessesServiceCfg; 21 | type CliCommands = NoCommands; 22 | type CliArgs = NoArgs; 23 | type Metrics = NoMetrics; 24 | 25 | async fn create(config: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self { 26 | let enabled = if config.has_backend() { 27 | true 28 | } else { 29 | info!("Processing backend configuration missing - service disabled"); 30 | false 31 | }; 32 | let backend = config 33 | .dagster_backend 34 | .clone() 35 | .map(|_cfg| DagsterBackend::new()); 36 | ProcessesService { backend, enabled } 37 | } 38 | fn conformance_classes(&self) -> Vec { 39 | vec![ 40 | "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core".to_string(), 41 | "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/json".to_string(), 42 | // |Core|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/core| 43 | // |OGC Process Description|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/ogc-process-description| 44 | // |JSON|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/json| 45 | // |HTML|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/html| 46 | // |OpenAPI Specification 3.0|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/oas30| 47 | // |Job list|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/job-list| 48 | // |Callback|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/callback| 49 | // |Dismiss|http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/dismiss| 50 | "http://www.opengis.net/spec/ogcapi-processes-1/1.0/conf/oas30".to_string(), 51 | ] 52 | } 53 | fn landing_page_links(&self, api_base: &str) -> Vec { 54 | if self.enabled { 55 | vec![ApiLink { 56 | href: format!("{api_base}/processes"), 57 | rel: Some("processes".to_string()), 58 | type_: Some("application/json".to_string()), 59 | title: Some("OGC API processes list".to_string()), 60 | hreflang: None, 61 | length: None, 62 | }] 63 | } else { 64 | vec![] 65 | } 66 | } 67 | fn openapi_yaml(&self) -> Option<&str> { 68 | Some(include_str!("openapi.yaml")) 69 | } 70 | fn metrics(&self) -> &'static Self::Metrics { 71 | no_metrics() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /bbox-routing-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-routing-server" 3 | version = "0.1.0" 4 | readme = "README.md" 5 | description = "BBOX Routing Service" 6 | keywords = ["routing"] 7 | categories = ["science::geo"] 8 | authors = ["Pirmin Kalberer "] 9 | 10 | edition.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | homepage.workspace = true 15 | 16 | [dependencies] 17 | actix-web = { workspace = true } 18 | async-trait = { workspace = true } 19 | bbox-core = { path = "../bbox-core", version = "0.6.0" } 20 | bincode = "1.3.3" 21 | clap = { workspace = true } 22 | fast_paths = "0.2.0" 23 | #fast_paths = { git = "https://github.com/pka/fast_paths", branch="edges-access" } 24 | figment = "0.10.6" 25 | futures = { workspace = true } 26 | geo = "0.27.0" 27 | geo-types = "0.7.6" 28 | geojson = "0.24.1" 29 | geozero = { workspace = true, features = [ "with-gpkg", "with-postgis-sqlx" ] } 30 | log = { workspace = true } 31 | polyline = "0.11.0" 32 | rstar = "0.11.0" 33 | serde = { workspace = true } 34 | serde_json = { workspace = true } 35 | sqlx = { workspace = true } 36 | thiserror = { workspace = true } 37 | 38 | [dev-dependencies] 39 | tokio = { workspace = true, features = ["full"] } 40 | 41 | [[bin]] 42 | name = "bbox-routing-server" 43 | path = "src/main.rs" 44 | -------------------------------------------------------------------------------- /bbox-routing-server/README.md: -------------------------------------------------------------------------------- 1 | # BBOX Routing Service 2 | 3 | Routing services with Contraction Hierarchy. 4 | 5 | Features: 6 | - [ ] OGC API - Routes - Part 1: Core 7 | - [x] Multiple search APIs 8 | - [x] OGC API route requests 9 | - [x] Basic from/to requests 10 | - [x] Valhalla API compatible requests 11 | - [x] Builtin storage backends: PostGIS, GeoPackage 12 | - [ ] Extract routing graphs from OSM planet files 13 | 14 | 15 | ## Configuration 16 | 17 | See [Documentation](https://www.bbox.earth/docs/routing-server/configuration/) for examples. 18 | 19 | ## Usage 20 | 21 | Request examples: 22 | 23 | ``` 24 | curl -s -X 'POST' \ 25 | 'http://localhost:8080/routes?mode=sync' \ 26 | -H 'accept: application/geo+json' \ 27 | -H 'Content-Type: application/json' \ 28 | -d '{ 29 | "name": "Route from A to B", 30 | "waypoints": { 31 | "type": "MultiPoint", 32 | "coordinates": [ 33 | [9.35213353, 47.0935012], 34 | [9.3422712, 47.1011887] 35 | ] 36 | }, 37 | "preference": "fastest", 38 | "dataset": "OSM" 39 | }' 40 | ``` 41 | 42 | Basic from/to request: 43 | 44 | curl -s 'http://localhost:8080/routes/basic?profile=railway&from_pos=9.35213353,47.0935012&to_pos=9.3422712,47.1011887' 45 | 46 | Zurich - Munich: 47 | 48 | curl -s 'http://localhost:8080/routes/basic?profile=railway&from_pos=8.53636,47.37726&to_pos=11.56096,48.14019' 49 | 50 | 51 | Valhalla endpoint (e.g. for Valhalla QGIS Plugin): 52 | 53 | Base URL: http://localhost:8080/routes/valhalla 54 | -------------------------------------------------------------------------------- /bbox-routing-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::config::{from_config_opt_or_exit, ConfigError, DsPostgisCfg}; 2 | use bbox_core::service::ServiceConfig; 3 | use clap::ArgMatches; 4 | use serde::Deserialize; 5 | 6 | #[derive(Deserialize, Default, Debug)] 7 | #[serde(default, deny_unknown_fields)] 8 | pub struct RoutingServiceCfg { 9 | pub service: Vec, 10 | } 11 | 12 | /// Routing service configuration 13 | #[derive(Deserialize, Clone, Default, Debug)] 14 | #[serde(deny_unknown_fields)] 15 | pub struct RoutingCfg { 16 | pub profile: Option, 17 | /// Node search distance 18 | pub search_dist: Option, 19 | pub gpkg: String, 20 | pub postgis: Option, 21 | /// Edge table 22 | pub table: String, 23 | /// Node/Vertices table 24 | pub node_table: Option, 25 | /// Geometry column 26 | pub geom: String, 27 | /// Node ID column in node table 28 | pub node_id: Option, 29 | /// Cost column (default: geodesic line length) 30 | pub cost: Option, 31 | /// Column with source node ID 32 | pub node_src: Option, 33 | /// Column with destination (target) node ID 34 | pub node_dst: Option, 35 | } 36 | 37 | impl ServiceConfig for RoutingServiceCfg { 38 | fn initialize(_cli: &ArgMatches) -> Result { 39 | let cfg = from_config_opt_or_exit("routing").unwrap_or_default(); 40 | Ok(cfg) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /bbox-routing-server/src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error and Result types. 2 | use thiserror::Error; 3 | 4 | #[derive(Error, Debug)] 5 | #[allow(clippy::enum_variant_names)] 6 | pub enum Error { 7 | #[error("No node found")] 8 | NodeNotFound, 9 | #[error("No route found")] 10 | NoRouteFound, 11 | // Requests 12 | #[error("Argument error `{0}`")] 13 | ArgumentError(String), 14 | // General 15 | #[error(transparent)] 16 | IoError(#[from] std::io::Error), 17 | #[error("Bincode error")] 18 | BincodeError(#[from] bincode::Error), 19 | #[error(transparent)] 20 | DbError(#[from] sqlx::Error), 21 | } 22 | 23 | pub type Result = std::result::Result; 24 | -------------------------------------------------------------------------------- /bbox-routing-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | mod ds; 3 | mod endpoints; 4 | mod engine; 5 | mod error; 6 | pub mod service; 7 | 8 | pub use service::*; 9 | -------------------------------------------------------------------------------- /bbox-routing-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use bbox_core::service::run_service; 2 | use bbox_routing_server::service::RoutingService; 3 | 4 | fn main() { 5 | run_service::().unwrap(); 6 | } 7 | -------------------------------------------------------------------------------- /bbox-routing-server/src/service.rs: -------------------------------------------------------------------------------- 1 | use crate::config::RoutingServiceCfg; 2 | use crate::engine::Router; 3 | use async_trait::async_trait; 4 | use bbox_core::cli::{NoArgs, NoCommands}; 5 | use bbox_core::config::{config_error_exit, CoreServiceCfg}; 6 | use bbox_core::metrics::{no_metrics, NoMetrics}; 7 | use bbox_core::ogcapi::ApiLink; 8 | use bbox_core::service::OgcApiService; 9 | use log::warn; 10 | 11 | #[derive(Clone)] 12 | pub struct RoutingService { 13 | pub router: Option, 14 | } 15 | 16 | #[async_trait] 17 | impl OgcApiService for RoutingService { 18 | type Config = RoutingServiceCfg; 19 | type CliCommands = NoCommands; 20 | type CliArgs = NoArgs; 21 | type Metrics = NoMetrics; 22 | 23 | async fn create(config: &Self::Config, _core_cfg: &CoreServiceCfg) -> Self { 24 | let router = match config.service.len() { 25 | 1 => { 26 | let service = &config.service[0]; 27 | Some(Router::from_config(service).await.unwrap()) 28 | } 29 | 0 => { 30 | warn!("No routing config available"); 31 | None 32 | } 33 | _ => { 34 | config_error_exit(figment::Error::from( 35 | "Currently only one routing service supported".to_string(), 36 | )); 37 | None 38 | } 39 | }; 40 | RoutingService { router } 41 | } 42 | fn conformance_classes(&self) -> Vec { 43 | vec![ 44 | // Core 45 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/core".to_string(), 46 | /* 47 | // Manage routes 48 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/manage-routes".to_string(), 49 | // Modes 50 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/mode".to_string(), 51 | // Intermediate waypoints 52 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/intermediate-waypoints".to_string(), 53 | // Height restrictions 54 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/height".to_string(), 55 | // Weight restrictions 56 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/weight".to_string(), 57 | // Obstacles 58 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/obstacles".to_string(), 59 | // Temporal constraints 60 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/time".to_string(), 61 | */ 62 | // OpenAPI Specification 63 | "http://www.opengis.net/spec/ogcapi-routes-1/1.0.0-draft.1/conf/oas30".to_string(), 64 | ] 65 | } 66 | fn landing_page_links(&self, api_base: &str) -> Vec { 67 | vec![ApiLink { 68 | href: format!("{api_base}/routes"), 69 | rel: Some("routes".to_string()), 70 | type_: Some("application/json".to_string()), 71 | title: Some("OGC API routes".to_string()), 72 | hreflang: None, 73 | length: None, 74 | }] 75 | } 76 | fn openapi_yaml(&self) -> Option<&str> { 77 | Some(include_str!("openapi.yaml")) 78 | } 79 | fn metrics(&self) -> &'static Self::Metrics { 80 | no_metrics() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /bbox-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bbox-server" 3 | version = "0.6.2" 4 | readme = "README.md" 5 | description = "BBOX OGC API Services" 6 | keywords = ["maps", "tiles", "postgis"] 7 | categories = ["science::geo"] 8 | authors = ["Pirmin Kalberer "] 9 | 10 | edition.workspace = true 11 | license.workspace = true 12 | repository.workspace = true 13 | rust-version.workspace = true 14 | homepage.workspace = true 15 | 16 | [features] 17 | default = ["feature-server", "asset-server", "map-server", "processes-server", "tile-server", "frontend"] 18 | feature-server = ["bbox-feature-server"] 19 | asset-server = ["bbox-asset-server"] 20 | map-server = ["bbox-map-server", "bbox-tile-server?/map-server", "bbox-frontend?/map-server"] 21 | processes-server = ["bbox-processes-server"] 22 | routing-server = ["bbox-routing-server"] 23 | tile-server = ["bbox-tile-server"] 24 | frontend = ["bbox-frontend", "bbox-feature-server?/html"] 25 | qwc2 = ["bbox-frontend?/qwc2"] 26 | 27 | [dependencies] 28 | actix-service = "2.0.2" 29 | actix-web = { workspace = true } 30 | async-trait = { workspace = true } 31 | bbox-asset-server = { path = "../bbox-asset-server", version = "0.6.2", optional = true } 32 | bbox-core = { path = "../bbox-core", version = "0.6.2" } 33 | bbox-feature-server = { path = "../bbox-feature-server", version = "0.6.2", optional = true } 34 | bbox-frontend = { path = "../bbox-frontend", version = "0.6.2", optional = true } 35 | bbox-map-server = { path = "../bbox-map-server", version = "0.6.2", optional = true } 36 | bbox-processes-server = { path = "../bbox-processes-server", version = "0.6.2", optional = true } 37 | bbox-routing-server = { path = "../bbox-routing-server", version = "0.1.0", optional = true } 38 | bbox-tile-server = { path = "../bbox-tile-server", version = "0.6.2", default-features = false, optional = true } 39 | clap = { workspace = true } 40 | log = { workspace = true } 41 | minijinja = { workspace = true } 42 | once_cell = { workspace = true } 43 | open = "5.3.0" 44 | rust-embed = { workspace = true } 45 | serde = { workspace = true } 46 | serde_json = { workspace = true } 47 | 48 | [[bin]] 49 | name = "bbox-server" 50 | path = "src/main.rs" 51 | -------------------------------------------------------------------------------- /bbox-tile-server/.gitignore: -------------------------------------------------------------------------------- 1 | /s3data 2 | -------------------------------------------------------------------------------- /bbox-tile-server/bbox-mvtbench.toml: -------------------------------------------------------------------------------- 1 | [webserver] 2 | loglevel = "Warn" 3 | 4 | [mapserver] 5 | num_fcgi_processes = 0 6 | 7 | [[datasource]] 8 | name = "pg" 9 | [datasource.postgis] 10 | #url = "postgresql://mvtbench:mvtbench@mvtbenchdb/mvtbench" 11 | url = "postgresql://mvtbench:mvtbench@127.0.0.1:5439/mvtbench" 12 | 13 | [[tileset]] 14 | name = "ne_countries" 15 | # No tile cache for HTTP tile server testing. Enabled for seeding tests via CLI options. 16 | [[tileset.tms]] 17 | id = "WebMercatorQuad" 18 | minzoom = 0 19 | maxzoom = 6 20 | [tileset.postgis] 21 | attribution = "Natural Earth v4" 22 | postgis2 = false 23 | [tileset.postgis.extent] 24 | minx = -179.97277 25 | miny = -83.05457 26 | maxx = 179.99366 27 | maxy = 83.23559 28 | 29 | #[tileset.postgis.diagnostics] 30 | #reference_size = 800_000 31 | 32 | [[tileset.postgis.layer]] 33 | name = "country" 34 | geometry_field = "wkb_geometry" 35 | geometry_type = "MULTIPOLYGON" 36 | # simplify = true 37 | # tolerance = "!pixel_width!/2" 38 | buffer_size = 3 39 | 40 | [[tileset.postgis.layer.query]] 41 | minzoom = 0 42 | sql = "SELECT wkb_geometry, adm0_a3, mapcolor7 FROM ne_10m_admin_0_countries WHERE min_zoom::integer <= !zoom!" 43 | 44 | [[tileset.postgis.layer]] 45 | name = "country-name" 46 | geometry_field = "wkb_geometry" 47 | geometry_type = "POINT" 48 | buffer_size = 0 49 | 50 | [[tileset.postgis.layer.query]] 51 | minzoom = 0 52 | sql = "SELECT wkb_geometry, abbrev, name FROM ne_10m_admin_0_country_points" 53 | 54 | [[tileset.postgis.layer]] 55 | name = "geo-lines" 56 | geometry_field = "wkb_geometry" 57 | geometry_type = "MULTILINESTRING" 58 | buffer_size = 0 59 | 60 | [[tileset.postgis.layer.query]] 61 | minzoom = 1 62 | maxzoom = 4 63 | sql = "SELECT wkb_geometry, name FROM ne_50m_geographic_lines" 64 | 65 | [[tileset.postgis.layer.query]] 66 | minzoom = 5 67 | sql = "SELECT wkb_geometry, name FROM ne_10m_geographic_lines" 68 | 69 | [[tileset.postgis.layer]] 70 | name = "land-border-country" 71 | geometry_field = "wkb_geometry" 72 | geometry_type = "MULTILINESTRING" 73 | fid_field = "ogc_fid" 74 | # simplify = true 75 | # tolerance = "!pixel_width!/2" 76 | buffer_size = 0 77 | 78 | [[tileset.postgis.layer.query]] 79 | minzoom = 0 80 | sql = "SELECT wkb_geometry FROM ne_10m_admin_0_boundary_lines_land WHERE min_zoom::integer <= !zoom!" 81 | 82 | [[tileset.postgis.layer]] 83 | name = "state" 84 | geometry_field = "wkb_geometry" 85 | geometry_type = "MULTILINESTRING" 86 | # simplify = true 87 | # tolerance = "!pixel_width!/2" 88 | buffer_size = 0 89 | 90 | [[tileset.postgis.layer.query]] 91 | minzoom = 0 92 | sql = "SELECT wkb_geometry, adm0_a3 FROM ne_10m_admin_1_states_provinces_lines WHERE min_zoom::integer <= !zoom!" 93 | -------------------------------------------------------------------------------- /bbox-tile-server/performance.md: -------------------------------------------------------------------------------- 1 | # BBOX tile server performance measurements 2 | 3 | ## Local S3 test setup 4 | 5 | Run MinIO: 6 | 7 | just start-minio 8 | 9 | Setup Bucket: 10 | 11 | just setup-minio 12 | 13 | Access MinIO Console: http://localhost:9001 14 | 15 | Stop MinIO: 16 | 17 | just stop-minio 18 | 19 | 20 | ## Seeding tests 21 | 22 | Relase Build: 23 | 24 | cargo build --release 25 | 26 | Local file seeding test: 27 | 28 | ../target/release/bbox-tile-server seed --tileset=gebco --tile-path=/tmp/tiles --maxzoom=2 29 | 30 | or mvtbench: 31 | 32 | just seed-s3 33 | 34 | Set S3 env vars: 35 | 36 | export S3_ENDPOINT_URL="http://localhost:9000" 37 | export AWS_ACCESS_KEY_ID=miniostorage 38 | export AWS_SECRET_ACCESS_KEY=miniostorage 39 | 40 | Run: 41 | 42 | ../target/release/bbox-tile-server seed --tileset=ne_extracts --s3-path=s3://tiles --maxzoom=5 43 | 44 | 45 | ## MVT benchmark 46 | 47 | https://github.com/pka/mvt-benchmark 48 | 49 | Start DB: 50 | 51 | just start-db 52 | 53 | Tile seeding benchmarks: 54 | 55 | just seed-bench-files 56 | just seed-bench-mbtiles 57 | just seed-bench-pmtiles 58 | 59 | 60 | ## S3 upload benchmarks 61 | 62 | ### s3cmd 63 | 64 | time s3cmd sync ~/code/gis/vogeldatenbank/tiles/ s3://tiles 65 | 66 | Done. Uploaded 448854168 bytes in 95.8 seconds, 4.47 MB/s. 67 | 68 | -> real 1m38.220s 69 | 70 | ### s5cmd 71 | 72 | export S3_ENDPOINT_URL="http://localhost:9000" 73 | 74 | time s5cmd cp /home/pi/code/gis/vogeldatenbank/tiles/ s3://tiles 75 | 76 | -> real 0m15.807s 77 | 78 | time s5cmd rm s3://tiles/* 79 | 80 | -> real 0m3.856s 81 | 82 | ### bbox-tile-server 83 | 84 | Initial sequential implementation: 85 | 86 | export S3_ENDPOINT_URL="http://localhost:9000" 87 | 88 | cargo build --release 89 | time ../target/release/bbox-tile-server upload --srcdir=/home/pi/code/gis/vogeldatenbank/tiles/ --s3-path=s3://tiles 90 | 91 | -> real 0m53.257s 92 | 93 | Parallel tasks: 94 | 95 | Default values (8+2 threads / 256 tasks) 96 | 97 | -> real 0m13.578s (10s-20s) 98 | 99 | ### WMS requests 100 | 101 | Local QGIS NaturalEarth WMS 102 | 103 | ../target/release/bbox-tile-server seed --tileset=ne_extracts --maxzoom=18 --s3-path=s3://tiles 104 | 105 | -> 14s 106 | 107 | Local QGIS NaturalEarth WMS to local directory 108 | 109 | ../target/release/bbox-tile-server seed --tileset=ne_extracts --maxzoom=18 --tile-path=/tmp/tiles 110 | 111 | -> 13s 112 | -------------------------------------------------------------------------------- /bbox-tile-server/src/datasource/mbtiles.rs: -------------------------------------------------------------------------------- 1 | //! MBTiles tile source. 2 | 3 | use crate::config::TileSetCfg; 4 | use crate::datasource::{ 5 | wms_fcgi::HttpRequestParams, LayerInfo, SourceType, TileResponse, TileSource, TileSourceError, 6 | }; 7 | use crate::filter_params::FilterParams; 8 | use crate::mbtiles_ds::MbtilesDatasource; 9 | use crate::store::TileReader; 10 | use async_trait::async_trait; 11 | use bbox_core::Format; 12 | use martin_mbtiles::Metadata; 13 | use tile_grid::{Tms, Xyz}; 14 | use tilejson::TileJSON; 15 | 16 | #[async_trait] 17 | impl TileSource for MbtilesDatasource { 18 | async fn xyz_request( 19 | &self, 20 | _tms: &Tms, 21 | tile: &Xyz, 22 | _filter: &FilterParams, 23 | _format: &Format, 24 | _request_params: HttpRequestParams<'_>, 25 | ) -> Result { 26 | if let Some(tile) = (self as &dyn TileReader) 27 | .get_tile(tile) 28 | .await 29 | .map_err(|_| TileSourceError::TileXyzError)? 30 | { 31 | Ok(tile) 32 | } else { 33 | Err(TileSourceError::TileXyzError) // TODO: check for empty tile? 34 | } 35 | } 36 | fn source_type(&self) -> SourceType { 37 | SourceType::Vector // TODO: Support Mbtiles raster 38 | } 39 | async fn tilejson(&self, _tms: &Tms, _format: &Format) -> Result { 40 | let metadata = self.get_metadata().await?; 41 | Ok(metadata.tilejson) 42 | } 43 | async fn layers(&self) -> Result, TileSourceError> { 44 | let metadata = self.get_metadata().await?; 45 | let layers = metadata 46 | .tilejson 47 | .vector_layers 48 | .unwrap_or(Vec::new()) 49 | .iter() 50 | .map(|l| LayerInfo { 51 | name: l.id.clone(), 52 | geometry_type: None, 53 | style: None, 54 | }) 55 | .collect(); 56 | Ok(layers) 57 | } 58 | async fn mbtiles_metadata( 59 | &self, 60 | _tileset: &TileSetCfg, 61 | _format: &Format, 62 | ) -> Result { 63 | Ok(self.get_metadata().await?) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bbox-tile-server/src/datasource/pmtiles.rs: -------------------------------------------------------------------------------- 1 | //! PMTiles tile source. 2 | 3 | use crate::datasource::{ 4 | wms_fcgi::HttpRequestParams, LayerInfo, SourceType, TileResponse, TileSource, TileSourceError, 5 | }; 6 | use crate::filter_params::FilterParams; 7 | use crate::store::pmtiles::PmtilesStoreReader; 8 | use crate::store::TileReader; 9 | use async_trait::async_trait; 10 | use bbox_core::Format; 11 | use log::debug; 12 | use tile_grid::{Tms, Xyz}; 13 | use tilejson::tilejson; 14 | use tilejson::TileJSON; 15 | 16 | #[async_trait] 17 | impl TileSource for PmtilesStoreReader { 18 | async fn xyz_request( 19 | &self, 20 | _tms: &Tms, 21 | tile: &Xyz, 22 | _filter: &FilterParams, 23 | _format: &Format, 24 | _request_params: HttpRequestParams<'_>, 25 | ) -> Result { 26 | if let Some(tile) = self 27 | .get_tile(tile) 28 | .await 29 | .map_err(|_| TileSourceError::TileXyzError)? 30 | { 31 | Ok(tile) 32 | } else { 33 | Err(TileSourceError::TileXyzError) // TODO: check for empty tile? 34 | } 35 | } 36 | fn source_type(&self) -> SourceType { 37 | SourceType::Vector //TODO 38 | } 39 | async fn tilejson(&self, _tms: &Tms, format: &Format) -> Result { 40 | debug!( 41 | "Metadata {}: {}", 42 | self.path.display(), 43 | self.get_metadata().await? 44 | ); 45 | let mut tj = tilejson! { tiles: vec![] }; 46 | tj.other 47 | .insert("format".to_string(), format.file_suffix().into()); 48 | Ok(tj) 49 | } 50 | async fn layers(&self) -> Result, TileSourceError> { 51 | Ok(vec![LayerInfo { 52 | name: self.path.to_string_lossy().to_string(), // TODO: file name only 53 | geometry_type: None, 54 | style: None, 55 | }]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /bbox-tile-server/src/datasource/wms_http.rs: -------------------------------------------------------------------------------- 1 | //! HTTP tile sources like remote WMS services. 2 | 3 | use crate::config::WmsHttpSourceParamsCfg; 4 | use crate::datasource::{ 5 | wms_fcgi::HttpRequestParams, LayerInfo, SourceType, TileSource, TileSourceError, 6 | }; 7 | use crate::filter_params::FilterParams; 8 | use crate::service::TmsExtensions; 9 | use async_trait::async_trait; 10 | use bbox_core::config::WmsHttpSourceProviderCfg; 11 | use bbox_core::{Format, TileResponse}; 12 | use log::debug; 13 | use std::io::Cursor; 14 | use tile_grid::{BoundingBox, Tms, Xyz}; 15 | use tilejson::{tilejson, TileJSON}; 16 | 17 | #[derive(Clone, Debug)] 18 | pub struct WmsHttpSource { 19 | client: reqwest::Client, 20 | pub req_url: String, 21 | } 22 | 23 | impl WmsHttpSource { 24 | pub fn from_config( 25 | provider: &WmsHttpSourceProviderCfg, 26 | params: &WmsHttpSourceParamsCfg, 27 | srid: i32, 28 | ) -> Self { 29 | let client = reqwest::Client::new(); 30 | let req_url = format!( 31 | "{}&SERVICE=WMS&REQUEST=GetMap&CRS=EPSG:{}&WIDTH={}&HEIGHT={}&LAYERS={}&STYLES=&FORMAT={}", 32 | provider.baseurl, 33 | srid, 34 | 256, //grid.width, 35 | 256, //grid.height, 36 | params.layers, 37 | provider.format, 38 | ); 39 | WmsHttpSource { client, req_url } 40 | } 41 | fn get_map_request(&self, extent: &BoundingBox) -> String { 42 | format!( 43 | "{}&BBOX={},{},{},{}", 44 | self.req_url, extent.left, extent.bottom, extent.right, extent.top 45 | ) 46 | } 47 | 48 | pub async fn get_map_response( 49 | &self, 50 | extent: &BoundingBox, 51 | ) -> Result { 52 | let req = self.get_map_request(extent); 53 | debug!("Request {req}"); 54 | self.client.get(req).send().await.map_err(Into::into) 55 | } 56 | 57 | async fn bbox_request(&self, extent: &BoundingBox) -> Result { 58 | let wms_resp = self.get_map_response(extent).await?; 59 | let mut response = TileResponse::new(); 60 | if let Some(content_type) = wms_resp 61 | .headers() 62 | .get("content-type") 63 | .map(|ct| ct.to_str().expect("invalid content-type")) 64 | { 65 | response.set_content_type(content_type); 66 | } 67 | let body = Box::new(Cursor::new(wms_resp.bytes().await?)); 68 | Ok(response.with_body(body)) 69 | } 70 | } 71 | 72 | #[async_trait] 73 | impl TileSource for WmsHttpSource { 74 | async fn xyz_request( 75 | &self, 76 | tms: &Tms, 77 | tile: &Xyz, 78 | _filter: &FilterParams, 79 | _format: &Format, 80 | _request_params: HttpRequestParams<'_>, 81 | ) -> Result { 82 | let extent_info = tms.xyz_extent(tile)?; 83 | self.bbox_request(&extent_info.extent).await 84 | } 85 | fn source_type(&self) -> SourceType { 86 | SourceType::Raster 87 | } 88 | async fn tilejson(&self, _tms: &Tms, format: &Format) -> Result { 89 | let mut tj = tilejson! { tiles: vec![] }; 90 | tj.other 91 | .insert("format".to_string(), format.file_suffix().into()); 92 | Ok(tj) 93 | } 94 | async fn layers(&self) -> Result, TileSourceError> { 95 | Ok(vec![LayerInfo { 96 | name: "WmsHttpSource".to_string(), // TODO: unique name in tileset 97 | geometry_type: None, 98 | style: None, 99 | }]) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /bbox-tile-server/src/filter_params.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug, Deserialize, Default, Clone)] 5 | pub struct FilterParams { 6 | pub datetime: Option, 7 | pub filters: HashMap, 8 | } 9 | 10 | #[derive(Debug)] 11 | pub enum TemporalType { 12 | DateTime(chrono::DateTime), 13 | Open, 14 | } 15 | 16 | impl FilterParams { 17 | pub fn as_args(&self) -> String { 18 | let mut args = vec![ 19 | Some("".to_string()), 20 | self.datetime.as_ref().map(|v| format!("datetime={v}")), 21 | ] 22 | .into_iter() 23 | .flatten() 24 | .collect::>() 25 | .join("&"); 26 | 27 | for (key, val) in &self.filters { 28 | args.push_str(&format!("&{key}={val}")) 29 | } 30 | if !args.is_empty() { 31 | // replace & with ? 32 | args.replace_range(0..1, "?"); 33 | } 34 | args 35 | } 36 | pub fn temporal(&self) -> Result>, Box> { 37 | if let Some(dt) = &self.datetime { 38 | let parts: Vec<&str> = dt.split('/').collect(); 39 | let mut parsed_parts = vec![]; 40 | for part in &parts { 41 | match *part { 42 | ".." => parsed_parts.push(TemporalType::Open), 43 | p => { 44 | parsed_parts.push(TemporalType::DateTime( 45 | chrono::DateTime::parse_from_rfc3339(p)?, 46 | )); 47 | } 48 | } 49 | } 50 | return Ok(Some(parsed_parts)); 51 | } 52 | Ok(None) 53 | } 54 | pub fn other_params(&self) -> Result<&HashMap, Box> { 55 | Ok(&self.filters) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /bbox-tile-server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod cli; 2 | pub mod config; 3 | pub mod config_t_rex; 4 | pub mod datasource; 5 | mod endpoints; 6 | mod filter_params; 7 | mod mbtiles_ds; 8 | pub mod seed; 9 | pub mod service; 10 | pub mod store; 11 | 12 | pub use service::*; 13 | -------------------------------------------------------------------------------- /bbox-tile-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{middleware, middleware::Condition, App, HttpServer}; 2 | use bbox_core::cli::CliArgs; 3 | use bbox_core::config::CoreServiceCfg; 4 | use bbox_core::service::{CoreService, OgcApiService, ServiceConfig, ServiceEndpoints}; 5 | use bbox_tile_server::config::TileServiceCfg; 6 | use bbox_tile_server::service::TileService; 7 | 8 | #[cfg(feature = "asset-server")] 9 | use bbox_asset_server::{config::AssetServiceCfg, AssetService}; 10 | #[cfg(feature = "map-server")] 11 | use bbox_map_server::{config::MapServiceCfg, MapService}; 12 | 13 | #[cfg(not(feature = "map-server"))] 14 | use bbox_core::service::{DummyService as MapService, NoConfig as MapServiceCfg}; 15 | #[cfg(not(feature = "asset-server"))] 16 | use bbox_core::service::{DummyService as AssetService, NoConfig as AssetServiceCfg}; 17 | 18 | #[actix_web::main] 19 | async fn run_service() -> std::io::Result<()> { 20 | let mut cli = CliArgs::default(); 21 | cli.register_service_args::(); 22 | cli.register_service_args::(); 23 | cli.register_service_args::(); 24 | cli.register_service_args::(); 25 | cli.apply_global_args(); 26 | let matches = cli.cli_matches(); 27 | 28 | let core_cfg = CoreServiceCfg::initialize(&matches).unwrap(); 29 | let mut core = CoreService::create(&core_cfg, &core_cfg).await; 30 | 31 | let cfg = MapServiceCfg::initialize(&matches).unwrap(); 32 | let map_service = MapService::create(&cfg, &core_cfg).await; 33 | core.add_service(&map_service); 34 | 35 | let cfg = TileServiceCfg::initialize(&matches).unwrap(); 36 | #[allow(unused_mut)] 37 | let mut tile_service = TileService::create(&cfg, &core_cfg).await; 38 | core.add_service(&tile_service); 39 | 40 | let cfg = AssetServiceCfg::initialize(&matches).unwrap(); 41 | let asset_service = AssetService::create(&cfg, &core_cfg).await; 42 | core.add_service(&asset_service); 43 | 44 | #[cfg(feature = "map-server")] 45 | tile_service.set_map_service(&map_service); 46 | 47 | if map_service.cli_run(&matches).await { 48 | return Ok(()); 49 | } 50 | if tile_service.cli_run(&matches).await { 51 | return Ok(()); 52 | } else { 53 | tile_service.setup_tile_stores().await.unwrap(); 54 | } 55 | if asset_service.cli_run(&matches).await { 56 | return Ok(()); 57 | } 58 | 59 | let workers = core.workers(); 60 | let server_addr = core.server_addr().to_string(); 61 | let tls_config = core.tls_config(); 62 | let mut server = HttpServer::new(move || { 63 | App::new() 64 | .wrap(Condition::new(core.has_cors(), core.cors())) 65 | .wrap(Condition::new(core.has_metrics(), core.middleware())) 66 | .wrap(Condition::new(core.has_metrics(), core.metrics().clone())) 67 | .wrap(middleware::Logger::default()) 68 | .wrap(middleware::Compress::default()) 69 | .configure(|cfg| core.register_endpoints(cfg)) 70 | .configure(bbox_core::static_assets::register_endpoints) 71 | .configure(|cfg| map_service.register_endpoints(cfg)) 72 | .configure(|cfg| tile_service.register_endpoints(cfg)) 73 | .configure(|cfg| asset_service.register_endpoints(cfg)) 74 | }); 75 | if let Some(tls_config) = tls_config { 76 | server = server.bind_rustls(server_addr, tls_config)?; 77 | } else { 78 | server = server.bind(server_addr)?; 79 | } 80 | server.workers(workers).run().await 81 | } 82 | 83 | fn main() { 84 | run_service().unwrap(); 85 | } 86 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://github.com/LukeMathWalker/cargo-chef 2 | FROM lukemathwalker/cargo-chef:latest-rust-slim-bookworm as chef 3 | 4 | RUN apt-get update && \ 5 | DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y protobuf-compiler && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | WORKDIR app 9 | 10 | # ------ 11 | FROM chef AS planner 12 | COPY . . 13 | RUN cargo chef prepare --recipe-path recipe.json 14 | 15 | # ------------------------------------------------------------------------------ 16 | # Build Stage 17 | # ------------------------------------------------------------------------------ 18 | 19 | FROM chef AS builder 20 | 21 | COPY --from=planner /app/recipe.json recipe.json 22 | RUN cargo chef cook --release --recipe-path recipe.json 23 | 24 | COPY . . 25 | 26 | ARG BUILD_DIR=bbox-server 27 | ARG BUILD_FEATURES="" 28 | 29 | RUN cd $BUILD_DIR && cargo install $BUILD_FEATURES --path . --verbose 30 | 31 | # build Debian package 32 | RUN apt update && apt install -y dpkg-dev 33 | RUN cargo install cargo-deb --locked 34 | RUN cd bbox-tile-server && cargo deb 35 | # Build package and copy to host: 36 | # docker build --build-arg BUILD_DIR=bbox-tile-server --target builder -t bbox-deb-build -f docker/Dockerfile . 37 | # docker run -v $PWD/assets:/mnt bbox-deb-build bash -c "cp /app/target/debian/* /mnt" 38 | 39 | # ------------------------------------------------------------------------------ 40 | # Final Stage 41 | # ------------------------------------------------------------------------------ 42 | 43 | FROM debian:bookworm-slim 44 | 45 | ARG BUILD_DIR=bbox-server 46 | ARG BUILDAPP=$BUILD_DIR 47 | 48 | COPY --from=builder /usr/local/cargo/bin/$BUILDAPP /usr/local/bin/$BUILDAPP 49 | RUN ln -s $BUILDAPP /usr/local/bin/bbox-app 50 | 51 | WORKDIR /var/www 52 | USER www-data 53 | ENV BBOX_WEBSERVER__SERVER_ADDR="0.0.0.0:8080" 54 | EXPOSE 8080 55 | CMD ["bbox-app", "serve"] 56 | -------------------------------------------------------------------------------- /docker/Dockerfile-mapserver: -------------------------------------------------------------------------------- 1 | FROM rust:bookworm as builder 2 | RUN apt-get update && \ 3 | DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y protobuf-compiler && \ 4 | rm -rf /var/lib/apt/lists/* 5 | WORKDIR /usr/src/bbox 6 | COPY . . 7 | ARG BUILD_DIR=bbox-server 8 | ARG BUILD_FEATURES="" 9 | 10 | RUN cd $BUILD_DIR && cargo install $BUILD_FEATURES --path . 11 | 12 | # ------ 13 | FROM debian:bookworm-slim 14 | 15 | RUN apt-get update && apt-get install -y libcurl4 cgi-mapserver procps && \ 16 | rm -rf /var/lib/apt/lists/* 17 | 18 | ARG BUILD_DIR=bbox-server 19 | ARG BUILDAPP=$BUILD_DIR 20 | 21 | COPY --from=builder /usr/local/cargo/bin/$BUILDAPP /usr/local/bin/$BUILDAPP 22 | RUN ln -s $BUILDAPP /usr/local/bin/bbox-app 23 | 24 | WORKDIR /var/www 25 | USER www-data 26 | ENV BBOX_WEBSERVER__SERVER_ADDR="0.0.0.0:8080" 27 | EXPOSE 8080 28 | CMD ["bbox-app", "serve"] 29 | -------------------------------------------------------------------------------- /docker/Dockerfile-qgis-server: -------------------------------------------------------------------------------- 1 | FROM rust:bookworm as builder 2 | RUN apt-get update && \ 3 | DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y protobuf-compiler && \ 4 | rm -rf /var/lib/apt/lists/* 5 | WORKDIR /usr/src/bbox 6 | COPY . . 7 | ARG BUILD_DIR=bbox-server 8 | ARG BUILD_FEATURES="" 9 | 10 | RUN cd $BUILD_DIR && cargo install $BUILD_FEATURES --path . --verbose 11 | 12 | # ------ 13 | FROM debian:bookworm-slim 14 | 15 | ARG DEBIAN_REPO=debian 16 | RUN apt-get update && \ 17 | apt-get install --no-install-recommends -y ca-certificates gnupg2 curl apt-utils && \ 18 | curl -L https://qgis.org/downloads/qgis-2022.gpg.key | gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/qgis-archive.gpg --import && \ 19 | chmod a+r /etc/apt/trusted.gpg.d/qgis-archive.gpg && \ 20 | echo "deb https://qgis.org/$DEBIAN_REPO bookworm main" > /etc/apt/sources.list.d/qgis.org.list && \ 21 | apt-get update && \ 22 | DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y qgis-server-wms qgis-server-wfs && \ 23 | rm -rf /var/lib/apt/lists/* 24 | 25 | # Workaround for broken Proj data (last tested with proj-data 6.3.1-1) 26 | RUN ln -s CHENYX06.gsb /usr/share/proj/CHENyx06a.gsb 27 | 28 | ARG BUILD_DIR=bbox-server 29 | ARG BUILDAPP=$BUILD_DIR 30 | 31 | COPY --from=builder /usr/local/cargo/bin/$BUILDAPP /usr/local/bin/$BUILDAPP 32 | RUN ln -s $BUILDAPP /usr/local/bin/bbox-app 33 | 34 | COPY bbox-map-server/qgis/plugins /var/www/bbox-map-server/qgis/plugins 35 | 36 | RUN chown -R www-data /var/www 37 | WORKDIR /var/www 38 | USER www-data 39 | ENV BBOX_WEBSERVER__SERVER_ADDR="0.0.0.0:8080" 40 | EXPOSE 8080 41 | CMD ["bbox-app", "serve"] 42 | -------------------------------------------------------------------------------- /docker/bbox/.gitignore: -------------------------------------------------------------------------------- 1 | /.env 2 | -------------------------------------------------------------------------------- /docker/bbox/Makefile: -------------------------------------------------------------------------------- 1 | profile ?= default 2 | 3 | up: 4 | docker compose --profile $(profile) up -d 5 | 6 | ps: 7 | docker compose --profile $(profile) ps 8 | 9 | logs: 10 | docker compose --profile $(profile) logs -f --tail=20 $(c) 11 | 12 | restart: 13 | docker compose --profile $(profile) restart $(c) 14 | 15 | down: 16 | docker compose --profile $(profile) down 17 | 18 | loaddb: 19 | #docker compose --profile processes run windmill_worker ogr2ogr -f PostgreSQL PG:'host=postgresql user=geodb password=$$PG_PASS dbname=geodb' /assets/ne_extracts.gpkg 20 | docker run --rm --user $$(id -u):$$(id -g) --network bbox --env-file .env -v $$PWD/../../assets:/assets osgeo/gdal:alpine-normal-latest ogr2ogr -f PostgreSQL PG:"host=postgresql user=geodb dbname=geodb" /assets/ne_extracts.gpkg 21 | 22 | resetdb: 23 | docker stop bbox_postgresql_1 24 | docker rm bbox_postgresql_1 25 | docker volume rm bbox_postgres-data 26 | 27 | teamengine: 28 | docker compose up -d teamengine 29 | # open http://localhost:8081/te2/ 30 | # Login with ogctest / ogctest 31 | # Landing page location: http://bbox:8080/ 32 | -------------------------------------------------------------------------------- /docker/bbox/README.md: -------------------------------------------------------------------------------- 1 | Docker Compose Environment 2 | ========================== 3 | 4 | Setup 5 | ----- 6 | 7 | cp template.env .env 8 | echo "PG_PASS=$(pwgen -s 40 1)" >> .env 9 | echo "AUTHENTIK_SECRET_KEY=$(pwgen -s 50 1)" >> .env 10 | 11 | Usage 12 | ----- 13 | 14 | docker-compose --profile default up -d 15 | 16 | ### BBOX 17 | 18 | x-www-browser http://127.0.0.1:8080/ 19 | 20 | ### Authentik 21 | 22 | docker-compose --profile auth up -d 23 | 24 | x-www-browser http://127.0.0.1:9000/ 25 | 26 | Login: akadmin 27 | Password: akadmin 28 | 29 | ### Windmill 30 | 31 | docker-compose --profile processes up -d 32 | 33 | x-www-browser http://127.0.0.1:8000/ 34 | 35 | Email: admin@windmill.dev 36 | Password: changeme 37 | 38 | 39 | Instrumentation 40 | --------------- 41 | 42 | ### Grafana 43 | 44 | https://grafana.com/docs/grafana/ 45 | 46 | Open Grafana: 47 | 48 | x-www-browser http://127.0.0.1:8080/grafana/ 49 | 50 | - Enter `admin` for username and password 51 | 52 | 53 | BBOX WMS metrics examples: 54 | 55 | Average request duration 56 | 57 | rate(http_requests_duration_sum[5m])/rate(http_requests_duration_count[5m]) 58 | 59 | Request duration 90th percentile 60 | 61 | histogram_quantile(0.9, rate(http_requests_duration_bucket[5m])) 62 | 63 | https://www.robustperception.io/how-does-a-prometheus-histogram-work 64 | 65 | WMS Endpoint: 66 | 67 | http_requests_duration_sum{endpoint="/qgis/{project:.+}"} 68 | 69 | 70 | ### Jaeger tracing 71 | 72 | View spans: 73 | 74 | x-www-browser http://127.0.0.1:16686/ 75 | -------------------------------------------------------------------------------- /docker/bbox/bbox-full.toml: -------------------------------------------------------------------------------- 1 | [webserver] 2 | # Web server settings 3 | # Environment variable prefix: BBOX_WEBSERVER__ 4 | server_addr = "0.0.0.0:8080" # Default: 127.0.0.1:8080 5 | # worker_threads = 4 # Default: number of CPU cores 6 | # public_server_url = "http://bbox:8080" # Default: Request address 7 | 8 | [metrics.prometheus] 9 | # Prometheus metrics endpoint 10 | # Environment variable prefix: BBOX_METRICS__PROMETHEUS__ 11 | path = "/metrics" 12 | 13 | [metrics.jaeger] 14 | # Jaeger tracing 15 | # Environment variable prefix: BBOX_METRICS__JAEGER__ 16 | agent_endpoint = "jaeger:6831" 17 | 18 | [[datasource.directory]] 19 | path = "../../data" 20 | 21 | #[[datasource.postgis]] 22 | #url = "postgres://geodb:xxx@postgresql/geodb" 23 | 24 | [featureserver] 25 | 26 | [[fileserver.static]] 27 | # Static file serving 28 | # Env var example: BBOX_FILESERVER__STATIC='[{dir="data",path="data"}]' 29 | # ./data/* -> http://localhost:8080/data/ 30 | dir = "./data" 31 | path = "data" 32 | 33 | [[fileserver.repo]] 34 | # QGIS plugin repository 35 | # Env var example: BBOX_FILESERVER__REPO='[{dir="plugins",path="qgisrepo"}]' 36 | # ./plugins/*.zip -> http://localhost:8080/qgisrepo/plugins.xml 37 | dir = "./plugins" 38 | path = "qgisrepo" 39 | 40 | [wmsserver] 41 | # WMS server settings 42 | # Environment variable prefix: BBOX_WMSSERVER__ 43 | num_fcgi_processes = 2 # Default: number of CPU cores 44 | # wait_timeout = 30000 # FCGI wait timeout in ms. Default: 90s 45 | # search_projects = false # Scan directories and build inventory 46 | 47 | [wmsserver.qgis_backend] 48 | # QGIS Server settings 49 | # Environment variable prefix: BBOX_WMSSERVER__QGIS_BACKEND__ 50 | project_basedir = "/data" # Base dir for project files (.qgs, .qgz) 51 | qgs.path = "/qgis" # WMS URL base path 52 | qgz.path = "/qgz" # WMS URL base path 53 | 54 | #[wmsserver.umn_backend] 55 | # UMN MapServer settings 56 | # Environment variable prefix: BBOX_WMSSERVER__UMN_BACKEND__ 57 | #project_basedir = "/data" # Base dir for project files (.map) 58 | #path = "/wms/map" # WMS URL base path 59 | 60 | #[wmsserver.mock_backend] 61 | # Enable FCGI mockup backend (for testing) 62 | # Environment variable prefix: BBOX_WMSSERVER__MOCK_BACKEND__ 63 | 64 | #[processes.dagster_backend] 65 | # Dagster backend settings 66 | # Environment variable prefix: BBOX_PROCESSES__DAGSTER_BACKEND__ 67 | # graphql_url = "http://localhost:3000/dagster/graphql" 68 | # repository_name = "fpds2_processing_repository" 69 | # repository_location_name = "fpds2_processing.repos" 70 | 71 | [grid.user] 72 | width = 256 73 | height = 256 74 | extent = { minx = 2420000.0, miny = 1030000.0, maxx = 2920000.0, maxy = 1350000.0 } 75 | srid = 2056 76 | units = "m" 77 | resolutions = [4000,3750,3500,3250,3000,2750,2500,2250,2000,1750,1500,1250,1000,750,650,500,250,100,50,20,10,5,2.5,2,1.5,1,0.5,0.25,0.125,0.1,0.0625] 78 | origin = "TopLeft" 79 | 80 | [tile.wms] 81 | #baseurl = "http://localhost/cgi-bin/qgis_mapserv.fcgi?MAP=/opt/qgis_server_data/ch_051_1_version1_7_mn95.qgz&version=1.1.1&transparent=True&srs=EPSG:2056&styles=" 82 | #layers = "ch.ti.051_1.piano_registro_fondiario_colori" 83 | #format = "image/png" 84 | baseurl = "http://localhost:8080/qgis/ne?version=1.3.0" 85 | layers = "country" 86 | format = "image/png; mode=8bit" 87 | -------------------------------------------------------------------------------- /docker/bbox/create-multiple-postgresql-databases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -u 5 | 6 | function create_user_and_database() { 7 | local database=$1 8 | echo " Creating user and database '$database'" 9 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 10 | CREATE USER $database PASSWORD '$POSTGRES_PASSWORD'; 11 | CREATE DATABASE $database OWNER $database; 12 | EOSQL 13 | } 14 | 15 | function setup_windmill() { 16 | psql windmill -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" -f /docker-entrypoint-initdb.d/init-windmill-as-superuser.sql 17 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL 18 | GRANT windmill_admin TO windmill; 19 | GRANT windmill_user TO windmill; 20 | EOSQL 21 | } 22 | 23 | if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then 24 | echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" 25 | for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do 26 | create_user_and_database $db 27 | if [ "$db" == "windmill" ]; then 28 | setup_windmill 29 | fi 30 | done 31 | echo "Multiple databases created" 32 | fi 33 | -------------------------------------------------------------------------------- /docker/bbox/init-windmill-as-superuser.sql: -------------------------------------------------------------------------------- 1 | DO 2 | $do$ 3 | BEGIN 4 | IF NOT EXISTS ( 5 | SELECT 6 | FROM pg_catalog.pg_roles 7 | WHERE rolname = 'windmill_user') THEN 8 | 9 | LOCK TABLE pg_catalog.pg_roles; 10 | 11 | CREATE ROLE windmill_user; 12 | 13 | GRANT ALL 14 | ON ALL TABLES IN SCHEMA public 15 | TO windmill_user; 16 | 17 | GRANT ALL PRIVILEGES 18 | ON ALL SEQUENCES IN SCHEMA public 19 | TO windmill_user; 20 | 21 | ALTER DEFAULT PRIVILEGES 22 | IN SCHEMA public 23 | GRANT ALL ON TABLES TO windmill_user; 24 | 25 | ALTER DEFAULT PRIVILEGES 26 | IN SCHEMA public 27 | GRANT ALL ON SEQUENCES TO windmill_user; 28 | 29 | END IF; 30 | END 31 | $do$; 32 | 33 | DO 34 | $do$ 35 | BEGIN 36 | IF NOT EXISTS ( 37 | SELECT 38 | FROM pg_catalog.pg_roles 39 | WHERE rolname = 'windmill_admin') THEN 40 | CREATE ROLE windmill_admin WITH BYPASSRLS; 41 | 42 | GRANT ALL 43 | ON ALL TABLES IN SCHEMA public 44 | TO windmill_admin; 45 | 46 | GRANT ALL PRIVILEGES 47 | ON ALL SEQUENCES IN SCHEMA public 48 | TO windmill_admin; 49 | 50 | ALTER DEFAULT PRIVILEGES 51 | IN SCHEMA public 52 | GRANT ALL ON TABLES TO windmill_admin; 53 | 54 | ALTER DEFAULT PRIVILEGES 55 | IN SCHEMA public 56 | GRANT ALL ON SEQUENCES TO windmill_admin; 57 | END IF; 58 | END 59 | $do$; 60 | -------------------------------------------------------------------------------- /docker/bbox/instrumentation/grafana/provisioning/dashboards/dashboards.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'System Bash Boards' 5 | orgId: 1 6 | folder: 'System' 7 | type: file 8 | disableDeletion: false 9 | editable: true 10 | options: 11 | path: /etc/grafana/dashboards/system 12 | -------------------------------------------------------------------------------- /docker/bbox/instrumentation/grafana/provisioning/datasources/prometheus.yml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: Prometheus 5 | type: prometheus 6 | access: proxy 7 | url: http://prometheus:9090 8 | editable: false 9 | isDefault: true 10 | -------------------------------------------------------------------------------- /docker/bbox/instrumentation/grafana/provisioning/notifiers/telegram.yml: -------------------------------------------------------------------------------- 1 | ##notifiers: 2 | ## - name: "Grafana Alerting Telegram" 3 | ## type: telegram 4 | ## uid: notifier1 5 | ## org_name: Main Org. 6 | ## is_default: true 7 | ## send_reminder: true 8 | ## frequency: 15m 9 | ## disable_resolve_message: false 10 | ## # See `Supported Settings` section for settings supported for each 11 | ## # alert notification type. 12 | ## settings: 13 | ## # bottoken: '1234567890:AAF_k7w8BOtsEmns,adasdasdascnnWR9vY' 14 | ## # chatid: '-544993956' 15 | ## uploadImage: false 16 | -------------------------------------------------------------------------------- /docker/bbox/instrumentation/prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 2s 3 | evaluation_interval: 2s 4 | 5 | scrape_configs: 6 | - job_name: "grafana" 7 | scrape_interval: 5s 8 | static_configs: 9 | - targets: 10 | - grafana:3000 11 | - job_name: "prometheus" 12 | scrape_interval: 5s 13 | static_configs: 14 | - targets: 15 | - localhost:9090 16 | - job_name: "bbox" 17 | scrape_interval: 5s 18 | static_configs: 19 | - targets: 20 | - bbox:8080 21 | - job_name: "authentik_server" 22 | scrape_interval: 10s 23 | static_configs: 24 | - targets: 25 | - authentik_server:9300 26 | - job_name: "windmill" 27 | scrape_interval: 5s 28 | static_configs: 29 | - targets: 30 | - windmill:8001 31 | - job_name: "windmill_worker" 32 | scrape_interval: 5s 33 | static_configs: 34 | - targets: 35 | - windmill_worker:8001 36 | -------------------------------------------------------------------------------- /docker/bbox/nginx/authentik.conf.template: -------------------------------------------------------------------------------- 1 | upstream authentik { 2 | server authentik_server:9000; 3 | # Improve performance by keeping some connections alive. 4 | keepalive 10; 5 | } 6 | 7 | # Upgrade WebSocket if requested, otherwise use keepalive 8 | map $http_upgrade $connection_upgrade_keepalive { 9 | default upgrade; 10 | '' ''; 11 | } 12 | 13 | 14 | server { 15 | #listen 443 ssl http2; 16 | listen 9000; 17 | server_name _; 18 | 19 | #ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; 20 | #ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; 21 | 22 | # Increase buffer size for large headers 23 | # This is needed only if you get 'upstream sent too big header while reading response 24 | # header from upstream' error when trying to access an application protected by goauthentik 25 | #proxy_buffers 8 16k; 26 | #proxy_buffer_size 32k; 27 | 28 | root /usr/share/nginx/html; 29 | 30 | location / { 31 | proxy_pass http://authentik; 32 | 33 | proxy_http_version 1.1; 34 | proxy_set_header X-Forwarded-Proto $scheme; 35 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 36 | proxy_set_header Host $host; 37 | proxy_set_header Upgrade $http_upgrade; 38 | proxy_set_header Connection $connection_upgrade_keepalive; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docker/bbox/nginx/windmill.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | #listen 443 ssl http2; 3 | listen 8000; 4 | server_name _; 5 | 6 | #ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; 7 | #ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; 8 | 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 11 | proxy_set_header X-Forwarded-Proto https; 12 | proxy_set_header Host $http_host; 13 | 14 | proxy_redirect off; 15 | 16 | location /ws/ { 17 | proxy_pass http://windmill_worker:3001/; 18 | } 19 | location / { 20 | proxy_pass http://windmill:8000; 21 | } 22 | 23 | location = /50x.html { 24 | root html; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /docker/bbox/template.env: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | POSTGIS_DB=geodb 3 | POSTGRES_MULTIPLE_DATABASES=authentik,windmill 4 | 5 | # Windmill 6 | WM_BASE_URL=http://localhost:8000 7 | #TZ=Europe/Zurich 8 | 9 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | 3 | set shell := ["bash", "-c"] 4 | 5 | # Publish to crates.io 6 | publish: 7 | cd bbox-core && cargo publish 8 | cd bbox-feature-server && cargo publish 9 | cd bbox-map-server && cargo publish 10 | cd bbox-asset-server && cargo publish 11 | cd bbox-tile-server && cargo publish 12 | cd bbox-processes-server && cargo publish 13 | # cd bbox-routing-server && cargo publish 14 | cd bbox-frontend && cargo publish 15 | cd bbox-server && cargo publish 16 | -------------------------------------------------------------------------------- /plugins/instantprint.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/plugins/instantprint.zip -------------------------------------------------------------------------------- /templates/maplibre-asset-style.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MapLibre Viewer 6 | 7 | 8 | 9 | 10 | 11 |
12 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /templates/maplibre.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MapLibre Viewer 6 | 7 | 8 | 9 | 10 | 11 |
12 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | /.hugo_build.lock 3 | reference.md 4 | -------------------------------------------------------------------------------- /website/assets/css/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Lato:ital@0;1&family=League+Gothic&display=swap'); 2 | 3 | :root { 4 | font-family: "Lato", sans-serif; 5 | font-size: 1.1rem; 6 | /* geopole orange */ 7 | --primary-hue: 45deg; 8 | --primary-saturation: 100%; 9 | 10 | --geopole-red: #cc0000; 11 | --geopole-orange: #ffbf00; 12 | --geopole-light-olive: #e4e7d7; 13 | --geopole-green: #43bc06; 14 | --geopole-olive: #babfa7; 15 | --geopole-blue: #0e5174; 16 | --geopole-list-divider: #707463; 17 | --geopole-grey: #cccccc; 18 | --geopole-black: #424535; 19 | } 20 | 21 | body { 22 | background-color: var(--geopole-light-olive); 23 | } 24 | 25 | /* Title bar */ 26 | .nav-container-blur { 27 | background-color: hsl(45deg 100% 45% / 1); 28 | } 29 | .nav-container .hx-font-extrabold { 30 | font-family: "League Gothic", sans-serif; 31 | font-size: 1.55em; 32 | font-weight: 400; 33 | } 34 | 35 | /* Menu hover background */ 36 | aside.sidebar-container .hover\:hx-bg-gray-100:hover { 37 | background-color: hsl(45deg 100% 39% / 0.2); 38 | } 39 | 40 | aside.sidebar-container .sidebar-active-item.hx-bg-primary-100 { 41 | background-color: hsl(45deg 100% 39% / 0.1); 42 | } 43 | 44 | /* background for theme switcher */ 45 | html:not(.dark) aside.sidebar-container .hx-sticky { 46 | background-color: var(--geopole-light-olive); 47 | --tw-shadow: none; 48 | } 49 | 50 | /* background for "Edit this page" */ 51 | nav .hx-bg-white { 52 | background-color: var(--geopole-light-olive); 53 | --tw-shadow: none; 54 | } 55 | 56 | .content h1, h2, h3 { 57 | font-family: "League Gothic", sans-serif; 58 | } 59 | 60 | .content h1 { 61 | font-size: 3.77em; 62 | font-weight: 400; 63 | letter-spacing: normal; 64 | } 65 | .content h2 { 66 | font-size: 2.11em; 67 | font-weight: 400; 68 | letter-spacing: normal; 69 | } 70 | .content h3 { 71 | font-size: 1.55em; 72 | font-weight: 400; 73 | letter-spacing: normal; 74 | } 75 | 76 | /* Table rows */ 77 | .content :where(table):not(:where(.hextra-code-block table, [class~="not-prose"], [class~="not-prose"] *)) tr:nth-child(2n) { 78 | background-color: hsl(45deg 100% 39% / 0.05); 79 | } 80 | 81 | html:not(.dark) footer.hextra-footer.hx-bg-gray-100 { 82 | background-color: hsl(45deg 100% 45% / 1); 83 | } 84 | 85 | /* Reduce footer height */ 86 | footer .hx-py-12 { 87 | padding-top: 1rem; 88 | padding-bottom: 1rem; 89 | } 90 | footer .hx-mt-6 { 91 | margin-top: 0; 92 | } 93 | -------------------------------------------------------------------------------- /website/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://www.bbox.earth/" 2 | title = "BBOX Server" 3 | theme = "hextra" 4 | enableRobotsTXT = false 5 | enableGitInfo = true 6 | enableInlineShortcodes = true 7 | 8 | [languages.en] 9 | languageName = "English" 10 | weight = 1 11 | #title = "BBOX Server" 12 | 13 | [markup.goldmark.renderer] 14 | unsafe = true 15 | 16 | [markup.highlight] 17 | noClasses = false 18 | 19 | [[menu.main]] 20 | identifier = "documentation" 21 | name = "Documentation" 22 | pageRef = "/docs" 23 | weight = 1 24 | 25 | [[menu.main]] 26 | name = "Changelog" 27 | weight = 2 28 | url = "https://github.com/bbox-services/bbox/blob/main/CHANGELOG.md" 29 | 30 | [[menu.main]] 31 | name = "Search" 32 | weight = 5 33 | [menu.main.params] 34 | type = "search" 35 | 36 | [[menu.main]] 37 | name = "GitHub" 38 | weight = 6 39 | url = "https://github.com/bbox-services/bbox" 40 | [menu.main.params] 41 | icon = "github" 42 | 43 | [params] 44 | description = "Composable spatial services" 45 | displayUpdatedDate = false 46 | dateFormat = "January 2, 2006" 47 | 48 | [params.navbar] 49 | displayTitle = true 50 | displayLogo = true 51 | width = "normal" 52 | 53 | [params.navbar.logo] 54 | path = "/favicon.svg" 55 | dark = "/favicon-dark.svg" 56 | width = 28 57 | 58 | [params.page] 59 | width = "normal" 60 | 61 | [params.theme] 62 | default = "light" 63 | displayToggle = true 64 | 65 | [params.footer] 66 | enable = true 67 | displayCopyright = true 68 | displayPoweredBy = false 69 | width = "normal" 70 | 71 | [params.search] 72 | enable = true 73 | type = "flexsearch" 74 | 75 | [params.search.flexsearch] 76 | index = "content" 77 | tokenize = "forward" 78 | 79 | [params.editURL] 80 | enable = true 81 | base = "https://github.com/bbox-services/bbox/edit/main/website/content" 82 | 83 | [params.highlight.copy] 84 | enable = true 85 | display = "hover" 86 | -------------------------------------------------------------------------------- /website/content/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | toc: false 3 | --- 4 | 5 | # BBOX Server 6 | 7 | Composable spatial services. 8 | 9 | ![bbox-services](bbox-services.png) 10 | 11 | ## Components 12 | 13 | * [BBOX Feature server](docs/feature-server/): OGC API Features service 14 | * [BBOX Map server](docs/map-server/): OGC API Map service 15 | * [BBOX Tile server](docs/tile-server/): OGC API Tile service 16 | * [BBOX Asset server](docs/asset-server/): Serving static and templated files 17 | * [BBOX Processes server](docs/processes-server/): OGC API Processes service 18 | * [BBOX Routing server](docs/routing-server/): OGC API Routing service (experimental) 19 | 20 | ## Features 21 | 22 | * Built-in high performance HTTP server 23 | * OpenAPI support with built-in UI 24 | * Instrumentation: Prometheus metrics and Jaeger tracing 25 | * Healths endpoints for Docker and Kubernetes hosting 26 | * Common configuration file 27 | * Open Source under Apache-2.0 / MIT license 28 | 29 | ## Installation 30 | 31 | * [BBOX Server](docs/installation/) (contains all components) 32 | * [BBOX Tile Server](docs/tile-server/installation/) 33 | 34 | ## Links 35 | 36 | * [Documentation](docs) 37 | * [Changelog](https://github.com/bbox-services/bbox/blob/main/CHANGELOG.md) 38 | -------------------------------------------------------------------------------- /website/content/bbox-services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/content/bbox-services.png -------------------------------------------------------------------------------- /website/content/docs/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | next: docs/installation 3 | --- 4 | 5 | # BBOX Server Documentation 6 | 7 | ## Core 8 | 9 | * [Installation](installation) 10 | * [Configuration](core/configuration) 11 | * [API Endpoints](api-endpoints) 12 | * [Instrumentation](instrumentation) 13 | 14 | ## Components 15 | 16 | * [BBOX Feature server](feature-server/) 17 | * [BBOX Map server](map-server/) 18 | * [BBOX Tile server](tile-server/) 19 | * [BBOX Asset server](asset-server/) 20 | * [BBOX Processes server](processes-server/) 21 | * [BBOX Routing server](routing-server/) 22 | -------------------------------------------------------------------------------- /website/content/docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 5 3 | --- 4 | 5 | # API Endpoints 6 | 7 | ## OGC API Endpoints 8 | 9 | Services are available via the HTTP `GET` endpoints: 10 | 11 | | URL | Description | 12 | |----------------------------------------|------------------------------------------------| 13 | | `/` | Landing page (HTML or JSON) | 14 | | `/conformance` | API conforomance | 15 | | `/openapi` | OpenAPI specification (YAML) | 16 | | `/openapi.yaml` | OpenAPI specification (YAML) | 17 | | `/openapi.json` | OpenAPI specification (JSON) | 18 | 19 | 20 | Available formats: 21 | 22 | | URL | Description | 23 | |---------|---------------------------| 24 | | `.json` | JSON / GeoJSON format | 25 | | `.html` | HTML format, if available | 26 | 27 | 28 | ## BBOX API Endpoints 29 | 30 | 31 | | URL | Description | 32 | |-----------|---------------------| 33 | | `/health` | Server health check | 34 | 35 | 36 | ## Request examples 37 | 38 | curl -s -H 'Accept: application/json' http://localhost:8080/ | jq . 39 | 40 | curl -s http://localhost:8080/openapi.json | jq . 41 | -------------------------------------------------------------------------------- /website/content/docs/asset-server/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 9 3 | next: docs/asset-server/configuration 4 | --- 5 | 6 | # BBOX Asset server 7 | 8 | Serving static and templated files. 9 | 10 | Features: 11 | - [x] Configurable base directories and endpoints 12 | - [x] Serve fonts and other assets for Tile services 13 | - [x] QGIS plugin repository 14 | - [ ] Templates with inputs from path, arguments and configuration 15 | 16 | 17 | ## Usage 18 | 19 | Run feature server with `bbox.toml` configuration: 20 | 21 | bbox-asset-server serve 22 | -------------------------------------------------------------------------------- /website/content/docs/asset-server/configuration.md: -------------------------------------------------------------------------------- 1 | # Asset server Configuration 2 | 3 | Static file serving: 4 | ```toml 5 | [[assets.static]] 6 | # ./assets/* -> http://localhost:8080/assets/ 7 | dir = "assets" # Relative to configuration file 8 | path = "/assets" 9 | ``` 10 | 11 | Template file serving: 12 | ```toml 13 | [[assets.template]] 14 | # ./templates/name.html -> http://localhost:8080/html/name/param 15 | dir = "templates" 16 | path = "/html" 17 | ``` 18 | 19 | QGIS plugin repository: 20 | ```toml 21 | [[assets.repo]] 22 | # ./plugins/*.zip -> http://localhost:8080/qgisrepo/plugins.xml 23 | dir = "plugins" 24 | path = "/qgisrepo" 25 | ``` 26 | -------------------------------------------------------------------------------- /website/content/docs/core/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 3 3 | next: docs/core/configuration 4 | --- 5 | -------------------------------------------------------------------------------- /website/content/docs/core/configuration.md: -------------------------------------------------------------------------------- 1 | # Core Configuration 2 | 3 | Configuration is read from `bbox.toml` and environment variables. 4 | 5 | ## Webserver 6 | 7 | ```toml 8 | [webserver] 9 | # Web server settings 10 | # Environment variable prefix: BBOX_WEBSERVER__ 11 | server_addr = "0.0.0.0:8080" # Default: 127.0.0.1:8080 12 | # worker_threads = 4 # Default: number of CPU cores 13 | loglevel = "Info" # Error, Warn, Info, Debug, Trace 14 | ``` 15 | -------------------------------------------------------------------------------- /website/content/docs/feature-server/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 6 3 | next: docs/feature-server/configuration 4 | --- 5 | 6 | # BBOX OGC API Features Service 7 | 8 | Asynchronous OGC API Features server implementation. 9 | 10 | Features: 11 | - [x] OGC API - Features - Part 1: Core 1.0 12 | - [ ] OGC API - Features - Part 2: Coordinate Reference Systems by Reference 1.0 13 | - [x] Builtin storage backends: PostGIS, GeoPackage 14 | - [x] SQL queries with time and custom query parameters 15 | - [x] Output formats: GeoJSON 16 | - [x] Compatibility: WFS + WFS-T via QGIS Server 17 | 18 | 19 | ## Usage 20 | 21 | Run feature server with `bbox.toml` configuration: 22 | 23 | bbox-feature-server serve 24 | 25 | or with a custom configuration: 26 | 27 | bbox-feature-server --config=bbox-pg.toml serve 28 | -------------------------------------------------------------------------------- /website/content/docs/feature-server/configuration.md: -------------------------------------------------------------------------------- 1 | # Feature server configuration 2 | 3 | ## Datasources 4 | 5 | ```toml 6 | [[datasource]] 7 | name = "mvtbenchdb" 8 | [datasource.postgis] 9 | url = "postgresql://mvtbench:mvtbench@127.0.0.1:5439/mvtbench" 10 | 11 | [[datasource]] 12 | name = "ne_extracts" 13 | [datasource.gpkg] 14 | path = "../data/ne_extracts.gpkg" 15 | ``` 16 | 17 | ## Collections with auto discovery 18 | 19 | ```toml 20 | [[collections.postgis]] 21 | url = "postgresql://mvtbench:mvtbench@127.0.0.1:5439/mvtbench" 22 | 23 | [[collections.directory]] 24 | dir = "../data" # Relative to configuration file 25 | ``` 26 | 27 | ## Collections 28 | 29 | ```toml 30 | [[collection]] 31 | name = "populated_places" 32 | title = "populated places" 33 | description = "Natural Earth populated places" 34 | [collection.gpkg] 35 | datasource = "ne_extracts" 36 | table_name = "ne_10m_populated_places" 37 | ``` 38 | 39 | With custom SQL query: 40 | ```toml 41 | [[collection]] 42 | name = "populated_places_names" 43 | title = "populated places names" 44 | description = "Natural Earth populated places" 45 | [collection.gpkg] 46 | datasource = "ne_extracts" 47 | sql = "SELECT fid, name, geom FROM ne_10m_populated_places" 48 | geometry_field = "geom" 49 | fid_field = "fid" 50 | ``` 51 | 52 | Collections with a PostGIS datasource: 53 | ```toml 54 | [[collection]] 55 | name = "states_provinces_lines" 56 | title = "States/provinces borders" 57 | description = "Natural Earth states/provinces borders" 58 | [collection.postgis] 59 | datasource = "mvtbenchdb" 60 | table_name = "ne_10m_admin_1_states_provinces_lines" 61 | 62 | [[collection]] 63 | name = "country_labels" 64 | title = "Country names" 65 | description = "Natural Earth country names" 66 | [collection.postgis] 67 | datasource = "mvtbenchdb" 68 | sql = "SELECT fid, abbrev, name, wkb_geometry FROM ne_10m_admin_0_country_points" 69 | geometry_field = "wkb_geometry" 70 | fid_field = "fid" 71 | ``` 72 | 73 | With queriable fields: 74 | ```toml 75 | [[collection]] 76 | name = "gpstracks" 77 | title = "GPS tracks" 78 | description = "Daily GPS tracks" 79 | [collection.postgis] 80 | datasource = "trackingdb" 81 | sql = "SELECT id, date, ST_Point(lon, lat, 4326) AS geom FROM gpslog" 82 | geometry_field = "geom" 83 | fid_field = "id" 84 | queryable_fields = ["date"] 85 | ``` 86 | 87 | Queriable fields are passed by name: `/collections/gpstracks/items?date=2024-11-08` 88 | 89 | Temporal filters can be applied by configuring `temporal_field` and optionally `temporal_end_field`. 90 | -------------------------------------------------------------------------------- /website/content/docs/feature-server/endpoints.md: -------------------------------------------------------------------------------- 1 | # BBOX API Endpoints 2 | 3 | Services are available via the HTTP `GET` endpoints: 4 | 5 | | URL | Description | 6 | |----------------------------------|---------------------| 7 | | `/collections` | List of collections | 8 | | `/collections/{name}/items` | Collection items | 9 | | `/collections/{name}/items/{id}` | Single item | 10 | 11 | 12 | ## Request examples 13 | 14 | Inspect collections: 15 | 16 | x-www-browser http://127.0.0.1:8080/collections 17 | 18 | Feature requests: 19 | 20 | curl -s http://127.0.0.1:8080/collections/populated_places/items | jq . 21 | 22 | curl -s http://127.0.0.1:8080/collections/populated_places_names/items/2 | jq . 23 | -------------------------------------------------------------------------------- /website/content/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 2 3 | next: docs/core/configuration 4 | --- 5 | 6 | ## Binary Distributions 7 | 8 | You can download BBOX from [GitHub releases page](https://github.com/bbox-services/bbox/releases). 9 | 10 | | Platform | Downloads (latest) | 11 | |-----------|---------------------------| 12 | | Linux | [64-bit][rl-linux-tar] | 13 | | Linux ARM | [ARM64][rl-linux-arm-tar] | 14 | | macOS | [64-bit][rl-macos-tar] | 15 | | macOS ARM | [ARM64][rl-macos-arm-tar] | 16 | | Windows | [64-bit][rl-win64-zip] | 17 | 18 | [rl-linux-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-server-x86_64-unknown-linux-gnu.tar.gz 19 | [rl-linux-arm-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-server-aarch64-unknown-linux-gnu.tar.gz 20 | [rl-macos-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-server-x86_64-apple-darwin.tar.gz 21 | [rl-macos-arm-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-server-aarch64-apple-darwin.tar.gz 22 | [rl-win64-zip]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-server-x86_64-pc-windows-msvc.zip 23 | 24 | ## Installing with Cargo 25 | 26 | If you [install Rust](https://www.rust-lang.org/tools/install), you can install BBOX from crates.io: 27 | 28 | ```shell 29 | cargo install bbox-server --locked 30 | # or 31 | cargo binstall bbox-server 32 | 33 | bbox-server --help 34 | ``` 35 | 36 | ## Docker 37 | 38 | BBOX is also available as a [Docker image](https://hub.docker.com/r/sourcepole/bbox-server-qgis). You can either share a configuration file from the host with the container via the `-v` param, or run BBOX in auto-discovery mode. 39 | 40 | ```shell 41 | docker run --rm --user=$UID -p 8080:8080 -v $PWD/bbox.toml:/bbox.toml:ro -v $PWD/assets:/assets:ro sourcepole/bbox-server-qgis:v0.6.2 42 | ``` 43 | -------------------------------------------------------------------------------- /website/content/docs/instrumentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 12 3 | --- 4 | 5 | # Instrumentation 6 | 7 | ## Configuration 8 | 9 | ### Prometheus metrics 10 | 11 | ```toml 12 | [metrics.prometheus] 13 | # Prometheus metrics endpoint 14 | # Environment variable prefix: BBOX_METRICS__PROMETHEUS__ 15 | path = "/metrics" 16 | ``` 17 | 18 | ### Jaeger tracing 19 | 20 | ```toml 21 | [metrics.jaeger] 22 | # Environment variable prefix: BBOX_METRICS__JAEGER__ 23 | agent_endpoint = "localhost:6831" 24 | ``` 25 | 26 | ## Applications 27 | 28 | ### Prometheus 29 | 30 | 31 | 32 | Run Prometheus: 33 | 34 | docker run --rm -p 127.0.0.1:9090:9090 -v $PWD/instrumentation/prometheus.yml:/etc/prometheus/prometheus.yml:ro prom/prometheus 35 | 36 | Test expression browser: 37 | 38 | x-www-browser http://localhost:9090/ 39 | 40 | Expression example: 41 | 42 | http_requests_duration_bucket 43 | 44 | 45 | ### Jaeger tracing 46 | 47 | Run jaeger in background: 48 | 49 | docker run --rm -d -p 6831:6831/udp -p 6832:6832/udp -p 16686:16686 jaegertracing/all-in-one:latest 50 | 51 | View spans: 52 | 53 | x-www-browser http://localhost:16686/ 54 | 55 | 56 | ### Grafana 57 | 58 | 59 | 60 | Run Grafana: 61 | 62 | docker run -rm -p 127.0.0.1:3000:3000 grafana/grafana 63 | 64 | Open Grafana: 65 | 66 | x-www-browser http://localhost:3000/ 67 | 68 | - Enter `admin` for username and password 69 | - Add Prometheus datasource with URL http://172.17.0.1:9090/ 70 | - Add Jaeger datasource with URL http://172.17.0.1:16686/ 71 | 72 | Average request duration: 73 | 74 | rate(http_requests_duration_sum[5m])/rate(http_requests_duration_count[5m]) 75 | 76 | Request duration 90th percentile 77 | 78 | histogram_quantile(0.9, rate(http_requests_duration_bucket[5m])) 79 | 80 | 81 | 82 | WMS Endpoint: 83 | 84 | http_requests_duration_sum{endpoint="/qgis/{project:.+}"} 85 | -------------------------------------------------------------------------------- /website/content/docs/map-server/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 7 3 | next: docs/map-server/configuration 4 | --- 5 | 6 | # BBOX map server 7 | 8 | Asynchronous map server with FCGI backend. 9 | 10 | Features: 11 | - [x] OGC WMS 1.3 Server 12 | - [ ] OGC API – Maps (Draft) 13 | - [x] FCGI backends: 14 | - [x] QGIS Server 15 | - [x] UNN Mapserver 16 | - [x] Instrumentation data for WMS backends 17 | - [x] FCGI dispatcher optimized for WMS requests 18 | 19 | 20 | ## Usage 21 | 22 | Run map server with `bbox.toml` configuration: 23 | 24 | bbox-map-server serve 25 | -------------------------------------------------------------------------------- /website/content/docs/map-server/configuration.md: -------------------------------------------------------------------------------- 1 | # Map Server Configuration 2 | 3 | ## Map server settings 4 | 5 | ```toml 6 | [mapserver] 7 | # num_fcgi_processes = 4 # Default: number of CPU cores 8 | # wait_timeout = 30000 # FCGI wait timeout in ms. Default: 90s 9 | # search_projects = false # Scan directories and build inventory 10 | ``` 11 | 12 | ## QGIS Server settings 13 | 14 | ```toml 15 | [mapserver.qgis_backend] 16 | project_basedir = "./projects" # Base dir for project files (.qgs, .qgz) 17 | qgs.path = "/qgis" # URL base path *.qgs 18 | qgz.path = "/qgz" # URL base path *.qgz 19 | ``` 20 | 21 | ## UMN MapServer settings 22 | 23 | ```toml 24 | [mapserver.umn_backend] 25 | project_basedir = "./maps" # Base dir for project files (.map) 26 | path = "/wms/map" # URL base path 27 | ``` 28 | -------------------------------------------------------------------------------- /website/content/docs/map-server/endpoints.md: -------------------------------------------------------------------------------- 1 | # BBOX API Endpoints 2 | 3 | Services are available via the following HTTP endpoints: 4 | 5 | | URL | Description | 6 | |----------------------------------|-------------------------------------------------------| 7 | | `/{prefix}/{project}` | WMS map endpoint with configurable prefix per backend | 8 | 9 | Example configurations: 10 | 11 | | URL | Description | 12 | |-------------------|----------------------------------------------| 13 | | `/qgis/{project}` | QGIS Server backend with *.qgs project files | 14 | | `/qgz/{project}` | QGIS Server backend with *.qgz project files | 15 | | `/map/{project}` | UMN Mapserver backend | 16 | 17 | 18 | WMS request examples: 19 | 20 | curl -s 'http://127.0.0.1:8080/qgis/ne?SERVICE=WMS&REQUEST=GetCapabilities' 21 | 22 | curl -o /tmp/map.png 'http://127.0.0.1:8080/qgis/ne?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=-20037508.34278924391,-5966981.031407224014,19750246.20310878009,17477263.06060761213&CRS=EPSG:900913&WIDTH=1399&HEIGHT=824&LAYERS=country&STYLES=&FORMAT=image/png;%20mode%3D8bit' 23 | 24 | curl -o /tmp/legend.png 'http://127.0.0.1:8080/qgis/ne?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetLegendGraphic&LAYER=country&FORMAT=image/png&STYLE=default&TRANSPARENT=true' 25 | 26 | curl -s 'http://127.0.0.1:8080/qgis/helloworld?SERVICE=WMS&REQUEST=GetProjectSettings' 27 | 28 | curl -o /tmp/print.pdf 'http://127.0.0.1:8080/qgis/helloworld' -X POST \ 29 | -d 'SERVICE=WMS&VERSION=1.3.0&REQUEST=GetPrint&FORMAT=pdf' \ 30 | -d 'TEMPLATE=Composer 1&DPI=300&CRS=EPSG:4326' \ 31 | -d 'map0:LAYERS=Country,Hello&map0:extent=-92.8913,-185.227,121.09,191.872' 32 | 33 | UMN Mapserver: 34 | 35 | curl -s 'http://127.0.0.1:8080/wms/map/ne?SERVICE=WMS&REQUEST=GetCapabilities' 36 | 37 | curl -o /tmp/map.png 'http://127.0.0.1:8080/wms/map/ne?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&BBOX=40.83354209954528358,0.542981257600549938,49.84069885574058389,15.5221558872974672&CRS=epsg:4326&WIDTH=1372&HEIGHT=825&LAYERS=country&STYLES=&FORMAT=image%2Fpng%3B%20mode%3D8bit' 38 | 39 | 40 | WFS request examples: 41 | 42 | curl -s 'http://127.0.0.1:8080/qgis/ne?SERVICE=WFS&REQUEST=GetCapabilities' 43 | 44 | curl -s 'http://127.0.0.1:8080/qgis/ne?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=country&SRSNAME=EPSG:3857&BBOX=1059483.34824404888786376,5959680.16110791172832251,1061700.73825845750980079,5962445.67000228632241488,EPSG:3857' 45 | 46 | curl -s --data @wfsadd.xml 'http://127.0.0.1:8080/qgis/ne?SERVICE=WFS&REQUEST=Transaction' 47 | -------------------------------------------------------------------------------- /website/content/docs/processes-server/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 10 3 | next: docs/processes-server/configuration 4 | --- 5 | 6 | # BBOX Processes Service 7 | 8 | The OGC API - Processes standard specifies an interface for executing computational tasks. 9 | 10 | Overview: https://ogcapi.ogc.org/processes/ 11 | 12 | Features: 13 | - [ ] OGC API - Processes - Part 1: Core 14 | - [x] Support synchronous and asynchronous process execution 15 | - [x] OpenAPI endpoint 16 | - [x] Multiple backend engines 17 | - [x] [Dagster](https://dagster.io/) 18 | - [ ] [Windmill](https://www.windmill.dev/) 19 | 20 | 21 | ## Usage 22 | 23 | Run feature server with `bbox.toml` configuration: 24 | 25 | bbox-processes-server serve 26 | -------------------------------------------------------------------------------- /website/content/docs/processes-server/configuration.md: -------------------------------------------------------------------------------- 1 | # Processes Service configuration 2 | 3 | ## Dagster Backend 4 | 5 | ```toml 6 | [processes.dagster_backend] 7 | graphql_url = "http://localhost:3000/graphql" 8 | repository_name = "the_repository" 9 | repository_location_name = "the.repos" 10 | ``` 11 | -------------------------------------------------------------------------------- /website/content/docs/processes-server/endpoints.md: -------------------------------------------------------------------------------- 1 | # BBOX API Endpoints 2 | 3 | Services are available via the following HTTP endpoints: 4 | 5 | | URL | Description | 6 | |-------------------------------|-----------------------------| 7 | | `/processes` | List of available processes | 8 | | `/processes/{name}/execution` | Execute processes | 9 | | `/processes/jobs/{jobid}` | Job status | 10 | | `/processes/{jobid}/results` | Job results | 11 | 12 | 13 | ## Request examples 14 | 15 | Execute process: 16 | 17 | curl --header "Content-Type: application/json" \ 18 | --request POST \ 19 | --data '{"inputs": {"ops": {"pos_info_query": {"inputs": {"pos_x": 2607545, "pos_y": 1171421}}}}}' \ 20 | http://localhost:8080/processes/pos_info/execution 21 | 22 | Execute process asynchronous: 23 | 24 | curl --header "Content-Type: application/json" \ 25 | --header "Prefer: respond-async" \ 26 | --request POST \ 27 | --data '{"inputs": {"ops": {"export_fpds2": {"inputs": {"fixpunkte": ["12575280", "12575100"], "in_bearbeitung": false }}}}}' \ 28 | http://localhost:8080/processes/export_fpds2_to_csv/execution 29 | 30 | JOBID=386f6c55-d718-4160-b4df-afc5ad5c7a73 31 | 32 | Get job status: 33 | 34 | curl http://localhost:8080/jobs/$JOBID 35 | 36 | Return result of a job: 37 | 38 | curl http://localhost:8080/jobs/$JOBID/results 39 | -------------------------------------------------------------------------------- /website/content/docs/routing-server/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 11 3 | next: docs/routing-server/configuration 4 | --- 5 | 6 | # BBOX Routing Service (EXPERIMENTAL) 7 | 8 | Routing services with Contraction Hierarchy. 9 | 10 | Features: 11 | - [ ] OGC API - Routes - Part 1: Core 12 | - [x] Multiple search APIs 13 | - [x] OGC API route requests 14 | - [x] Basic from/to requests 15 | - [x] Valhalla API compatible requests 16 | - [x] Builtin storage backends: PostGIS, GeoPackage 17 | - [ ] Extract routing graphs from OSM planet files 18 | 19 | 20 | ## Usage 21 | 22 | Run tile server with `bbox.toml` configuration: 23 | 24 | bbox-routing-server serve 25 | -------------------------------------------------------------------------------- /website/content/docs/routing-server/configuration.md: -------------------------------------------------------------------------------- 1 | # Routing Service Configuration 2 | 3 | ## GeoPackage line geometry table 4 | 5 | ```toml 6 | [[routing.service]] 7 | profile = "railway" 8 | gpkg = "assets/railway-test.gpkg" 9 | table = "flows" 10 | geom = "geom" 11 | ``` 12 | 13 | ## PostGIS Edge/Vertices tables 14 | 15 | ```toml 16 | # Node search distance 17 | search_dist = 0.01 18 | # Edge table 19 | table = "rail_arcs" 20 | # Node/Vertices table 21 | node_table = "rail_arcs_vertices_pgr" 22 | # Geometry column 23 | geom = "geom" 24 | # Node ID column in node table 25 | node_id = "id" 26 | # Cost column 27 | cost = "cost" 28 | # Column with source node ID 29 | node_src = "source" 30 | # Column with destination (target) node ID 31 | node_dst = "target" 32 | ``` 33 | 34 | This assumes tables created e.g. with PgRouting `pgr_createTopology`. 35 | 36 | The contraction hierarchy is created on first startup and stored as cache files named `.graph.bin` and `.nodes.bin` 37 | -------------------------------------------------------------------------------- /website/content/docs/routing-server/endpoints.md: -------------------------------------------------------------------------------- 1 | # BBOX API Endpoints 2 | 3 | Services are available via the following HTTP endpoints: 4 | 5 | | URL | Description | 6 | |--------------------|------------------------------| 7 | | `/routes` | OGC API endpoint | 8 | | `/routes/basic` | Basic from/to API endpoint | 9 | | `/routes/valhalla` | Valhalla compatible endpoint | 10 | 11 | 12 | ## Request examples 13 | 14 | ### OGC API 15 | 16 | curl -s -X 'POST' \ 17 | 'http://localhost:8080/routes?mode=sync' \ 18 | -H 'accept: application/geo+json' \ 19 | -H 'Content-Type: application/json' \ 20 | -d '{ 21 | "name": "Route from A to B", 22 | "waypoints": { 23 | "type": "MultiPoint", 24 | "coordinates": [ 25 | [9.35213353, 47.0935012], 26 | [9.3422712, 47.1011887] 27 | ] 28 | }, 29 | "preference": "fastest", 30 | "dataset": "OSM" 31 | }' 32 | 33 | ### Basic from/to request: 34 | 35 | curl -s 'http://localhost:8080/routes/basic?profile=railway&from_pos=9.35213353,47.0935012&to_pos=9.3422712,47.1011887' 36 | 37 | Zurich - Munich: 38 | 39 | curl -s 'http://localhost:8080/routes/basic?profile=railway&from_pos=8.53636,47.37726&to_pos=11.56096,48.14019' 40 | 41 | 42 | ### Valhalla endpoint 43 | 44 | Base URL example for Valhalla QGIS Plugin: http://localhost:8080/routes/valhalla 45 | -------------------------------------------------------------------------------- /website/content/docs/running.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 4 3 | --- 4 | 5 | # Running BBOX 6 | 7 | ## Command line options 8 | 9 | ```shell 10 | Usage: bbox-server [OPTIONS] 11 | 12 | Commands: 13 | serve Run service 14 | seed Seed tiles 15 | upload Upload tiles 16 | help Print this message or the help of the given subcommand(s) 17 | 18 | Options: 19 | -c, --config Config file (Default: bbox.toml) 20 | --loglevel Log level (Default: info) [possible values: error, warn, info, debug, trace] 21 | -t, --t-rex-config T-Rex config file 22 | -h, --help Print help 23 | ``` 24 | 25 | ```shell 26 | Usage: bbox-server serve [FILE_OR_URL] 27 | 28 | Arguments: 29 | [FILE_OR_URL] Serve service from file or URL 30 | 31 | Options: 32 | -h, --help Print help 33 | ``` 34 | 35 | ## Access Web Backend 36 | 37 | x-www-browser http://127.0.0.1:8080/ 38 | 39 | 40 | ## Service components 41 | 42 | Service components are included in `bbox-server`, but can also be run as standalone service: 43 | 44 | - `bbox-feature-server`: [Feature server](feature-server/) 45 | - `bbox-map-server`: [Map server](map-server/) 46 | - `bbox-tile-server`: [Tile Server](tile-server/) 47 | - `bbox-asset-server`: [Asset server](asset-server/) 48 | - `bbox-processes-server`: [Processes server](processes-server/) 49 | - `bbox-routing-server`: [Routing server](routing-server/) 50 | 51 | 52 | ## Docker 53 | 54 | docker run -p 8080:8080 sourcepole/bbox-server-qgis 55 | 56 | Serve tiles from file: 57 | 58 | docker run -p 8080:8080 -v $PWD/assets:/assets:ro sourcepole/bbox-server-qgis bbox-server serve /assets/liechtenstein.mbtiles 59 | 60 | Run with configuration file: 61 | 62 | docker run -p 8080:8080 -v $PWD/bbox.toml:/var/www/bbox.toml:ro -v $PWD/assets:/var/www/assets:ro sourcepole/bbox-server-qgis 63 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 8 3 | next: docs/tile-server/installation 4 | --- 5 | 6 | # BBOX tile server 7 | 8 | Map tile serving and tile cache seeding. 9 | 10 | Features: 11 | - [x] OGC API – Tiles - Part 1: Core 1.0 12 | - [x] Vector tile server 13 | - [X] Vector data source: PostGIS 14 | - [X] Tile archives: MBTiles, PMTiles 15 | - [x] Raster tile server (Backends: QGIS Server and MapServer) 16 | - [x] Tile proxy server (WMS backend) 17 | - [x] XYZ tile service endpoint with TileJSON metadata 18 | - [x] Support for Custom Tile Matrix Sets 19 | - [ ] OGC WMTS (via map service backend) 20 | 21 | Tile seeder features: 22 | - [x] Parallelized seeding of raster and vector tiles 23 | - [x] Storage backends: Files, S3, MBTiles, PMTiles 24 | 25 | 26 | ## Usage 27 | 28 | Run tile server with `bbox.toml` configuration: 29 | 30 | bbox-tile-server serve 31 | 32 | Run tile server with auto discovery: 33 | 34 | bbox-tile-server serve ../assets/liechtenstein.mbtiles 35 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 2 3 | --- 4 | 5 | # Tile server configuration 6 | 7 | ## Datasources 8 | 9 | ```toml 10 | [[datasource]] 11 | name = "mvtbenchdb" 12 | [datasource.postgis] 13 | url = "postgresql://mvtbench:mvtbench@127.0.0.1:5439/mvtbench" 14 | 15 | [[datasource]] 16 | name = "gebco" 17 | [datasource.wms_proxy] 18 | baseurl = "https://wms.gebco.net/mapserv?version=1.3.0" 19 | format = "image/jpeg" 20 | ``` 21 | 22 | ## Vector tiles from PostGIS table 23 | 24 | ```toml 25 | [[tileset]] 26 | name = "ne_countries" 27 | [tileset.postgis] 28 | datasource = "mvtbenchdb" 29 | extent = [-179.97277, -83.05457, 179.99366, 83.23559] 30 | attribution = "Natural Earth v4" 31 | 32 | [[tileset.postgis.layer]] 33 | name = "country-name" 34 | #table_name = "ne_10m_admin_0_country_points" 35 | geometry_type = "POINT" 36 | [[tileset.postgis.layer.query]] 37 | sql = """SELECT wkb_geometry, abbrev, name FROM ne_10m_admin_0_country_points""" 38 | ``` 39 | 40 | Query with a custom runtime parameter: 41 | ```toml 42 | sql = """ 43 | SELECT id, date, ST_Point(lon, lat, 4326) AS geom 44 | FROM gpslog 45 | WHERE date = !date! 46 | """ 47 | ``` 48 | 49 | A custom parameter is passed by name: `/xyz/gpstracks/0/0/0.mvt?date=2024-11-08` 50 | 51 | 52 | ## Raster tiles from map service 53 | 54 | QGIS Server backend: 55 | ```toml 56 | [[tileset]] 57 | name = "ne_extracts" 58 | map_service = { project = "ne_extracts", suffix = "qgz", layers = "ne_extracts" } 59 | cache = "tilecache" 60 | ``` 61 | 62 | UMN Mapserver backend: 63 | ```toml 64 | [[tileset]] 65 | name = "ne_umn" 66 | map_service = { project = "ne", suffix = "map", layers = "country", tile_size = 512 } 67 | ``` 68 | 69 | Raster tiles from external WMS: 70 | ```toml 71 | [[tileset]] 72 | name = "gebco" 73 | wms_proxy = { source = "gebco", layers = "gebco_latest" } 74 | ``` 75 | 76 | ## Tile caches 77 | 78 | ```toml 79 | [[tilecache]] 80 | name = "tilecache" 81 | [tilecache.files] 82 | base_dir = "/tmp/tilecache" 83 | 84 | [[tilecache]] 85 | name = "aws" 86 | [tilecache.s3] 87 | path = "s3://tiles" 88 | 89 | [[tilestore]] 90 | name = "mbtilecache" 91 | [tilestore.mbtiles] 92 | path = "/tmp/tilecache.mbtiles" 93 | 94 | [[tilestore]] 95 | name = "pmtilecache" 96 | [tilestore.pmtiles] 97 | path = "/tmp/tilecache.pmtiles" 98 | ``` 99 | 100 | To use a tilecache when serving tiles, add the tilecache name to the tileset: 101 | 102 | ```toml 103 | [[tileset]] 104 | name = "ne_countries" 105 | cache = "tilecache" 106 | ``` 107 | 108 | ## Custom tile grid 109 | 110 | ```toml 111 | [[grid]] 112 | json = "assets/custom-grid-lv95.json" 113 | ``` 114 | 115 | To use the custom tile grid, add the tms name to the tileset: 116 | 117 | ```toml 118 | [[tileset]] 119 | name = "rivers_lakes" 120 | [[tileset.tms]] 121 | id = "LV95" 122 | ``` 123 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/endpoints.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 4 3 | --- 4 | 5 | # BBOX API Endpoints 6 | 7 | Services are available via the following HTTP endpoints: 8 | 9 | | URL | Description | 10 | |---------------------------------------|-----------------------------| 11 | | `/tiles` | List of available tilesets | 12 | | `/tiles/{tileset}` | Tileset metadata | 13 | | `/map/tiles/{grid}/{z}/{x}/{y}` | Map tiles endpoint | 14 | | `/xyz/{tileset}/{z}/{x}/{y}.{format}` | XYZ tile endpoint | 15 | | `/xyz/{tileset}.json` | Tilejson endpoint | 16 | | `/xyz/{tileset}.style.json` | Generic Style JSON endpoint | 17 | | `/xyz/{tileset}/metadata.json` | MBTiles metadata JSON | 18 | 19 | ## Request examples 20 | 21 | Tile requests: 22 | 23 | curl -o /tmp/tile.png http://localhost:8080/xyz/ne_extracts/2/2/2.png 24 | 25 | curl -o /tmp/tile.png http://localhost:8080/xyz/ne_umn/2/2/2.png 26 | 27 | curl -o /tmp/tile.jpg http://localhost:8080/xyz/gebco/0/0/0.jpeg 28 | 29 | curl -o /tmp/tile.mvt http://localhost:8080/xyz/mbtiles_mvt_fl/14/8621/5759.mvt 30 | 31 | curl -o /tmp/tilegz.mvt -H 'Content-Encoding: gzip' http://localhost:8080/xyz/mbtiles_mvt_fl/14/8621/5759.mvt 32 | 33 | curl -o /tmp/tile.png -H 'Accept: image/png; mode=8bit' http://localhost:8080/map/tiles/WebMercatorQuad/2/2/2 34 | 35 | curl -o /tmp/tile.mvt http://localhost:8080/xyz/liechtenstein/14/8621/5759.mvt 36 | 37 | XYZ URL (Leaflet, QGIS, etc.): 38 | 39 | http://localhost:8080/xyz/ne_extracts/{z}/{x}/{y}.png 40 | 41 | Tilejson requests: 42 | 43 | curl -s http://localhost:8080/xyz/mbtiles_mvt_fl.json | jq . 44 | 45 | Style JSON requests: 46 | 47 | curl -s http://localhost:8080/xyz/mbtiles_mvt_fl.style.json | jq . 48 | 49 | curl -s http://localhost:8080/xyz/ne_extracts.style.json | jq . 50 | 51 | Map viewer examples: 52 | 53 | x-www-browser http://127.0.0.1:8080/assets/usergrid.html?debug=1 54 | 55 | Map viewer template examples: 56 | 57 | x-www-browser http://localhost:8080/html/maplibre/mbtiles_mvt_fl?style=/assets/mbtiles_mvt_fl-style.json 58 | 59 | With PostGIS Service: 60 | 61 | docker run -p 127.0.0.1:5439:5432 -d --name mvtbenchdb --rm sourcepole/mvtbenchdb 62 | 63 | curl -s http://localhost:8080/xyz/ne_countries.style.json | jq . 64 | x-www-browser http://localhost:8080/html/maplibre/ne_countries 65 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 1 3 | --- 4 | 5 | ## Binary Distributions 6 | 7 | You can download BBOX tile server from [GitHub releases page](https://github.com/bbox-services/bbox/releases). 8 | 9 | | Platform | Downloads (latest) | 10 | |-----------|---------------------------| 11 | | Linux | [64-bit][rl-linux-tar] | 12 | | Linux ARM | [ARM64][rl-linux-arm-tar] | 13 | | macOS | [64-bit][rl-macos-tar] | 14 | | macOS ARM | [ARM64][rl-macos-arm-tar] | 15 | | Windows | [64-bit][rl-win64-zip] | 16 | 17 | [rl-linux-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server-x86_64-unknown-linux-gnu.tar.gz 18 | [rl-linux-arm-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server-aarch64-unknown-linux-gnu.tar.gz 19 | [rl-macos-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server-x86_64-apple-darwin.tar.gz 20 | [rl-macos-arm-tar]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server-aarch64-apple-darwin.tar.gz 21 | [rl-win64-zip]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server-x86_64-pc-windows-msvc.zip 22 | 23 | ## Debian packages 24 | 25 | | Distribution | Downloads (latest) | 26 | |-----------------|-----------------------| 27 | | Ubuntu Jammy | [x86_64][deb-jammy] | 28 | | Debian Bookworm | [x86_64][deb-bookworm] | 29 | | Debian Bullseye | [x86_64][deb-bullseye] | 30 | 31 | [deb-jammy]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server_0.6.2-jammy_amd64.deb 32 | [deb-bookworm]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server_0.6.2-bookworm_amd64.deb 33 | [deb-bullseye]: https://github.com/bbox-services/bbox/releases/download/v0.6.2/bbox-tile-server_0.6.2-bullseye_amd64.deb 34 | 35 | ## Installing with Cargo 36 | 37 | If you [install Rust](https://www.rust-lang.org/tools/install), you can install BBOX from crates.io: 38 | 39 | ```shell 40 | cargo install bbox-tile-server --locked 41 | # or 42 | cargo binstall bbox-tile-server 43 | 44 | bbox-tile-server --help 45 | ``` 46 | 47 | ## Docker 48 | 49 | BBOX tile server is also available as a [Docker image](https://hub.docker.com/r/sourcepole/bbox-tile-server). You can either share a configuration file from the host with the container via the `-v` param, or you can run BBOX in auto-discover mode. 50 | 51 | ```shell 52 | docker run --rm --user=$UID -p 8080:8080 -v $PWD/bbox.toml:/bbox.toml:ro -v $PWD/assets:/assets:ro sourcepole/bbox-server-qgis:v0.6.2 53 | ``` 54 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/running.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 3 3 | --- 4 | 5 | # Running BBOX tile server 6 | 7 | ## Command line options 8 | 9 | ```shell 10 | Usage: bbox-tile-server [OPTIONS] 11 | 12 | Commands: 13 | serve Run service 14 | seed Seed tiles 15 | upload Upload tiles 16 | help Print this message or the help of the given subcommand(s) 17 | 18 | Options: 19 | -c, --config Config file (Default: bbox.toml) 20 | --loglevel Log level (Default: info) [possible values: error, warn, info, debug, trace] 21 | -t, --t-rex-config T-Rex config file 22 | -h, --help Print help 23 | ``` 24 | 25 | ```shell 26 | Usage: bbox-tile-server serve [FILE_OR_URL] 27 | 28 | Arguments: 29 | [FILE_OR_URL] Serve service from file or URL 30 | 31 | Options: 32 | -h, --help Print help 33 | ``` 34 | 35 | ```shell 36 | Usage: bbox-tile-server seed [OPTIONS] --tileset [FILE_OR_URL] 37 | 38 | Arguments: 39 | [FILE_OR_URL] Read tiles from file or URL 40 | 41 | Options: 42 | --tileset tile set name 43 | --minzoom Minimum zoom level 44 | --maxzoom Maximum zoom level 45 | --tms tile matrix set id 46 | --extent Extent minx,miny,maxx,maxy (in grid reference system) 47 | --tile-path Base directory for file store 48 | --s3-path S3 path to upload to (e.g. s3://tiles) 49 | --mb-path MBTiles path to store tiles 50 | --pm-path PMTiles path to store tiles 51 | --no-store No tile store (for read benchmarks) 52 | -t, --threads Number of threads to use, defaults to number of logical cores 53 | --tasks Size of tasks queue for parallel processing 54 | --overwrite Overwrite previously cached tiles [possible values: true, false] 55 | -h, --help Print help 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/seeding.md: -------------------------------------------------------------------------------- 1 | --- 2 | weight: 5 3 | --- 4 | 5 | # Tile seeding 6 | 7 | Seed with proxy WMS: 8 | 9 | bbox-tile-server seed --tileset=gebco --tile-path=/tmp/tiles/gebco --maxzoom=2 10 | 11 | Seed with embedded map service: 12 | 13 | bbox-tile-server seed --tileset=ne_extracts --tile-path=/tmp/tiles/ne_extracts --maxzoom=2 14 | 15 | Seed PostGIS MVT tiles: 16 | 17 | bbox-tile-server seed --tileset=ne_countries --tile-path=/tmp/tiles/ne_countries --maxzoom=2 18 | 19 | ## Seed to S3 storage 20 | 21 | Set S3 env vars: 22 | 23 | export S3_ENDPOINT_URL="http://localhost:9000" 24 | export AWS_ACCESS_KEY_ID=miniostorage 25 | export AWS_SECRET_ACCESS_KEY=miniostorage 26 | 27 | Seed raster tiles: 28 | 29 | bbox-tile-server seed --tileset=ne_extracts --s3-path=s3://tiles --maxzoom=5 30 | 31 | ## Seed to MBTiles archive 32 | 33 | bbox-tile-server seed --mb-path=/tmp/mvtbench.mbtiles --tileset=ne_countries --maxzoom=6 34 | 35 | ## Seed to PMTiles archive 36 | 37 | bbox-tile-server seed --pm-path=/tmp/mvtbench.pmtiles --tileset=ne_countries --maxzoom=6 38 | -------------------------------------------------------------------------------- /website/content/docs/tile-server/styling.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: 3 | exclude: true 4 | --- 5 | 6 | # Vector Tile styling 7 | 8 | ### Using Maputnik for MVT styling 9 | 10 | * Download latest public.zip from https://github.com/maputnik/editor/releases 11 | * Unpack into ../assets/ and rename public to maputnik 12 | 13 | Open example: 14 | 15 | http://localhost:8080/assets/maputnik/index.html?style=http://localhost:8080/assets/maplibre-style.json 16 | http://localhost:8080/assets/maputnik/index.html#11.0/47.0944/9.5076 17 | 18 | http://localhost:8080/assets/maputnik/index.html?style=http://localhost:8080/xyz/mbtiles_mvt_fl.style.json 19 | -------------------------------------------------------------------------------- /website/i18n/en.yaml: -------------------------------------------------------------------------------- 1 | copyright: "Copyright © 2020-2024 by Pirmin Kalberer" 2 | -------------------------------------------------------------------------------- /website/layouts/partials/custom/head-end.html: -------------------------------------------------------------------------------- 1 | {{ if hugo.IsProduction -}} 2 | 3 | {{ end -}} -------------------------------------------------------------------------------- /website/static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/static/apple-touch-icon.png -------------------------------------------------------------------------------- /website/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/static/favicon-16x16.png -------------------------------------------------------------------------------- /website/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/static/favicon-32x32.png -------------------------------------------------------------------------------- /website/static/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /website/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bbox-services/bbox/921d23bd54f5ac49a29307a2840161a5e8b18e75/website/static/favicon.ico -------------------------------------------------------------------------------- /website/static/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/static/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"BBOX Server","short_name":"BBOX","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} --------------------------------------------------------------------------------