|
1 | 1 | # mlnative |
2 | 2 |
|
3 | 3 | [](https://pypi.org/project/mlnative/) |
4 | | -[](https://github.com/adonm/mlnative/blob/main/LICENSE) |
| 4 | +[](LICENSE) |
5 | 5 |
|
6 | | -> **⚠️ Warning: This is an alpha release. The API may change significantly. Not recommended for production use.** |
| 6 | +Render static map images from Python using MapLibre Native. |
7 | 7 |
|
8 | | -Simple Python wrapper for MapLibre GL Native using a native Rust renderer. |
| 8 | +**Platform:** Linux x64, ARM64 |
| 9 | +**Python:** 3.12+ |
9 | 10 |
|
10 | | -A grug-brained library for rendering static map images with minimal complexity. |
11 | | - |
12 | | -## Features |
13 | | - |
14 | | -- **Simple API**: One class, minimal methods, zero confusion |
15 | | -- **Native Performance**: Rust backend with MapLibre Native C++ core |
16 | | -- **Built-in Defaults**: Uses OpenFreeMap Liberty style by default - no configuration needed |
17 | | -- **Address Support**: Built-in geocoding with geopy for rendering addresses directly |
18 | | -- **Geometry Support**: Shapely integration for bounds fitting and GeoJSON |
19 | | - |
20 | | -## Installation |
| 11 | +## Quick Start |
21 | 12 |
|
22 | 13 | ```bash |
23 | 14 | pip install mlnative |
24 | 15 | ``` |
25 | 16 |
|
26 | | -Platform-specific wheels include the native renderer binary: |
27 | | -- Linux x86_64, aarch64 |
28 | | - |
29 | | -## Quick Start |
30 | | - |
31 | 17 | ```python |
32 | 18 | from mlnative import Map |
33 | 19 |
|
34 | | -# Render a map - uses OpenFreeMap Liberty style by default |
35 | 20 | with Map(512, 512) as m: |
36 | | - png_bytes = m.render(center=[-122.4, 37.8], zoom=12) |
37 | | - |
38 | | - with open("map.png", "wb") as f: |
39 | | - f.write(png_bytes) |
| 21 | + png = m.render(center=[-122.4, 37.8], zoom=12) |
| 22 | + open("map.png", "wb").write(png) |
40 | 23 | ``` |
41 | 24 |
|
42 | | -## Rendering from Addresses |
| 25 | +## Features |
| 26 | + |
| 27 | +- **Zero config** - Works out of the box with OpenFreeMap tiles |
| 28 | +- **HiDPI support** - `pixel_ratio=2` for sharp retina displays |
| 29 | +- **Batch rendering** - Efficiently render hundreds of maps |
| 30 | +- **Address geocoding** - Built-in support via geopy |
| 31 | +- **Custom markers** - Add GeoJSON points, lines, polygons |
| 32 | + |
| 33 | +## Examples |
| 34 | + |
| 35 | +### Render from address |
43 | 36 |
|
44 | 37 | ```python |
45 | 38 | from mlnative import Map |
46 | 39 | from geopy.geocoders import ArcGIS |
47 | 40 |
|
48 | | -# Geocode an address and render |
49 | 41 | geolocator = ArcGIS() |
50 | 42 | location = geolocator.geocode("Sydney Opera House") |
51 | 43 |
|
52 | 44 | with Map(512, 512) as m: |
53 | 45 | png = m.render( |
54 | 46 | center=[location.longitude, location.latitude], |
55 | | - zoom=15 # Good for landmark/building view |
| 47 | + zoom=15 |
56 | 48 | ) |
57 | | - open("sydney.png", "wb").write(png) |
58 | 49 | ``` |
59 | 50 |
|
60 | | -## Custom Styles |
61 | | - |
62 | | -While OpenFreeMap Liberty is the default, you can override it: |
| 51 | +### Fit bounds to show area |
63 | 52 |
|
64 | 53 | ```python |
65 | | -# OpenFreeMap styles |
66 | | -m.load_style("https://tiles.openfreemap.org/styles/liberty") |
67 | | -m.load_style("https://tiles.openfreemap.org/styles/positron") |
68 | | -m.load_style("https://tiles.openfreemap.org/styles/dark") |
| 54 | +from mlnative import Map, feature_collection, point |
| 55 | + |
| 56 | +# Show multiple locations |
| 57 | +markers = feature_collection([ |
| 58 | + point(-122.4194, 37.7749), # SF |
| 59 | + point(-122.2712, 37.8044), # Oakland |
| 60 | +]) |
| 61 | + |
| 62 | +with Map(800, 600) as m: |
| 63 | + # Load style as dict to modify it |
| 64 | + style = {"version": 8, ...} # your style |
| 65 | + m.load_style(style) |
| 66 | + m.set_geojson("markers", markers) |
| 67 | + |
| 68 | + # Fit map to show all markers |
| 69 | + center, zoom = m.fit_bounds( |
| 70 | + (-122.5, 37.7, -122.2, 37.9), # xmin, ymin, xmax, ymax |
| 71 | + padding=50 |
| 72 | + ) |
| 73 | + png = m.render(center=center, zoom=zoom) |
| 74 | +``` |
69 | 75 |
|
70 | | -# MapLibre demo tiles (good for testing) |
71 | | -m.load_style("https://demotiles.maplibre.org/style.json") |
| 76 | +### Batch render multiple views |
72 | 77 |
|
73 | | -# Or load from dict |
74 | | -m.load_style({"version": 8, "sources": {...}, "layers": [...]}) |
| 78 | +```python |
| 79 | +views = [ |
| 80 | + {"center": [0, 0], "zoom": 1}, |
| 81 | + {"center": [-122.4, 37.8], "zoom": 12}, |
| 82 | + {"center": [151.2, -33.9], "zoom": 10, "bearing": 45}, |
| 83 | +] |
| 84 | + |
| 85 | +with Map(512, 512) as m: |
| 86 | + pngs = m.render_batch(views) # Returns list of PNG bytes |
75 | 87 | ``` |
76 | 88 |
|
77 | | -## API Reference |
| 89 | +### HiDPI rendering |
78 | 90 |
|
79 | | -### Map Class |
| 91 | +```python |
| 92 | +# Retina/HiDPI display (2x resolution) |
| 93 | +with Map(512, 512, pixel_ratio=2) as m: |
| 94 | + png = m.render(center=[0, 0], zoom=5) |
| 95 | + # Image is 1024x1024, text appears sharp |
| 96 | +``` |
80 | 97 |
|
81 | | -**`Map(width, height, pixel_ratio=1.0)`** |
| 98 | +## API Reference |
82 | 99 |
|
83 | | -Create a new map renderer. |
| 100 | +### Map(width, height, pixel_ratio=1.0) |
84 | 101 |
|
85 | | -- `width`: Image width in pixels (1-4096) |
86 | | -- `height`: Image height in pixels (1-4096) |
87 | | -- `pixel_ratio`: Pixel ratio for high-DPI rendering |
| 102 | +Create map renderer. Context manager ensures cleanup. |
88 | 103 |
|
89 | | -**`render(center, zoom, bearing=0, pitch=0)`** |
| 104 | +### render(center, zoom, bearing=0, pitch=0) |
90 | 105 |
|
91 | | -Render the map to PNG bytes. Uses OpenFreeMap Liberty style by default. |
| 106 | +Render single view. Returns PNG bytes. |
92 | 107 |
|
93 | | -- `center`: `[longitude, latitude]` list |
94 | | -- `zoom`: Zoom level (0-24) |
| 108 | +- `center`: `[longitude, latitude]` |
| 109 | +- `zoom`: 0-24 |
95 | 110 | - `bearing`: Rotation in degrees (0-360) |
96 | 111 | - `pitch`: Tilt in degrees (0-85) |
97 | 112 |
|
98 | | -**`render_batch(views)`** |
| 113 | +### render_batch(views) |
99 | 114 |
|
100 | 115 | Render multiple views efficiently. |
101 | 116 |
|
102 | | -**`fit_bounds(bounds, padding=0, max_zoom=24)`** |
| 117 | +```python |
| 118 | +views = [ |
| 119 | + {"center": [lon, lat], "zoom": z}, |
| 120 | + {"center": [lon, lat], "zoom": z, "geojson": {"markers": {...}}}, |
| 121 | +] |
| 122 | +``` |
| 123 | + |
| 124 | +### fit_bounds(bounds, padding=0, max_zoom=24) |
103 | 125 |
|
104 | | -Calculate center/zoom to fit bounds. Returns `(center, zoom)`. |
| 126 | +Calculate center/zoom to fit bounding box. |
105 | 127 |
|
106 | 128 | ```python |
107 | | -bounds = (-122.6, 37.7, -122.3, 37.9) # (xmin, ymin, xmax, ymax) |
108 | | -center, zoom = m.fit_bounds(bounds) |
| 129 | +center, zoom = m.fit_bounds((xmin, ymin, xmax, ymax)) |
109 | 130 | png = m.render(center=center, zoom=zoom) |
110 | 131 | ``` |
111 | 132 |
|
112 | | -**`set_geojson(source_id, geojson)`** |
| 133 | +### set_geojson(source_id, geojson) |
113 | 134 |
|
114 | | -Update GeoJSON source data (requires style loaded as dict). |
| 135 | +Update GeoJSON source in style (requires dict style, not URL). |
115 | 136 |
|
116 | 137 | ```python |
117 | | -from shapely import Point |
118 | | -m.set_geojson("markers", Point(-122.4, 37.8)) |
| 138 | +m.set_geojson("markers", {"type": "FeatureCollection", "features": [...]}) |
119 | 139 | ``` |
120 | 140 |
|
121 | | -**`load_style(style)`** |
122 | | - |
123 | | -Load custom style (URL, file path, or dict). Call before render if not using default. |
| 141 | +### load_style(style) |
124 | 142 |
|
125 | | -### GeoJSON Helpers |
| 143 | +Load custom style (URL, file path, or dict). |
126 | 144 |
|
127 | 145 | ```python |
128 | | -from mlnative import point, feature_collection, from_coordinates, from_latlng |
129 | | - |
130 | | -# From coordinates (lng, lat) |
131 | | -fc = from_coordinates([(-122.4, 37.8), (-74.0, 40.7)]) |
| 146 | +# OpenFreeMap styles |
| 147 | +m.load_style("https://tiles.openfreemap.org/styles/liberty") |
| 148 | +m.load_style("https://tiles.openfreemap.org/styles/positron") |
132 | 149 |
|
133 | | -# From GPS coordinates (lat, lng) |
134 | | -fc = from_latlng([(37.8, -122.4), (40.7, -74.0)]) |
| 150 | +# MapLibre demo |
| 151 | +m.load_style("https://demotiles.maplibre.org/style.json") |
135 | 152 |
|
136 | | -# From shapely |
137 | | -from shapely import MultiPoint, Point |
138 | | -fc = feature_collection(MultiPoint([Point(-122.4, 37.8), Point(-74.0, 40.7)])) |
| 153 | +# Custom style dict |
| 154 | +m.load_style({"version": 8, "sources": {...}, "layers": [...]}) |
139 | 155 | ``` |
140 | 156 |
|
141 | | -## Examples |
| 157 | +## GeoJSON Helpers |
142 | 158 |
|
143 | | -See `examples/` directory: |
144 | | -- `basic.py` - Simple usage |
145 | | -- `address_rendering.py` - Render from addresses |
146 | | -- `fastapi_server.py` - Static maps API |
147 | | - |
148 | | -## Supported Platforms |
149 | | - |
150 | | -- Linux x86_64, aarch64 |
| 159 | +```python |
| 160 | +from mlnative import point, feature_collection, from_coordinates, from_latlng |
151 | 161 |
|
152 | | -> **Note:** macOS and Windows support limited due to upstream MapLibre Native build constraints. |
| 162 | +# Create point |
| 163 | +sf = point(-122.4194, 37.7749, {"name": "San Francisco"}) |
153 | 164 |
|
154 | | -## Development |
| 165 | +# From coordinate tuples |
| 166 | +fc = from_coordinates([(-122.4, 37.8), (-74.0, 40.7)]) |
155 | 167 |
|
156 | | -Requires Python 3.12+, Rust 1.70+, and uv. |
| 168 | +# From GPS (lat, lng) order |
| 169 | +fc = from_latlng([(37.8, -122.4), (40.7, -74.0)]) |
| 170 | +``` |
157 | 171 |
|
158 | | -```bash |
159 | | -# Setup |
160 | | -uv pip install -e ".[dev,web]" |
161 | | -cd rust && cargo build --release |
| 172 | +## Notes |
162 | 173 |
|
163 | | -# Run tests |
164 | | -just test |
165 | | -``` |
| 174 | +- **Default style**: OpenFreeMap Liberty (no configuration needed) |
| 175 | +- **GeoJSON updates**: Requires style loaded as dict, not URL |
| 176 | +- **pixel_ratio**: Higher values = larger image, same geographic area |
| 177 | +- **Platform**: Linux only (macOS/Windows builds disabled due to upstream issues) |
166 | 178 |
|
167 | 179 | ## License |
168 | 180 |
|
|
0 commit comments