Hilbert Grid Overlay¶
This guide shows how to render the Yoro Hilbert grid on a Leaflet map. As users pan and zoom, the grid updates to show all cells at the appropriate precision level.
The Concept¶
At each zoom level, we compute which Yoro cells are visible in the map viewport and draw them as rectangles with their code labels. This gives users a visual understanding of the addressing system.
Client-Side Approach (Recommended)¶
The Yoro codec is pure math — no database needed. For the best UX, compute cells directly in JavaScript using the port of the algorithm.
Step 1: Include the Yoro JavaScript Codec¶
You can either copy the codec from the source or use it inline. Here's a minimal version:
JavaScript codec
The JavaScript port (yoro_codec.js) implements the same Hilbert curve algorithm as the Python library. The encode/decode results are identical. See the repository for the full source.
Step 2: Grid Overlay Layer¶
// Grid overlay for Leaflet
class YoroGridOverlay {
constructor(map, options = {}) {
this.map = map;
this.domain = options.domain || 'CI';
this.maxCells = options.maxCells || 1500;
this.gridLayer = L.layerGroup().addTo(map);
this.showLabels = options.showLabels !== false;
// Colors for different precision levels
this.colors = {
2: '#e65100', 4: '#f57c00', 7: '#ff9800',
9: '#ffa726', 12: '#ffb74d', 14: '#ffcc02',
17: '#4caf50', 19: '#2196f3', 21: '#9c27b0',
};
// Update grid when map moves
map.on('moveend zoomend', () => this.update());
this.update();
}
update() {
this.gridLayer.clearLayers();
const zoom = this.map.getZoom();
const precision = this._zoomToPrecision(zoom);
const bounds = this.map.getBounds();
// Use the JS codec to get visible cells
const cells = AltiusCodec.cellsInBounds(
bounds.getSouth(), bounds.getNorth(),
bounds.getWest(), bounds.getEast(),
precision, this.domain, this.maxCells
);
if (cells.length === 0) return; // Too many cells or out of domain
const color = this.colors[precision] || '#ff9800';
cells.forEach(cell => {
const rect = L.rectangle(
[[cell.bounds.lat_min, cell.bounds.lon_min],
[cell.bounds.lat_max, cell.bounds.lon_max]],
{
color: color,
weight: 1,
fillOpacity: 0.05,
interactive: true,
}
);
// Show code on hover
rect.bindTooltip(cell.code, {
direction: 'center',
className: 'yoro-tooltip',
permanent: this.showLabels && cells.length < 200,
});
// Show code on click
rect.on('click', () => {
navigator.clipboard.writeText(cell.code);
});
this.gridLayer.addLayer(rect);
});
}
_zoomToPrecision(zoom) {
if (zoom <= 8) return 4;
if (zoom <= 10) return 7;
if (zoom <= 12) return 9;
if (zoom <= 14) return 12;
if (zoom <= 16) return 14;
if (zoom <= 18) return 17;
return 19;
}
setDomain(domain) {
this.domain = domain;
this.update();
}
remove() {
this.gridLayer.clearLayers();
this.map.removeLayer(this.gridLayer);
}
}
Step 3: Usage¶
const map = L.map('map').setView([6.8, -5.3], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
// Add the Yoro grid
const grid = new YoroGridOverlay(map, {
domain: 'CI',
showLabels: true,
maxCells: 1500,
});
// Toggle with a layer control
const overlays = { 'Yoro Grid': grid.gridLayer };
L.control.layers(null, overlays).addTo(map);
Step 4: Styling¶
.yoro-tooltip {
background: rgba(230, 81, 0, 0.85);
color: white;
border: none;
font-family: monospace;
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
}
Server-Side Approach (for Django)¶
If you prefer computing cells on the server (useful when you need to join with database records):
View¶
import json
from django.http import JsonResponse
from yoro.codec import cells_in_bounds, domain_for_country
def grid_cells_view(request):
"""Return Yoro cells for a map viewport."""
lat_min = float(request.GET['lat_min'])
lat_max = float(request.GET['lat_max'])
lon_min = float(request.GET['lon_min'])
lon_max = float(request.GET['lon_max'])
precision = int(request.GET.get('precision', 12))
domain = domain_for_country(request.GET.get('domain', 'CI'))
cells = cells_in_bounds(
lat_min, lat_max, lon_min, lon_max,
precision=precision, domain=domain, max_cells=2000,
)
# Convert to GeoJSON for Leaflet
features = []
for cell in cells:
b = cell['bounds']
features.append({
'type': 'Feature',
'properties': {'code': cell['code']},
'geometry': {
'type': 'Polygon',
'coordinates': [[
[b['lon_min'], b['lat_min']],
[b['lon_max'], b['lat_min']],
[b['lon_max'], b['lat_max']],
[b['lon_min'], b['lat_max']],
[b['lon_min'], b['lat_min']],
]],
},
})
return JsonResponse({
'type': 'FeatureCollection',
'features': features,
})
JavaScript (fetch from server)¶
async function loadGrid() {
const bounds = map.getBounds();
const precision = zoomToPrecision(map.getZoom());
const resp = await fetch(
`/api/grid/?lat_min=${bounds.getSouth()}&lat_max=${bounds.getNorth()}` +
`&lon_min=${bounds.getWest()}&lon_max=${bounds.getEast()}` +
`&precision=${precision}&domain=CI`
);
const geojson = await resp.json();
if (gridLayer) map.removeLayer(gridLayer);
gridLayer = L.geoJSON(geojson, {
style: { color: '#e65100', weight: 1, fillOpacity: 0.05 },
onEachFeature: (feature, layer) => {
layer.bindTooltip(feature.properties.code, {
direction: 'center', permanent: geojson.features.length < 200,
});
},
}).addTo(map);
}
map.on('moveend', loadGrid);
Performance Tips¶
| Zoom Level | Precision | Visible Cells | Label? |
|---|---|---|---|
| 6-8 | 4 | ~20 | Yes |
| 9-10 | 7 | ~100 | Yes |
| 11-12 | 9 | ~200 | Yes |
| 13-14 | 12 | ~800 | No (tooltip on hover) |
| 15-16 | 14 | ~2000 | No |
| 17+ | 17 | Use client-side | No |
Cell count limit
cells_in_bounds() returns an empty list if the count exceeds max_cells (default 2000). This prevents the browser from freezing at low zoom levels with high precision.