├── .all-contributorsrc ├── .babelrc ├── .dockerignore ├── .eslintrc ├── .github └── workflows │ ├── docker.yml │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CHANGES.md ├── LICENSE ├── README.md ├── dist ├── cli.js ├── index.js ├── render.js └── server.js ├── docker ├── Dockerfile └── entrypoint.sh ├── package-lock.json ├── package.json ├── src ├── cli.js ├── index.js ├── render.js └── server.js └── tests ├── cli.test.js ├── fixtures ├── example-images-base64.json ├── example-images-remote.json ├── example-style-bad-glyphs.json ├── example-style-bad-source.json ├── example-style-empty.json ├── example-style-geojson-labels.json ├── example-style-geojson.json ├── example-style-image-base64-icons.json ├── example-style-image-remote-icons.json ├── example-style-image-source.json ├── example-style-mapbox-source.json ├── example-style-mbtiles-missing-source.json ├── example-style-mbtiles-source-vector.json ├── example-style-mbtiles-source.json ├── example-style-mbtiles-tiles-vector.json ├── example-style-mbtiles-tiles.json ├── example-style.json ├── expected-bad-source.png ├── expected-bearing90.png ├── expected-bounds.png ├── expected-geojson-label.png ├── expected-image-base64-icons.png ├── expected-image-remote-icons.png ├── expected-image-source.png ├── expected-mapbox-source.png ├── expected-mapbox-source@2x.png ├── expected-mbtiles-source-vector.png ├── expected-mbtiles-source.png ├── expected-mbtiles-tiles-vector.png ├── expected-mbtiles-tiles.png ├── expected-no-padding.png ├── expected-padding-25px.png ├── expected-padding25px.png ├── expected-pitch60.png ├── expected.png ├── geography-class-png.mbtiles └── land.mbtiles ├── render.test.js ├── server.test.js └── util.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "mbgl-renderer", 3 | "projectOwner": "consbio", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "none", 12 | "contributors": [ 13 | { 14 | "login": "brendan-ward", 15 | "name": "Brendan Ward", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/3375604?v=4", 17 | "profile": "https://astutespruce.com", 18 | "contributions": [ 19 | "code", 20 | "doc", 21 | "bug", 22 | "blog", 23 | "review", 24 | "ideas" 25 | ] 26 | }, 27 | { 28 | "login": "nikmolnar", 29 | "name": "Nik Molnar", 30 | "avatar_url": "https://avatars1.githubusercontent.com/u/2422416?v=4", 31 | "profile": "https://github.com/nikmolnar", 32 | "contributions": [ 33 | "code", 34 | "bug", 35 | "ideas" 36 | ] 37 | }, 38 | { 39 | "login": "ka7eh", 40 | "name": "Kaveh", 41 | "avatar_url": "https://avatars1.githubusercontent.com/u/4112646?v=4", 42 | "profile": "https://github.com/ka7eh", 43 | "contributions": [ 44 | "bug", 45 | "ideas" 46 | ] 47 | }, 48 | { 49 | "login": "bertrandmd", 50 | "name": "bertrandmd", 51 | "avatar_url": "https://avatars0.githubusercontent.com/u/9257198?v=4", 52 | "profile": "https://github.com/bertrandmd", 53 | "contributions": [ 54 | "code" 55 | ] 56 | }, 57 | { 58 | "login": "korpd", 59 | "name": "Daniel Korp", 60 | "avatar_url": "https://avatars1.githubusercontent.com/u/44464673?v=4", 61 | "profile": "https://github.com/korpd", 62 | "contributions": [ 63 | "code" 64 | ] 65 | }, 66 | { 67 | "login": "kjkurtz", 68 | "name": "Kyle Kurtz", 69 | "avatar_url": "https://avatars2.githubusercontent.com/u/6036168?v=4", 70 | "profile": "https://github.com/kjkurtz", 71 | "contributions": [ 72 | "code" 73 | ] 74 | }, 75 | { 76 | "login": "normanrz", 77 | "name": "Norman Rzepka", 78 | "avatar_url": "https://avatars1.githubusercontent.com/u/335438?v=4", 79 | "profile": "http://normanrz.com/", 80 | "contributions": [ 81 | "code" 82 | ] 83 | }, 84 | { 85 | "login": "submarcos", 86 | "name": "J-E Castagnede", 87 | "avatar_url": "https://avatars3.githubusercontent.com/u/7448208?v=4", 88 | "profile": "https://github.com/submarcos", 89 | "contributions": [ 90 | "bug" 91 | ] 92 | }, 93 | { 94 | "login": "jnicho02", 95 | "name": "Jez Nicholson", 96 | "avatar_url": "https://avatars3.githubusercontent.com/u/83251?v=4", 97 | "profile": "http://itsallinthega.me/", 98 | "contributions": [ 99 | "bug", 100 | "ideas" 101 | ] 102 | }, 103 | { 104 | "login": "bob-gray", 105 | "name": "Bob Gray", 106 | "avatar_url": "https://avatars0.githubusercontent.com/u/814812?v=4", 107 | "profile": "https://github.com/bob-gray", 108 | "contributions": [ 109 | "bug" 110 | ] 111 | }, 112 | { 113 | "login": "abraztsov", 114 | "name": "Nikita Abraztsov", 115 | "avatar_url": "https://avatars3.githubusercontent.com/u/19175580?v=4", 116 | "profile": "https://github.com/abraztsov", 117 | "contributions": [ 118 | "bug" 119 | ] 120 | }, 121 | { 122 | "login": "LePetitTim", 123 | "name": "Timothée Monty", 124 | "avatar_url": "https://avatars1.githubusercontent.com/u/26329336?v=4", 125 | "profile": "https://github.com/LePetitTim", 126 | "contributions": [ 127 | "bug" 128 | ] 129 | }, 130 | { 131 | "login": "Stunkymonkey", 132 | "name": "felix", 133 | "avatar_url": "https://avatars0.githubusercontent.com/u/1315818?v=4", 134 | "profile": "https://stunkymonkey.de", 135 | "contributions": [ 136 | "doc" 137 | ] 138 | }, 139 | { 140 | "login": "rstcruzo", 141 | "name": "Rodrigo Santa Cruz Ortega", 142 | "avatar_url": "https://avatars.githubusercontent.com/u/18235687?v=4", 143 | "profile": "https://github.com/rstcruzo", 144 | "contributions": [ 145 | "code" 146 | ] 147 | } 148 | ], 149 | "contributorsPerLine": 7 150 | } 151 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | [ 5 | "@babel/plugin-transform-runtime", 6 | { 7 | "regenerator": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /.git -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "prettier"], 3 | "plugins": ["prettier"], 4 | "env": { 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "settings": { 10 | "import/resolver": { 11 | "node": { 12 | "moduleDirectory": ["src", "node_modules"] 13 | } 14 | } 15 | }, 16 | "parser": "@babel/eslint-parser", 17 | "parserOptions": { 18 | "requireConfigFile": false, 19 | "ecmaVersion": 2017, 20 | "ecmaFeatures": { 21 | "experimentalObjectRestSpread": true, 22 | "impliedStrict": true, 23 | "classes": true 24 | }, 25 | "sourceType": "module" 26 | }, 27 | "globals": { 28 | "DEBUG": false, 29 | "MAPBOX_TOKEN": false 30 | }, 31 | "rules": { 32 | "quotes": [ 33 | 2, 34 | "single", 35 | { 36 | "avoidEscape": true, 37 | "allowTemplateLiterals": true 38 | } 39 | ], 40 | "default-case": "off", 41 | "function-paren-newline": ["error", "consistent"], 42 | "no-restricted-syntax": "off", 43 | "import/no-extraneous-dependencies": 0, 44 | "max-len": [ 45 | "warn", 46 | { 47 | "code": 120 48 | } 49 | ], 50 | "no-multi-spaces": [ 51 | "error", 52 | { 53 | "ignoreEOLComments": true 54 | } 55 | ], 56 | "no-param-reassign": [ 57 | "error", 58 | { 59 | "props": false 60 | } 61 | ], 62 | "no-plusplus": "off", 63 | "semi": ["error", "never"] 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build Docker images 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*.*.*" 9 | pull_request: 10 | paths: 11 | - ".github/workflows/docker.yml" 12 | - "docker/Dockerfile" 13 | - "docker/entrypoint.sh" 14 | workflow_dispatch: 15 | 16 | env: 17 | REGISTRY: ghcr.io 18 | IMAGE_NAME: ${{ github.repository }} 19 | 20 | jobs: 21 | buildx: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | concurrency: 27 | # cancel jobs on PRs only 28 | group: ${{ github.workflow }}-${{ github.ref }} 29 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Set up QEMU 34 | uses: docker/setup-qemu-action@v2 35 | with: 36 | platforms: "linux/amd64,linux/arm64" 37 | 38 | - name: Set up Docker Buildx 39 | uses: docker/setup-buildx-action@v2 40 | id: buildx 41 | with: 42 | install: true 43 | 44 | - name: Docker meta 45 | id: meta 46 | uses: docker/metadata-action@v4 47 | with: 48 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 49 | # add "type=ref,event=pr" if needed to test a PR 50 | tags: | 51 | type=raw,value=latest,enable={{is_default_branch}} 52 | type=semver,pattern={{version}} 53 | 54 | - name: Login to Github Container Registry 55 | if: github.event_name != 'pull_request' 56 | uses: docker/login-action@v2 57 | with: 58 | registry: ${{ env.REGISTRY }} 59 | username: ${{ github.actor }} 60 | password: ${{ secrets.GITHUB_TOKEN }} 61 | 62 | - name: Build and push 63 | uses: docker/build-push-action@v4 64 | with: 65 | file: docker/Dockerfile 66 | context: . 67 | platforms: "linux/amd64,linux/arm64" 68 | provenance: false 69 | push: ${{ github.event_name != 'pull_request' }} 70 | tags: ${{ steps.meta.outputs.tags }} 71 | labels: ${{ steps.meta.outputs.labels }} 72 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: 22.15 18 | registry-url: "https://registry.npmjs.org" 19 | - run: npm ci 20 | - run: | 21 | npx babel ./src -d ./dist 22 | npm publish 23 | env: 24 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | env: 11 | DEBIAN_FRONTEND: noninteractive 12 | 13 | concurrency: 14 | # cancel jobs on PRs only 15 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 16 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 17 | 18 | jobs: 19 | test: 20 | name: Test on ${{ matrix.os }} (NodeJS ${{ matrix.node-version }}) 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: 26 | - "ubuntu-22.04" 27 | - "macos-14" 28 | node-version: ["18", "20", "22"] 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-node@v4 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | 36 | - name: setup Linux dependencies 37 | if: runner.os == 'Linux' 38 | run: | 39 | sudo apt-get update 40 | sudo apt-get install -y \ 41 | curl \ 42 | wget \ 43 | libcairo2-dev \ 44 | libgles2-mesa-dev \ 45 | libgbm-dev \ 46 | libllvm11 \ 47 | libuv1-dev \ 48 | libprotobuf-dev \ 49 | libxxf86vm-dev \ 50 | libwebp-dev \ 51 | xvfb \ 52 | x11-utils 53 | wget --no-verbose http://archive.ubuntu.com/ubuntu/pool/main/i/icu/libicu66_66.1-2ubuntu2.1_amd64.deb 54 | sudo apt-get install -y ./libicu66_66.1-2ubuntu2.1_amd64.deb 55 | 56 | - run: | 57 | npm ci 58 | npm install coveralls 59 | 60 | - name: test on Linux 61 | if: runner.os == 'Linux' 62 | run: | 63 | xvfb-run -a --server-args="-screen 0 1024x768x24 -ac +render -noreset" \ 64 | npx jest --silent --coverage tests 65 | 66 | - name: test on MacOS 67 | if: runner.os == 'macos' 68 | run: npx jest --silent --coverage tests 69 | 70 | - name: Coveralls 71 | if: runner.os == 'Linux' 72 | run: curl -sL https://coveralls.io/coveralls-linux.tar.gz | tar -xz && ./coveralls 73 | env: 74 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.* 3 | .nvmrc 4 | 5 | coverage/* 6 | 7 | node_modules/* 8 | 9 | tiles/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 4, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: node_js 3 | 4 | env: 5 | global: 6 | - CXX=g++-6 7 | 8 | node_js: 9 | - '8' 10 | - '10' 11 | 12 | cache: 13 | directories: 14 | - 'node_modules' 15 | services: 16 | - xvfb 17 | 18 | addons: 19 | apt: 20 | sources: 21 | - ubuntu-toolchain-r-test 22 | packages: 23 | - g++-6 24 | - libgles2-mesa-dev 25 | - libgbm-dev 26 | - libllvm3.9 27 | - libprotobuf-dev 28 | - libxxf86vm-dev 29 | 30 | install: 31 | - npm install coveralls --skip-lock 32 | 33 | script: 34 | - npm run test 35 | 36 | after_script: 37 | - cat ./tests/coverage/lcov.info | coveralls 38 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | ## Changes 2 | 3 | ### 0.9.0 4 | 5 | #### Breaking changes: 6 | 7 | - upgrade minimum supported NodeJS version to 18 (#114) 8 | - changed logging framework from `morgan` to `pino`; logs are now JSON (#106) 9 | - changed Docker base image from Debian to Ubuntu (22.04) to align with builds of `maplibre-gl-native` (#114) 10 | 11 | #### Enhancements 12 | 13 | - switch from `@mapbox/mapbox-gl-native` (no longer maintained) to `@maplibre/maplibre-gl-native` (actively maintained) (#101) 14 | - upgraded dependencies to latest (#102) 15 | - better use builtin support in `commander` for validating arguments 16 | - Publish Docker images to [Github Container Registry](https://github.com/consbio/mbgl-renderer/pkgs/container/mbgl-renderer) instead of Docker Hub (#103) 17 | - fix error logging in server (#108) and log more errors to the logger when rendering 18 | - switched to `@mapbox/geo-viewport` to actively maintained `@placemarkio/geo-viewport` 19 | 20 | ### 0.8.0 21 | 22 | #### Enhancements 23 | 24 | - upgrade NodeJS version in Docker and use newer base OS 25 | - added ability to provide images that can be used for `icon-image`, `line-pattern`, `fill-pattern` properties in style 26 | 27 | ### 0.7.3 28 | 29 | #### Enhancements 30 | 31 | - upgraded JS dependencies 32 | 33 | #### Bug fixes 34 | 35 | - actually skip request logging for docker health check 36 | - avoid nesting error messages 37 | 38 | #### Potentially breaking changes: 39 | 40 | - @mapbox/geo-viewport 0.4.1 included a fix for calculating center points, which 41 | causes a small change in the center and zoom level automatically calculated 42 | here when `bounds` are provided for rendering. If you depend on precise 43 | control over how `bounds` are used for rendering, please check the outputs 44 | after upgrading. 45 | 46 | ### 0.7.2 47 | 48 | #### Enhancements 49 | 50 | - skip request logging for routes that do not exist (e.g., docker health check) 51 | 52 | ### 0.7.1 53 | 54 | #### Bug fixes 55 | 56 | - Fixed handling of `NaN` and `Infinity` in inputs for `bounds` and `center` (#58) 57 | 58 | ### 0.7.0 59 | 60 | #### Enhancements 61 | 62 | - Added support for padding image bounds 63 | - Added support for image sources (#52) 64 | - Added request logging (#54) 65 | 66 | #### Bug fixes 67 | 68 | - Handle missing remote assets correctly (#49) 69 | 70 | ### 0.6.2 71 | 72 | #### Bug fixes 73 | 74 | - Fix bad handling of root path (#43) 75 | 76 | ### 0.6.1 77 | 78 | #### Bug fixes 79 | 80 | - Docker: fix missing `/app/tiles` directory if user does not bind in a tiles directory (resolves #40) 81 | 82 | ### 0.6.0 83 | 84 | #### Enhancements 85 | 86 | - upgraded `mapbox-gl-native` to 5.0.0 (#35). NOTE: [fallback to source builds of `mapbox-gl-native` are no longer supported](https://github.com/mapbox/mapbox-gl-native/blob/master/platform/node/CHANGELOG.md#500). 87 | - warn rather than fail on missing tiles 88 | 89 | ### O.5.0 90 | 91 | #### Enhancements 92 | 93 | - upgraded Docker to NodeJS 10 94 | - reduced size of Docker image and simplified Xvfb management 95 | - added support for `pitch` and `bearing` options during rendering (#31) 96 | 97 | ### 0.4.0 98 | 99 | - rendering now uses floating point zoom levels when `bounds` are provided as inputs 100 | 101 | #### Bug fixes 102 | 103 | - downgraded supported version of Node to 8, due to occasional segfaults: https://github.com/mapbox/mapbox-gl-native/issues/12252 104 | 105 | Prior to `0.3.1`, there was a significant bug in rendering layers with transparency (#25). 106 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Conservation Biology Institute 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Static Map Renderer using MapLibre GL 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/consbio/mbgl-renderer/badge.svg?branch=main)](https://coveralls.io/github/consbio/mbgl-renderer?branch=main) 4 | 5 | Create static map images using MapLibre GL with a command-line interface, an HTTP interface, and a NodeJS API. 6 | 7 | ## Features: 8 | 9 | - Render static maps using NodeJS with [maplibre-gl-native](https://github.com/maplibre/maplibre-gl-native) 10 | - Supports raster and vector tiles 11 | - Compatible with Mapbox tiles (don't forget attribution) and other hosted tile providers 12 | - Use locally hosted mbtiles 13 | - Add GeoJSON overlays to your maps 14 | - Supports high DPI rendering 15 | - Also available for use in Docker 16 | 17 | [Blog post](https://medium.com/@brendan_ward/creating-a-static-map-renderer-using-the-mapbox-gl-native-nodejs-api-23db560b219e) describing the background and goals in a bit more detail. 18 | 19 | One of the nifty features of this package is that you can use locally hosted mbtiles files 20 | with your raster or vector tiles. This saves considerable time during rendering compared 21 | to using map services over the web. 22 | 23 | This package is intended to help with generating static maps for download or use in reports, 24 | especially when combined with your own styles or overlays. 25 | 26 | If you are only using hosted Mapbox styles and vector tiles, please use the [Mapbox Static API](https://www.mapbox.com/api-documentation/#static) instead; it is more full featured and more 27 | appropriate for static Mapbox maps. 28 | 29 | ### Attribution 30 | 31 | Please make sure to give appropriate attribution to the data sources and styles used in your maps, 32 | in the manner that those providers specify. 33 | 34 | If you use Mapbox styles or hosted tiles, make sure to include appropriate [attribution](https://www.mapbox.com/help/how-attribution-works/) in your output maps. 35 | 36 | ## Installation 37 | 38 | `npm add mbgl-renderer` 39 | or 40 | `npm install mbgl-renderer` 41 | 42 | ### Supported versions of NodeJS: 43 | 44 | - 18 45 | - 20 46 | - 22 47 | 48 | Only NodeJS versions with `@maplibre/maplibre-gl-native` binaries built by MapLibre are supported via `npm install`, otherwise you need to build `@maplibre/maplibre-gl-native` from source yourself. See [build instructions](https://github.com/maplibre/maplibre-gl-native/tree/main/platform/node) for more information. 49 | 50 | On a server, in addition to build tools, you need to install a GL environment. See the `Dockerfile` and `entrypoint.sh` for an example setup. 51 | 52 | ## Usage 53 | 54 | ### Style JSON: 55 | 56 | The primary input to every map rendering method below is a Mapbox GL Style JSON file. 57 | For full reference on this format, please see the [Mapbox documentation](https://docs.mapbox.com/mapbox-gl-js/style-spec/). 58 | 59 | ### Labels 60 | 61 | In order to use text labels, you need to include `glyphs` in your Style JSON (sibling of `sources`) 62 | 63 | To use Mapbox hosted glyphs (a Mapbox token is required): 64 | 65 | ```json 66 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf" 67 | ``` 68 | 69 | You can also use [OpenMapTiles hosted glyphs](https://github.com/openmaptiles/fonts): 70 | 71 | ```json 72 | "glyphs": "http://fonts.openmaptiles.org/{fontstack}/{range}.pbf", 73 | ``` 74 | 75 | In either case, you must make sure that the font you specify is available from that provider. 76 | See `tests/fixtures/example-style-geojson-label.json` for an example of adding labels based on coordinates specified in 77 | GeoJSON. 78 | 79 | ### NodeJS API: 80 | 81 | The following examples assume that you are using `babel` to provide ES6 features. 82 | 83 | ``` 84 | import render from 'mbgl-renderer' 85 | 86 | import style from `tests/fixtures/example-style.json` 87 | // style JSON file with MapBox style. Can also be opened and read instead of imported. 88 | 89 | const width = 512 90 | const height = 256 91 | const center = [-79.86, 32.68] 92 | const zoom = 10 93 | 94 | render(style, width, height, { zoom, center }) 95 | .then((data) => { 96 | fs.writeFileSync('test.png', data) 97 | })) 98 | ``` 99 | 100 | You can also provide `bounds` instead of `center` and `zoom`: 101 | 102 | ``` 103 | const width = 512 104 | const height = 256 105 | const bounds = [-80.23, 32.678, -79.73, 32.891] 106 | 107 | render(style, width, height, { bounds }) 108 | .then((data) => { 109 | fs.writeFileSync('test.png', data) 110 | })) 111 | ``` 112 | 113 | If you provide `bounds` you can also provide `padding` to add 114 | that many pixels to each side of the rendered image. These pixels 115 | are padded to the inside of the image, meaning that the resulting 116 | image matches the `width` and `height` you provide, but is zoomed 117 | out to allow padding around the edges. 118 | 119 | ``` 120 | const width = 512 121 | const height = 256 122 | const bounds = [-80.23, 32.678, -79.73, 32.891] 123 | const padding = 25 124 | 125 | render(style, width, height, { bounds, padding }) 126 | .then((data) => { 127 | fs.writeFileSync('test.png', data) 128 | })) 129 | ``` 130 | 131 | `padding` must be integers and not be greater than 1/2 of `width` 132 | or `height`, whichever is smaller. You can provide a negative 133 | padding to over-zoom the image. 134 | 135 | You can also supply a pixel ratio for High DPI screens, typically > 1 (max of 31 has been tested): 136 | 137 | ``` 138 | const width = 512 139 | const height = 256 140 | const center = [-79.86, 32.68] 141 | const zoom = 10 142 | const ratio = 2 143 | 144 | render(style, width, height, { zoom, center, ratio }) 145 | .then((data) => { 146 | fs.writeFileSync('test.png', data) 147 | })) 148 | ``` 149 | 150 | You can also provide an alternative bearing (0-360) or pitch (0-60): 151 | 152 | ``` 153 | const width = 512 154 | const height = 256 155 | const center = [-79.86, 32.68] 156 | const zoom = 10 157 | const bearing = 90 158 | const pitch = 30 159 | 160 | render(style, width, height, { zoom, center, bearing, pitch }) 161 | .then((data) => { 162 | fs.writeFileSync('test.png', data) 163 | })) 164 | 165 | ``` 166 | 167 | If your style includes a Mapbox hosted source (e.g., `"url": "mapbox://mapbox.mapbox-streets-v7"`), 168 | you need to pass in your Mapbox access token as well: 169 | 170 | ``` 171 | render(style, width, height, { bounds, token: '' }) 172 | .then((data) => { 173 | fs.writeFileSync('test.png', data) 174 | })) 175 | ``` 176 | 177 | You can also provide icon images that will be available for `icon-image`, `line-pattern`, and `fill-pattern` style properties. 178 | The `images` property is an object with keys for at least `url` and optional keys `pixelRatio` and `sdf`. 179 | 180 | `url` must point to a valid remote URL that is accessible from `mbgl-renderer`. It may include base64 encoded image data (example below). 181 | 182 | Set `sdf` to `true` to enable the image to be converted into an SDF icon. [Read more](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/). 183 | 184 | `pixelRatio` is only used for `symbols`; due to a bug in Mapbox GL Native, it does not work properly for `line-pattern` and `fill-pattern` icon images. 185 | 186 | ``` 187 | const images = { 188 | "exampleImageID": { 189 | "url": "data:image/png;base64,iVBORw0KGg....truncated...", 190 | "sdf": true / false (default), 191 | "pixelRatio": 1 (default) 192 | } 193 | } 194 | ``` 195 | 196 | Then refer to `exampleImageID` in your style: 197 | 198 | ``` 199 | ... 200 | layers:[ 201 | { 202 | "id": "...", 203 | "type": "symbol", 204 | "source": "...", 205 | "layout": { 206 | "icon-image": "exampleImageID", 207 | ... 208 | }, 209 | "paint": { 210 | ... 211 | } 212 | } 213 | ] 214 | ``` 215 | 216 | ### Command line interface: 217 | 218 | ``` 219 | Usage: mbgl-render [options] 220 | 221 | Export a Mapbox GL map to image. You must provide either center and zoom, or bounds. 222 | 223 | Options: 224 | 225 | -V, --version output the version number 226 | -c, --center center of map (NO SPACES) 227 | -z, --zoom Zoom level 228 | -r, --ratio Pixel ratio 229 | -b, --bounds Bounds (NO SPACES) 230 | --padding Number of pixels to add to the inside of each edge of the image. Can only be used with bounds option. 231 | --bearing Bearing in degrees (0-360) 232 | --pitch Pitch in degrees (0-60) 233 | -t, --tiles Directory containing local mbtiles files to render 234 | --token Mapbox access token (required for using Mapbox styles and sources) 235 | --images 267 | ``` 268 | 269 | Note: support for Mapbox hosted styles is still considered experimental. 270 | 271 | ### Static image server 272 | 273 | You start this from the command line: 274 | 275 | ``` 276 | Usage: mbgl-server [options] 277 | 278 | Start a server to render Mapbox GL map requests to images. 279 | 280 | Options: 281 | 282 | -V, --version output the version number 283 | -p, --port Server port 284 | -t, --tiles Directory containing local mbtiles files to render 285 | -v, --verbose Enable request logging 286 | -h, --help output usage information 287 | ``` 288 | 289 | To start this on port 8080 with local tiles in `tests/fixtures`: 290 | 291 | ``` 292 | mbgl-static-server -p 8080 -t tests/fixtures 293 | ``` 294 | 295 | You can also start this via `npm start` but you must use the `--` spacer before passing argmuents: 296 | 297 | ``` 298 | npm start -- --port 8080 --tiles tests/fixtures 299 | ``` 300 | 301 | #### Making requests 302 | 303 | In your client of choice, you can make either HTTP GET or POST requests to 304 | 305 | ``` 306 | http://localhost:8080/render 307 | ``` 308 | 309 | ##### GET requests: 310 | 311 | Include the following query parameters: 312 | 313 | - `height` and `width` are integer values (required). 314 | - `zoom` is a floating point value. 315 | - `ratio` is an integer value. 316 | - `center` if provided must be a `longitude,latitude` with floating point values (NO spaces or brackets). 317 | - `bounds` if provided must be `west,south,east,north` with floating point values (NO spaces or brackets). 318 | - `padding` if provided must be an integer value that is less than 1/2 of width or height, whichever is smaller. Can only be used with bounds. 319 | - `bearing is a floating point value (0-360)`pitch`is a floating point value (0-60). 320 | - `token` if provided must a string. 321 | - `images` if provided must be URL encoded JSON object (see `images` property above). 322 | 323 | Your style JSON needs to be URL encoded: 324 | 325 | ``` 326 | http://localhost:8080/render?height=1024&width=1024¢er=-80,-20&zoom=3&style=%7B%22version%22%3A8%2C%22sources%22%3A%7B%22land%22%3A%7B%22type%22%3A%22vector%22%2C%22url%22%3A%22mbtiles%3A%2F%2Fland%22%2C%22tileSize%22%3A256%7D%7D%2C%22layers%22%3A%5B%7B%22id%22%3A%22land%22%2C%22type%22%3A%22fill%22%2C%22source%22%3A%22land%22%2C%22source-layer%22%3A%22land%22%2C%22paint%22%3A%7B%22fill-color%22%3A%22%23AAAAAA%22%2C%22fill-opacity%22%3A1%7D%7D%5D%7D 327 | ``` 328 | 329 | If your style JSON points to local tilesets, you must have started the server up using those local tilesets. 330 | 331 | To test the server from the command line, for example with the excellent tool [`httpie`](https://httpie.org/) with the style file `tests/fixtures/example-style-mbtiles-source.json`: 332 | 333 | ``` 334 | http :8080/render width=400 height=400 zoom=0 center=0,0 style:=@tests/fixtures/example-style.json > /tmp/test.png 335 | ``` 336 | 337 | ##### POST requests: 338 | 339 | You can do a POST request with all of the above parameters in the body of the request, and the style can be passed directly as JSON instead of URL encoded. 340 | 341 | POST may be necessary where your style JSON file exceeds the maximum number of characters allowed in a GET request URL. 342 | 343 | ## Development 344 | 345 | Use `npm run watch` to start up a file watcher to recompile ES6 files in `src/` to ES5 files that are executable in Node in `dist/`. These are compiled using `babel`. 346 | 347 | ## Testing 348 | 349 | Tests are run using `jest`. Right now, our coverage is not great and tests only exercise the core functionality of the render function. 350 | 351 | Tests require a valid Mapbox API token. Set this in `.env.test` file in the root of the repository: 352 | 353 | ``` 354 | MAPBOX_API_TOKEN= 355 | ``` 356 | 357 | To run tests: 358 | 359 | ``` 360 | npm run test 361 | ``` 362 | 363 | This uses the `pixelmatch` package to determine if output images match those that are expected. This may fail when rendered on different machines for reasons we have not completely sorted out, so don't necessarily be alarmed that tests are failing for you - check the outputs. 364 | 365 | ## Docker 366 | 367 | Pull the latest image from [Github Container Registry](https://github.com/consbio/mbgl-renderer/pkgs/container/mbgl-renderer): 368 | 369 | ``` 370 | docker pull ghcr.io/consbio/mbgl-renderer:latest 371 | ``` 372 | 373 | To run `mbgl-server` in the docker container on port 8080: 374 | 375 | ``` 376 | docker run --rm -p 8080:80 consbio/mbgl-renderer 377 | ``` 378 | 379 | Mount your local tiles directory to `/app/tiles` in the container to use these in the server or CLI: 380 | 381 | ``` 382 | docker run --rm -p 8080:80 -v $(pwd)/tests/fixtures:/app/tiles consbio/mbgl-renderer 383 | ``` 384 | 385 | ### Build your own image: 386 | 387 | Build your own docker container with name ``: 388 | 389 | ``` 390 | docker build -t -f docker/Dockerfile . 391 | ``` 392 | 393 | ## Headless servers 394 | 395 | In order to use this package on a headless server, you need to use `xvfb`. See `docker/Dockerfile` and `docker/entrypoint.sh` for the basic configuration. 396 | 397 | ## Credits 398 | 399 | This project was made possible based on support from the 400 | [South Atlantic Landscape Conservation Cooperative](http://www.southatlanticlcc.org/), 401 | the [Paulson Institute](http://www.paulsoninstitute.org/), and the 402 | [California Department of Food and Agriculture - Office of Environmental Farming & Innovation](https://www.cdfa.ca.gov/oefi/). 403 | 404 | ## Contributors ✨ 405 | 406 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 |

Brendan Ward

💻 📖 🐛 📝 👀 🤔

Nik Molnar

💻 🐛 🤔

Kaveh

🐛 🤔

bertrandmd

💻

Daniel Korp

💻

Kyle Kurtz

💻

Norman Rzepka

💻

J-E Castagnede

🐛

Jez Nicholson

🐛 🤔

Bob Gray

🐛

Nikita Abraztsov

🐛

Timothée Monty

🐛

felix

📖

Rodrigo Santa Cruz Ortega

💻
431 | 432 | 433 | 434 | 435 | 436 | 437 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 438 | -------------------------------------------------------------------------------- /dist/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | "use strict"; 3 | 4 | var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); 5 | var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); 6 | var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); 7 | var _fs = _interopRequireDefault(require("fs")); 8 | var _commander = require("commander"); 9 | var _request = _interopRequireDefault(require("request")); 10 | var _package = _interopRequireDefault(require("../package.json")); 11 | var _render = require("./render.js"); 12 | var version = _package["default"].version; 13 | var raiseError = function raiseError(msg) { 14 | console.error('error', msg); 15 | process.exit(1); 16 | }; 17 | var parseListToFloat = function parseListToFloat(text) { 18 | return text.split(',').map(Number); 19 | }; 20 | var validateDimension = function validateDimension(value) { 21 | if (value <= 0) { 22 | throw new _commander.InvalidArgumentError('Must be greater than 0'); 23 | } 24 | }; 25 | _commander.program.version(version).name('mbgl-render').usage('