https://github.com/brendan-ward/pymgl
Python MapLibre GL Native Static Renderer
https://github.com/brendan-ward/pymgl
Last synced: 11 months ago
JSON representation
Python MapLibre GL Native Static Renderer
- Host: GitHub
- URL: https://github.com/brendan-ward/pymgl
- Owner: brendan-ward
- License: mit
- Created: 2021-12-16T05:23:08.000Z (about 4 years ago)
- Default Branch: main
- Last Pushed: 2024-09-30T19:47:36.000Z (over 1 year ago)
- Last Synced: 2025-03-26T07:07:08.576Z (12 months ago)
- Language: C
- Size: 982 KB
- Stars: 41
- Watchers: 3
- Forks: 9
- Open Issues: 7
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGES.md
- License: LICENSE
Awesome Lists containing this project
- awesome-vector-tiles - pymgl - Maplibre GL Native Static Renderer for Python (Low-level utilities)
README
# PyMGL: Maplibre GL Native Static Renderer for Python
This package provides an interface to [Mapblibre Native](https://github.com/maplibre/maplibre-native)
to render Mapbox GL / Maplibre GL styles to PNG images.
WARNING: this package is under active development and the API may change without notice.
## Goals
This package is intended to provide a lightweight interface to `maplibre-native`
for rendering Mapbox GL / Maplibre GL styles to PNG image data using Python.
This is particularly useful for server-side rendering of maps for use in reports.
This package provides only the Python API for interacting with `maplibre-native`;
it does not provide higher-level functionality such as a web server or a CLI.
## Install
### Supported operating systems
#### MacOS 12+, Ubuntu 18+, Debian 10+, Fedora 29+, RHEL 8+, Alma Linux 8+
x86_64 and arm64 wheels are available on PyPI:
```bash
pip install pymgl
```
NOTE: x86_64 wheels are not currently available for MacOS.
To verify that pymgl installed correctly, install with the test dependencies and
run the included test suite:
```bash
pip install pymgl[test]
pytest --pyargs pymgl -v
```
#### Windows
Windows is not and will not be supported.
## Usage
To create a map object, you must always provide a Mapbox GL / Maplibre GL style
JSON string or URL to a well-known style hosted by Mapbox or Maptiler:
```Python
from pymgl import Map
style = """{
"version": 8,
"sources": {
"basemap": {
"type": "raster",
"tiles": ["https://services.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}"],
"tileSize": 256
}
},
"layers": [
{ "id": "basemap", "source": "basemap", "type": "raster" }
]
}"""
map = Map(style, , , , , , , , )
```
See the [styles](#styles) section for more information about map styles.
Other than style, all other parameters are optional with default values.
NOTE: `style` and `ratio` cannot be changed once the instance is constructed.
You can use a well-known style instead of providing a style JSON string, but
you must also provide a token and identify the correct provider:
```Python
map = Map("mapbox://styles/mapbox/streets-v11", token=, provider="mapbox")
```
Valid providers are `mapbox`, `maptiler`, and `maplibre`.
### Map properties
You can set additional properties on the map instance after it is created:
```Python
map.setCenter(longitude, latitude)
map.setZoom(zoom)
map.setSize(width, height)
map.setBearing(bearing) # map bearing in degrees
map.setPitch(pitch) # map pitch in degrees
map.setFilter(layerId, filterJSON or None)
map.setPaintProperty(layerId, property, value)
map.setVisibility(layerId, True / False)
```
You can retrieve these values using attributes, if needed:
```Python
map.size # (width, height)
map.center # (longitude, latitude)
map.zoom
map.bearing
map.pitch
```
You can also retrive information about the map's style or a specific layer:
```Python
map.listLayers() # [, ...]
map.listSources() # [, ...]
map.getFilter() # returns JSON value or None
map.getPaintProperty(, ) # returns JSON value or None
map.getLayerJSON() # returns JSON describing layer
```
NOTE: paint properties may be decoded to their internal representation. For
example, a CSS color string `#FF0000` will be returned as `["rgba", 255, 0, 0, 1]`.
IMPORTANT: if you are using a remotely-hosted style, you need to force the map
to load - which loads all underying assets - before listing the style's layers,
sources, or other properties.
```Python
map = Map("mapbox://styles/mapbox/streets-v11", token=, provider="mapbox")
map.listLayers() # []
map.load()
map.listLayers() # [, ...]
```
Alternatively, you can download the style yourself and provide that as input to
the Map, and it will show all layers without requiring a render first. However, not all assets will be loaded until the first render.
```Python
from urllib.request import urlopen
url = f"https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token={MAPBOX_TOKEN}"
with urlopen(url) as r:
style = r.read()
map = Map(style.decode("UTF-8") token=, provider="mapbox")
map.listLayers() # [, ...]
```
You can auto-fit the map to bounds instead of using center longitude / lantitude
and zoom:
```Python
map.setBounds(xmin, ymin, xmax, ymax, )
```
You can register an image for use with your style by providing an ID,
raw image bytes, width, height, pixel ratio, and indicate if it should be
interpreted as SDF:
```
map.addImage("id", img_bytes, width, height, , )
```
See the [SDF image docs](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/) for more information about using SDF
images.
### Rendering
You can render the map to PNG bytes:
```Python
img_bytes = map.renderPNG()
```
This returns `bytes` containing the RGBA PNG data.
You can render the map to a raw buffer as a numpy array (`uint8` dtype):
```Python
array = map.renderBuffer()
```
The array is a sequence of RGBA values for each pixel in the image.
This may be useful if you are going to immediately read the image data into
another package such as `Pillow` or `pyvips` to combine with other image
operations.
### Map instances
WARNING: you must manually delete the map instance if you assign a new map
instance to that variable, or this package will segfault (not yet sure why).
This problem does not occur if separate instances are assigned to separate
variables.
```Python
map = Map(, <width>, <height>)
del map # must manually delete BEFORE creating a new instance assigned to this
map = Map(<style>, <width>, <height>)
```
For this reason, you should consider using a context manager:
```Python
with Map(<style>, <width>, <height>) as map:
map.renderPNG()
```
You can also use the map instance to directly render to PNG, if you don't need
to set other properties on the map instance:
```
Map(<style>, <width>, <height>).renderPNG()
```
## Styles
PyMGL should support basic styles as of Mapbox GL JS 1.13.
### Remote tilesets, sources, and assets
Remote tilesets, tile sources, and assets (glyphs, sprites) should be well-supported.
These are loaded by the underlying C++ library outside our control. Invalid
URLs will generally raise errors. However, network timeouts or incorrect formats
may cause the process to crash.
### Local mbtiles
Local MBTiles are supported, but must be provided using an absolute path to the
mbtiles file as the source `url` of a tileset; it must resolve to an actual file.
Local MBTiles are denoted with a `mbtiles://` URI prefix.
Example:
```json
{
"sources": {
"source_id": {
"url": "mbtiles:///<pymgl_root_dir>/tests/fixtures/geography-class-png.mbtiles",
...
}
},
"layers": [...],
...
}
```
### Local files
GeoJSON files and other local file assets are supported, but must be provided
using an absolute path to the file source.
Example:
```json
{
"sources": {
"geojson": {
"type": "geojson",
"data": "file:///<pymgl_root_dir>/tests/fixtures/test.geojson"
}
},
"layers": [...],
...
}
```
WARNING: providing a URI to tiles under the `tiles` key of a source is NOT currently supported by Maplibre Native;
attempting to do so will fail.
### Images
You must register the image with the map instance before rendering the map. See
`map.addImage()` above.
```json
{
"sources": {...},
"layers": [
{
...,
"paint": {
"fill-pattern": "pattern"
}
},
]
}
```
You can use map images as fill patterns or icon images.
### Adding sources and layers after construction
You can add sources and layers dynamically after constructing the map instance:
```Python
import json
map = Map("") # construct with empty style
map.addSource("my_id", json.dumps({
"type": "geojson",
"data": {"type": "Point", "coordinates": [0, 0]}
}))
map.addLayer(json.dumps({
"id": "geojson-point",
"source": "geojson",
"type": "circle",
"paint": { ... }
}))
```
### Feature state
You can get, set, and remove feature state after the map has been loaded.
```Python
map = Map(<style with source "exampleSource" and layer "exampleLayer">, ...)
map.load()
map.getFeatureState("exampleSource", "exampleLayer", "0") # returns None
map.setFeatureState("exampleSource", "exampleLayer", "0", "{\"a\": true}")
map.getFeatureState("exampleSource", "exampleLayer", "0") # returns "{\"a\": true}"
# remove the state value for key "a"
map.removeFeatureState("exampleSource", "exampleLayer", "0", "a")
map.render()
map.getFeatureState("exampleSource", "exampleLayer", "0") # returns None
```
NOTE: features must already have a unique, numeric ID set on each feature. There
is currently no support for `promoteId` like in MapLibre GL JS.
IMPORTANT: the map must be loaded before getting or setting feature state. You
must manually force a render in order for the map to update feature state after
removing a state key
### Unsupported features
PyMGL does not support alternative projections or 3D terrain.
## Developing
### Dependencies:
#### MacOS:
Developing on MacOS requires the following binary libraries to be installed
via `homebrew`:
- cmake
- ninja
#### Developing on Ubuntu requires the following binary libraries:
- cmake
- ninja-build
- build-essential
- libcurl4-openssl-dev
- libicu-dev
- libpng-dev
- libwebp-dev
- libprotobuf-dev
- libjpeg-turbo8-dev
- libx11-dev
- libegl-dev
- libopengl-dev
- xvfb
To run on Linux, XVFB must also be running; otherwise the process will segfault.
See [`docker/README.md`](./docker/README.md) for more information.
### nanobind
`nanonbind` is used to provide bindings for Python against a C++ class that wraps
`maplibre-native` for easier rendering operations.
It is included here as a git submodule, per the
[installation instructions]().
```bash
git submodule add https://github.com/wjakob/nanobind vendor/nanobind
cd vendor/nanobind
git submodule update --init --recursive
```
Then to upgrade to a specific version of nanobind for development, if needed:
```bash
cd vendor/nanobind
git checkout <version tag>
```
### Maplibre Native
Maplibre Native is included as a git submodule, and it includes many submodules
of its own.
```bash
git submodule add -b main https://github.com/maplibre/maplibre-native vendor/maplibre-native
```
### Git submodules
Run
```bash
git submodule update --init
```
We only need some of the submodules under maplibre-native. In particular,
we do not need `maplibre-gl-js` or Android / IOS dependencies.
Run the following:
```bash
cd vendor/maplibre-native
git submodule update --init --recursive \
vendor/boost \
vendor/cpp-httplib \
vendor/earcut.hpp \
vendor/eternal \
vendor/googletest \
vendor/metal-cpp \
vendor/polylabel \
vendor/protozero \
vendor/mapbox-base \
vendor/unique_resource \
vendor/unordered_dense \
vendor/vector-tile \
vendor/wagyu \
vendor/zip-archive
```
To later update `maplibre-native`:
```bash
cd vendor/maplibre-native
git checkout main
git pull origin
cd ../..
git commit -am "update maplibre-native" to latest
```
### Architecture
This package is composed of 2 main parts:
- wrapper around Maplibre Native classes to make constructing and managing
properties of the map easier
- Python bindings created using nanobind against that wrapper
The wrapper is located in `src/map.cpp`.
### Build
#### C++ tests
See [tests/README](tests/README.md) for more information.
#### Build Python extension
The Python `setup.py` script manages building the library and extension using
CMake.
From project root directory:
```bash
python setup.py build_ext --inplace
```
#### Docstrings / type information
Docstrings are maintained in both `src/_pymgl.cpp` and `pymgl/__init__.pyi`.
Python-friendly type annotations are maintained in `pymgl/__init__.pyi`.
Note: `pymgl/__init__.pyi` is necessary to support autocompletion and tooltips
in VSCode.
##### Building wheels
Most wheels are automatically built by Github when pushing a new version tag.
Linux Arm64 wheels must be built locally on an Arm64 machine (e.g., MacOS host).
These are created using the manylinux_2_28 Docker container.
```bash
docker build -f ci/Dockerfile.manylinux_2_28_aarch64 -t pymgl-manylinux_2_28_aarch64 .
docker run -v "$PWD/:/app" pymgl-manylinux_2_28_aarch64 ci/build_linux_wheels.sh
```
This will create aarch64 wheels in `dist` that can be uploaded directly to PyPI.
## See also
[mbgl-renderer](https://github.com/consbio/mbgl-renderer)
provides a NodeJS API, CLI, and server based on the NodeJS bindings to Mapbox GL Native.
## Credits
This project was developed with the support of the
[U.S. Fish and Wildlife Service](https://www.fws.gov/)
[Southeast Conservation Adaptation Strategy](https://secassoutheast.org/) for
use in the
[Southeast Conservation Blueprint Viewer](https://blueprint.geoplatform.gov/southeast/).
This project is made possible because of the
[mapbox-gl-native](https://github.com/mapbox/mapbox-gl-native/) project by
[Mapbox](https://www.mapbox.com/)
by the efforts of the Maplibre community maintaining the open-source fork of that
project at [maplibre-native](https://github.com/mapbox/mapbox-native/).