{"id":16896039,"url":"https://github.com/brendan-ward/pymgl","last_synced_at":"2025-04-02T12:12:01.687Z","repository":{"id":39619512,"uuid":"438869650","full_name":"brendan-ward/pymgl","owner":"brendan-ward","description":"Python MapLibre GL Native Static Renderer","archived":false,"fork":false,"pushed_at":"2024-09-30T19:47:36.000Z","size":1006,"stargazers_count":41,"open_issues_count":7,"forks_count":9,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-26T07:07:08.576Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brendan-ward.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-16T05:23:08.000Z","updated_at":"2025-02-05T09:06:37.000Z","dependencies_parsed_at":"2024-09-17T22:43:02.115Z","dependency_job_id":"53a4a100-ba70-4f75-9c3b-0ebee66d76ea","html_url":"https://github.com/brendan-ward/pymgl","commit_stats":{"total_commits":57,"total_committers":1,"mean_commits":57.0,"dds":0.0,"last_synced_commit":"45b59fd7869ade37fcd6dc0e83e68170c9d33e9b"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendan-ward%2Fpymgl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendan-ward%2Fpymgl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendan-ward%2Fpymgl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brendan-ward%2Fpymgl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brendan-ward","download_url":"https://codeload.github.com/brendan-ward/pymgl/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246811310,"owners_count":20837752,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-13T17:27:46.751Z","updated_at":"2025-04-02T12:12:01.636Z","avatar_url":"https://github.com/brendan-ward.png","language":"C","funding_links":[],"categories":["Low-level utilities"],"sub_categories":[],"readme":"# PyMGL: Maplibre GL Native Static Renderer for Python\n\nThis package provides an interface to [Mapblibre Native](https://github.com/maplibre/maplibre-native)\nto render Mapbox GL / Maplibre GL styles to PNG images.\n\nWARNING: this package is under active development and the API may change without notice.\n\n## Goals\n\nThis package is intended to provide a lightweight interface to `maplibre-native`\nfor rendering Mapbox GL / Maplibre GL styles to PNG image data using Python.\nThis is particularly useful for server-side rendering of maps for use in reports.\n\nThis package provides only the Python API for interacting with `maplibre-native`;\nit does not provide higher-level functionality such as a web server or a CLI.\n\n## Install\n\n### Supported operating systems\n\n#### MacOS 12+, Ubuntu 18+, Debian 10+, Fedora 29+, RHEL 8+, Alma Linux 8+\n\nx86_64 and arm64 wheels are available on PyPI:\n\n```bash\npip install pymgl\n```\n\nNOTE: x86_64 wheels are not currently available for MacOS.\n\nTo verify that pymgl installed correctly, install with the test dependencies and\nrun the included test suite:\n\n```bash\npip install pymgl[test]\npytest --pyargs pymgl -v\n```\n\n#### Windows\n\nWindows is not and will not be supported.\n\n## Usage\n\nTo create a map object, you must always provide a Mapbox GL / Maplibre GL style\nJSON string or URL to a well-known style hosted by Mapbox or Maptiler:\n\n```Python\nfrom pymgl import Map\n\nstyle = \"\"\"{\n    \"version\": 8,\n    \"sources\": {\n        \"basemap\": {\n            \"type\": \"raster\",\n            \"tiles\": [\"https://services.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}\"],\n            \"tileSize\": 256\n        }\n    },\n    \"layers\": [\n        { \"id\": \"basemap\", \"source\": \"basemap\", \"type\": \"raster\" }\n    ]\n}\"\"\"\n\nmap = Map(style, \u003cheight=256\u003e, \u003cwidth=256\u003e, \u003cratio=1\u003e, \u003clongitude=0\u003e, \u003clatitude=0\u003e, \u003czoom=0\u003e, \u003ctoken=None\u003e, \u003cprovider=None\u003e)\n```\n\nSee the [styles](#styles) section for more information about map styles.\n\nOther than style, all other parameters are optional with default values.\n\nNOTE: `style` and `ratio` cannot be changed once the instance is constructed.\n\nYou can use a well-known style instead of providing a style JSON string, but\nyou must also provide a token and identify the correct provider:\n\n```Python\nmap = Map(\"mapbox://styles/mapbox/streets-v11\", token=\u003cmapbox token\u003e, provider=\"mapbox\")\n```\n\nValid providers are `mapbox`, `maptiler`, and `maplibre`.\n\n### Map properties\n\nYou can set additional properties on the map instance after it is created:\n\n```Python\nmap.setCenter(longitude, latitude)\n\nmap.setZoom(zoom)\n\nmap.setSize(width, height)\n\nmap.setBearing(bearing)  # map bearing in degrees\n\nmap.setPitch(pitch)  # map pitch in degrees\n\nmap.setFilter(layerId, filterJSON or None)\n\nmap.setPaintProperty(layerId, property, value)\n\nmap.setVisibility(layerId, True / False)\n```\n\nYou can retrieve these values using attributes, if needed:\n\n```Python\nmap.size  # (width, height)\n\nmap.center  # (longitude, latitude)\n\nmap.zoom\n\nmap.bearing\n\nmap.pitch\n```\n\nYou can also retrive information about the map's style or a specific layer:\n\n```Python\nmap.listLayers()  # [\u003clayerId1\u003e, ...]\n\nmap.listSources()  # [\u003csourceId1\u003e, ...]\n\nmap.getFilter(\u003clayerId\u003e)  # returns JSON value or None\n\nmap.getPaintProperty(\u003clayerId\u003e, \u003cproperty\u003e)  # returns JSON value or None\n\nmap.getLayerJSON(\u003clayerId\u003e)  # returns JSON describing layer\n```\n\nNOTE: paint properties may be decoded to their internal representation. For\nexample, a CSS color string `#FF0000` will be returned as `[\"rgba\", 255, 0, 0, 1]`.\n\nIMPORTANT: if you are using a remotely-hosted style, you need to force the map\nto load - which loads all underying assets - before listing the style's layers,\nsources, or other properties.\n\n```Python\nmap = Map(\"mapbox://styles/mapbox/streets-v11\", token=\u003cmapbox token\u003e, provider=\"mapbox\")\n\nmap.listLayers()  # []\nmap.load()\nmap.listLayers()  # [\u003clayerId1\u003e, ...]\n```\n\nAlternatively, you can download the style yourself and provide that as input to\nthe Map, and it will show all layers without requiring a render first. However, not all assets will be loaded until the first render.\n\n```Python\nfrom urllib.request import urlopen\n\nurl = f\"https://api.mapbox.com/styles/v1/mapbox/streets-v11?access_token={MAPBOX_TOKEN}\"\n\nwith urlopen(url) as r:\n    style = r.read()\n\nmap = Map(style.decode(\"UTF-8\") token=\u003cmapbox token\u003e, provider=\"mapbox\")\nmap.listLayers()  # [\u003clayerId1\u003e, ...]\n```\n\nYou can auto-fit the map to bounds instead of using center longitude / lantitude\nand zoom:\n\n```Python\nmap.setBounds(xmin, ymin, xmax, ymax, \u003cpadding=0\u003e)\n```\n\nYou can register an image for use with your style by providing an ID,\nraw image bytes, width, height, pixel ratio, and indicate if it should be\ninterpreted as SDF:\n\n```\nmap.addImage(\"id\", img_bytes, width, height, \u003cratio=1\u003e, \u003cmake_SDF=False\u003e)\n```\n\nSee the [SDF image docs](https://docs.mapbox.com/help/troubleshooting/using-recolorable-images-in-mapbox-maps/) for more information about using SDF\nimages.\n\n### Rendering\n\nYou can render the map to PNG bytes:\n\n```Python\nimg_bytes = map.renderPNG()\n```\n\nThis returns `bytes` containing the RGBA PNG data.\n\nYou can render the map to a raw buffer as a numpy array (`uint8` dtype):\n\n```Python\narray = map.renderBuffer()\n```\n\nThe array is a sequence of RGBA values for each pixel in the image.\n\nThis may be useful if you are going to immediately read the image data into\nanother package such as `Pillow` or `pyvips` to combine with other image\noperations.\n\n### Map instances\n\nWARNING: you must manually delete the map instance if you assign a new map\ninstance to that variable, or this package will segfault (not yet sure why).\nThis problem does not occur if separate instances are assigned to separate\nvariables.\n\n```Python\n\nmap = Map(\u003cstyle\u003e, \u003cwidth\u003e, \u003cheight\u003e)\n\ndel map  # must manually delete BEFORE creating a new instance assigned to this\n\nmap = Map(\u003cstyle\u003e, \u003cwidth\u003e, \u003cheight\u003e)\n```\n\nFor this reason, you should consider using a context manager:\n\n```Python\nwith Map(\u003cstyle\u003e, \u003cwidth\u003e, \u003cheight\u003e) as map:\n    map.renderPNG()\n```\n\nYou can also use the map instance to directly render to PNG, if you don't need\nto set other properties on the map instance:\n\n```\nMap(\u003cstyle\u003e, \u003cwidth\u003e, \u003cheight\u003e).renderPNG()\n```\n\n## Styles\n\nPyMGL should support basic styles as of Mapbox GL JS 1.13.\n\n### Remote tilesets, sources, and assets\n\nRemote tilesets, tile sources, and assets (glyphs, sprites) should be well-supported.\nThese are loaded by the underlying C++ library outside our control. Invalid\nURLs will generally raise errors. However, network timeouts or incorrect formats\nmay cause the process to crash.\n\n### Local mbtiles\n\nLocal MBTiles are supported, but must be provided using an absolute path to the\nmbtiles file as the source `url` of a tileset; it must resolve to an actual file.\n\nLocal MBTiles are denoted with a `mbtiles://` URI prefix.\n\nExample:\n\n```json\n{\n    \"sources\": {\n        \"source_id\": {\n            \"url\": \"mbtiles:///\u003cpymgl_root_dir\u003e/tests/fixtures/geography-class-png.mbtiles\",\n            ...\n        }\n    },\n    \"layers\": [...],\n    ...\n}\n```\n\n### Local files\n\nGeoJSON files and other local file assets are supported, but must be provided\nusing an absolute path to the file source.\n\nExample:\n\n```json\n{\n    \"sources\": {\n        \"geojson\": {\n            \"type\": \"geojson\",\n            \"data\": \"file:///\u003cpymgl_root_dir\u003e/tests/fixtures/test.geojson\"\n        }\n    },\n    \"layers\": [...],\n    ...\n}\n\n```\n\nWARNING: providing a URI to tiles under the `tiles` key of a source is NOT currently supported by Maplibre Native;\nattempting to do so will fail.\n\n### Images\n\nYou must register the image with the map instance before rendering the map. See\n`map.addImage()` above.\n\n```json\n{\n    \"sources\": {...},\n    \"layers\": [\n        {\n            ...,\n            \"paint\": {\n                \"fill-pattern\": \"pattern\"\n            }\n        },\n    ]\n}\n```\n\nYou can use map images as fill patterns or icon images.\n\n### Adding sources and layers after construction\n\nYou can add sources and layers dynamically after constructing the map instance:\n\n```Python\nimport json\n\nmap = Map(\"\")  # construct with empty style\n\nmap.addSource(\"my_id\", json.dumps({\n    \"type\": \"geojson\",\n    \"data\": {\"type\": \"Point\", \"coordinates\": [0, 0]}\n}))\n\nmap.addLayer(json.dumps({\n    \"id\": \"geojson-point\",\n    \"source\": \"geojson\",\n    \"type\": \"circle\",\n    \"paint\": { ... }\n}))\n```\n\n### Feature state\n\nYou can get, set, and remove feature state after the map has been loaded.\n\n```Python\n\nmap = Map(\u003cstyle with source \"exampleSource\" and layer \"exampleLayer\"\u003e, ...)\n\nmap.load()\nmap.getFeatureState(\"exampleSource\", \"exampleLayer\", \"0\")  # returns None\nmap.setFeatureState(\"exampleSource\", \"exampleLayer\", \"0\", \"{\\\"a\\\": true}\")\nmap.getFeatureState(\"exampleSource\", \"exampleLayer\", \"0\")  # returns \"{\\\"a\\\": true}\"\n\n# remove the state value for key \"a\"\nmap.removeFeatureState(\"exampleSource\", \"exampleLayer\", \"0\", \"a\")\nmap.render()\nmap.getFeatureState(\"exampleSource\", \"exampleLayer\", \"0\")  # returns None\n```\n\nNOTE: features must already have a unique, numeric ID set on each feature. There\nis currently no support for `promoteId` like in MapLibre GL JS.\n\nIMPORTANT: the map must be loaded before getting or setting feature state. You\nmust manually force a render in order for the map to update feature state after\nremoving a state key\n\n### Unsupported features\n\nPyMGL does not support alternative projections or 3D terrain.\n\n## Developing\n\n### Dependencies:\n\n#### MacOS:\n\nDeveloping on MacOS requires the following binary libraries to be installed\nvia `homebrew`:\n\n-   cmake\n-   ninja\n\n#### Developing on Ubuntu requires the following binary libraries:\n\n-   cmake\n-   ninja-build\n-   build-essential\n-   libcurl4-openssl-dev\n-   libicu-dev\n-   libpng-dev\n-   libwebp-dev\n-   libprotobuf-dev\n-   libjpeg-turbo8-dev\n-   libx11-dev\n-   libegl-dev\n-   libopengl-dev\n-   xvfb\n\nTo run on Linux, XVFB must also be running; otherwise the process will segfault.\n\nSee [`docker/README.md`](./docker/README.md) for more information.\n\n### nanobind\n\n`nanonbind` is used to provide bindings for Python against a C++ class that wraps\n`maplibre-native` for easier rendering operations.\n\nIt is included here as a git submodule, per the\n[installation instructions]().\n\n```bash\ngit submodule add https://github.com/wjakob/nanobind vendor/nanobind\ncd vendor/nanobind\ngit submodule update --init --recursive\n```\n\nThen to upgrade to a specific version of nanobind for development, if needed:\n\n```bash\ncd vendor/nanobind\ngit checkout \u003cversion tag\u003e\n```\n\n### Maplibre Native\n\nMaplibre Native is included as a git submodule, and it includes many submodules\nof its own.\n\n```bash\ngit submodule add -b main https://github.com/maplibre/maplibre-native vendor/maplibre-native\n```\n\n### Git submodules\n\nRun\n\n```bash\ngit submodule update --init\n```\n\nWe only need some of the submodules under maplibre-native. In particular,\nwe do not need `maplibre-gl-js` or Android / IOS dependencies.\n\nRun the following:\n\n```bash\ncd vendor/maplibre-native\n\ngit submodule update --init --recursive \\\n    vendor/boost \\\n    vendor/cpp-httplib \\\n    vendor/earcut.hpp \\\n    vendor/eternal \\\n    vendor/googletest \\\n    vendor/metal-cpp \\\n    vendor/polylabel \\\n    vendor/protozero \\\n    vendor/mapbox-base \\\n    vendor/unique_resource \\\n    vendor/unordered_dense \\\n    vendor/vector-tile \\\n    vendor/wagyu \\\n    vendor/zip-archive\n```\n\nTo later update `maplibre-native`:\n\n```bash\ncd vendor/maplibre-native\ngit checkout main\ngit pull origin\n\ncd ../..\ngit commit -am \"update maplibre-native\" to latest\n```\n\n### Architecture\n\nThis package is composed of 2 main parts:\n\n-   wrapper around Maplibre Native classes to make constructing and managing\n    properties of the map easier\n-   Python bindings created using nanobind against that wrapper\n\nThe wrapper is located in `src/map.cpp`.\n\n### Build\n\n#### C++ tests\n\nSee [tests/README](tests/README.md) for more information.\n\n#### Build Python extension\n\nThe Python `setup.py` script manages building the library and extension using\nCMake.\n\nFrom project root directory:\n\n```bash\npython setup.py build_ext --inplace\n```\n\n#### Docstrings / type information\n\nDocstrings are maintained in both `src/_pymgl.cpp` and `pymgl/__init__.pyi`.\n\nPython-friendly type annotations are maintained in `pymgl/__init__.pyi`.\n\nNote: `pymgl/__init__.pyi` is necessary to support autocompletion and tooltips\nin VSCode.\n\n##### Building wheels\n\nMost wheels are automatically built by Github when pushing a new version tag.\nLinux Arm64 wheels must be built locally on an Arm64 machine (e.g., MacOS host).\n\nThese are created using the manylinux_2_28 Docker container.\n\n```bash\ndocker build -f ci/Dockerfile.manylinux_2_28_aarch64 -t pymgl-manylinux_2_28_aarch64 .\ndocker run -v \"$PWD/:/app\" pymgl-manylinux_2_28_aarch64 ci/build_linux_wheels.sh\n```\n\nThis will create aarch64 wheels in `dist` that can be uploaded directly to PyPI.\n\n## See also\n\n[mbgl-renderer](https://github.com/consbio/mbgl-renderer)\nprovides a NodeJS API, CLI, and server based on the NodeJS bindings to Mapbox GL Native.\n\n## Credits\n\nThis project was developed with the support of the\n[U.S. Fish and Wildlife Service](https://www.fws.gov/)\n[Southeast Conservation Adaptation Strategy](https://secassoutheast.org/) for\nuse in the\n[Southeast Conservation Blueprint Viewer](https://blueprint.geoplatform.gov/southeast/).\n\nThis project is made possible because of the\n[mapbox-gl-native](https://github.com/mapbox/mapbox-gl-native/) project by\n[Mapbox](https://www.mapbox.com/)\nby the efforts of the Maplibre community maintaining the open-source fork of that\nproject at [maplibre-native](https://github.com/mapbox/mapbox-native/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrendan-ward%2Fpymgl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrendan-ward%2Fpymgl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrendan-ward%2Fpymgl/lists"}