# 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](https://github.com/Altius-Academy-SNC/yoro) or use it inline. Here's a minimal version:

```html
<script src="/static/js/yoro_codec.js"></script>
```

!!! note "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](https://github.com/Altius-Academy-SNC/yoro) for the full source.

### Step 2: Grid Overlay Layer

```javascript
// 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

```javascript
const map = L.map('map').setView([6.8, -5.3], 12);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; 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

```css
.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

```python
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)

```javascript
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 |

!!! warning "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.
