PHPackages                             eduardoribeirodev/filament-leaflet - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. eduardoribeirodev/filament-leaflet

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

eduardoribeirodev/filament-leaflet
==================================

Um widget de mapa para FilamentPHP.

v4.7.2(1mo ago)134.6k↓14.7%6MITPHPPHP ^8.1

Since Dec 10Pushed 1mo ago2 watchersCompare

[ Source](https://github.com/eduardoribeirodev/filament-leaflet)[ Packagist](https://packagist.org/packages/eduardoribeirodev/filament-leaflet)[ RSS](/packages/eduardoribeirodev-filament-leaflet/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)Dependencies (3)Versions (22)Used By (0)

Filament Leaflet
================

[](#filament-leaflet)

A powerful and elegant Leaflet integration for Filament PHP that makes creating interactive maps a breeze. Build beautiful, feature-rich maps with markers, clusters, shapes, and more using a fluent, expressive API.

Features
--------

[](#features)

- 🗺️ **Interactive Maps** - Full Leaflet integration with customizable tile layers
- 📍 **Markers &amp; Clusters** - Beautiful markers with popup/tooltip support and intelligent clustering
- 🎨 **Shapes** - Circles, polygons, polylines, rectangles, and circle markers
- 🎯 **Click Events** - Handle clicks on markers, shapes, and the map itself
- 📊 **GeoJSON Support** - Display density maps with custom color schemes
- 🔄 **Model Binding** - Automatically create markers from Eloquent models
- 🎨 **Multiple Tile Layers** - Switch between OpenStreetMap, Satellite, and custom layers
- 💾 **CRUD Operations** - Create markers directly from map clicks
- 🎭 **Customizable** - Extensive configuration options for every element

### Latest Features

[](#latest-features)

- **Form Field (MapPicker)** - Pick coordinates directly in forms with automatic latitude/longitude sync or JSON storage
- **Table Column (MapColumn)** - Display maps in Filament table columns for at-a-glance location visualization
- **Infolist Entry (MapEntry)** - Display read-only maps in Filament infolists
- **Model GeoJSON Files** - Automatic GeoJSON loading from models with `HasGeoJsonFile` trait
- **Layer Groups** - Organize markers and shapes with automatic coverage area calculation
- **Editable Layers &amp; Draw Control** - Edit markers and shapes directly on the map
- **Dynamic Icons** - Marker icons with automatic sizing and anchor point calculation
- **Heroicon Support** - Use Filament Heroicons directly in markers with automatic SVG rendering
- **Enhanced Shapes** - Factory methods (`fromRecord()`) for all shape classes with support for JSON columns
- **JSON Storage** - Store coordinates as JSON in single database column
- **Map Interaction Control** - Toggle dragging, zooming, and auto-recenter behavior
- **Static Maps** - Display read-only maps with automatic interaction disabling
- **Auto-Recenter** - Automatically recenter maps after users pan around
- **Mapbox Tile Layer Support** - New tile layer provider with configurable Access Token and tile size
- **Marker CRUD Actions** - Built-in view, edit, and delete actions for markers on the map
- **Coordinate DTO** - Improved handling of latitude/longitude pairs with dedicated DTO class

Installation
------------

[](#installation)

```
composer require eduardoribeirodev/filament-leaflet
```

Publish the assets:

```
php artisan vendor:publish --tag=filament-leaflet
```

This will publish the Leaflet assets used by the package.

Table of Contents
-----------------

[](#table-of-contents)

- [Installation](#installation)
- [Core Components](#core-components)
    - [Map Widget](#map-widget)
    - [MapPicker (Form Field)](#mappicker-form-field)
    - [MapColumn (Table Column)](#mapcolumn-table-column)
    - [MapEntry (Infolist)](#mapentry-infolist)
- [Map Elements](#map-elements)
    - [Markers](#markers)
    - [Layer Groups](#layer-groups)
    - [Shapes](#shapes)
    - [Editable Layers](#editable-layers)
- [User Interactions](#user-interactions)
- [Advanced Features](#advanced-features)
- [Best Practices](#best-practices)
- [Configuration Reference](#configuration-reference)
    - [Method Reference](#method-reference)
    - [Concern Methods Reference](#concern-methods-reference)

Core Components
---------------

[](#core-components)

### Map Widget

[](#map-widget)

Create your first interactive map widget:

```
namespace App\Filament\Widgets;

use EduardoRibeiroDev\FilamentLeaflet\Widgets\MapWidget;
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;

class MyMapWidget extends MapWidget
{
    protected ?string $heading = 'My Locations';
    protected array $mapCenter = [-23.5505, -46.6333];
    protected int $defaultZoom = 12;
    protected int $mapHeight = 600;

    protected function getMarkers(): array
    {
        return [
            Marker::make(-23.5505, -46.6333)
                ->title('São Paulo')
                ->popupContent('The largest city in Brazil'),
        ];
    }
}
```

[![Widget Example](images/widget.png)](images/widget.png)

#### Basic Configuration

[](#basic-configuration)

```
class MyMapWidget extends MapWidget
{
    // Display
    protected ?string $heading = 'Store Locations';
    protected int $mapHeight = 600;

    // Map center and zoom
    protected array $mapCenter = [-14.235, -51.9253];
    protected int $defaultZoom = 4;
    protected int $maxZoom = 18;
    protected int $minZoom = 2;
}
```

#### Map Interaction Control

[](#map-interaction-control)

Control how users interact with your map:

```
class MyMapWidget extends MapWidget
{
    // Allow/prevent dragging
    protected bool $mapDraggable = true;

    // Allow/prevent zooming (scroll wheel, pinch)
    protected bool $mapZoomable = true;

    // Auto-recenter after user pans (in milliseconds)
    // Set to null to disable
    protected ?int $recenterMapTimeout = 5000;  // Recenter after 5 seconds

    // Auto-center to user's current location
    protected bool $autoCenter = false;
}
```

**Static Maps** - Disable all interactions:

```
MapPicker::make('location')
    ->static()  // Equivalent to ->mapDraggable(false)->mapZoomable(false)
```

#### Controls

[](#controls)

Enable/disable map controls:

```
class MyMapWidget extends MapWidget
{
    protected bool $hasAttributionControl = true;
    protected bool $hasScaleControl = true;
    protected bool $hasZoomControl = true;
    protected bool $hasFullscreenControl = true;
    protected bool $hasSearchControl = true;

    protected bool $hasDrawMarkerControl = true;
    protected bool $hasDrawCircleMarkerControl = true;
    protected bool $hasDrawCircleControl = true;
    protected bool $hasDrawPolylineControl = true;
    protected bool $hasDrawRectangleControl = true;
    protected bool $hasDrawPolygonControl = true;
    protected bool $hasDrawTextControl = true;
    protected bool $hasEditLayersControl = true;
    protected bool $hasDragLayersControl = true;
    protected bool $hasRemoveLayersControl = true;
    protected bool $hasRotateLayersControl = true;
    protected bool $hasCutPolygonControl = true;
}
```

[![Map Controls Example](images/map-controls.png)](images/map-controls.png)

Conditionally show controls at runtime:

```
protected function hasDrawCircleControl(): bool
{
    return auth()?->user()?->is_admin;
}
```

#### Tile Layers

[](#tile-layers)

Choose from multiple providers or add custom layers:

```
use EduardoRibeiroDev\FilamentLeaflet\Enums\TileLayer;

class MyMapWidget extends MapWidget
{
    // Single layer
    protected TileLayer|string|array $tileLayersUrl = TileLayer::OpenStreetMap;

    // Multiple layers
    protected TileLayer|string|array $tileLayersUrl = [
        'Street Map' => TileLayer::OpenStreetMap,
        'Satellite' => TileLayer::GoogleSatellite,
        'Custom' => 'https://{s}.tile.custom.com/{z}/{x}/{y}.png',
    ];
}
```

[![Widget With Custom Tile Layers Example](images/tile-layers.png)](images/tile-layers.png)

**Available providers:** `OpenStreetMap`, `GoogleStreets`, `GoogleSatellite`, `GoogleHybrid`, `GoogleTerrain`, `EsriWorldImagery`, `EsriWorldStreetMap`, `EsriNatGeo`, `CartoPositron`, `CartoDarkMatter`, `Mapbox`

**Mapbox Tile Layer:**

To use Mapbox tiles, configure your access token in `.env` or `config/services.php`:

**Environment variables:**

```
MAPBOX_ACCESS_TOKEN=your_mapbox_access_token_here
MAPBOX_TILE_SIZE=512
```

**Or in config/services.php:**

```
'mapbox' => [
    'token' => env('MAPBOX_ACCESS_TOKEN'),
    'tile_size' => env('MAPBOX_TILE_SIZE', 512),
],
```

Then use the Mapbox tile layer in your widget:

```
use EduardoRibeiroDev\FilamentLeaflet\Enums\TileLayer;

class MyMapWidget extends MapWidget
{
    // Single Mapbox layer
    protected TileLayer|string|array $tileLayersUrl = TileLayer::MapboxStreets;
}
```

**Available Mapbox providers:**

- `TileLayer::MapboxStreets` - Streets layer
- `TileLayer::MapboxOutdoors` - Outdoors layer
- `TileLayer::MapboxLight` - Light layer
- `TileLayer::MapboxDark` - Dark layer
- `TileLayer::MapboxSatellite` - Satellite layer

**Or use multiple Mapbox layers:**

```
protected TileLayer|string|array $tileLayersUrl = [
    'Street Map' => TileLayer::OpenStreetMap,
    'Mapbox Streets' => TileLayer::MapboxStreets,
    'Mapbox Satellite' => TileLayer::MapboxSatellite,
    'Mapbox Outdoors' => TileLayer::MapboxOutdoors,
];
```

### MapPicker (Form Field)

[](#mappicker-form-field)

Add an interactive map inside Filament forms. It syncs map clicks to form fields and supports all MapWidget configuration methods.

```
use EduardoRibeiroDev\FilamentLeaflet\Fields\MapPicker;
use EduardoRibeiroDev\FilamentLeaflet\Enums\TileLayer;

MapPicker::make('location')
    ->height(300)
    ->center(-23.5505, -46.6333)
    ->zoom(11)
    ->autoCenter()  // Auto-center to user's location
    ->tileLayersUrl(TileLayer::OpenStreetMap)
    ->columnSpanFull()
```

[![Form Field Example](images/form-field.png)](images/form-field.png)

**Default behavior:** Updates form's `latitude` and `longitude` fields. Customize with:

```
MapPicker::make('location')
    ->latitudeFieldName('lat')
    ->longitudeFieldName('lng')
```

**Store as JSON:** Keep coordinates in a single column:

```
MapPicker::make('location')
    ->storeAsJson()  // Stores: { "latitude": -23.5505, "longitude": -46.6333 }
```

**Load GeoJSON:** Automatically loads from models with `HasGeoJsonFile` trait or `getGeoJsonUrl()` method:

```
use EduardoRibeiroDev\FilamentLeaflet\Concerns\HasGeoJsonFile;

class DeliveryZone extends Model
{
    use HasGeoJsonFile;

    public function getGeoJsonFileAttributeName(): string
    {
        return 'geojson_file';
    }

    public function getGeoJsonFileDisk(): ?string
    {
        return 's3';
    }

    public function getExpirationTime(): ?DateTime
    {
        return now()->addHour();
    }
}
```

**Customize pick marker:** Visual feedback when clicking the map:

```
MapPicker::make('location')
    ->pickMarker(fn(Marker $marker) => $marker->blue()->title('Selected'))
```

**Dynamic configuration:** Most methods accept closures:

```
MapPicker::make('location')
    ->center(fn() => [$lat, $lng])
    ->height(fn($record) => 300)
    ->zoom(fn($record) => $record->zoom_level)
    ->mapDraggable(fn($record) => $record->is_editable)
    ->recenterTimeout(fn($record) => $record->is_read_only ? 3000 : null)
```

### MapColumn (Table Column)

[](#mapcolumn-table-column)

Display maps directly in Filament table columns:

```
use EduardoRibeiroDev\FilamentLeaflet\Tables\MapColumn;
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;

MapColumn::make('location')
    ->height(100)
    ->zoom(8)
    ->pickMarker(fn(Marker $marker) => $marker->black())
    ->static()    // Disable interactions in table preview
```

[![Table Column Example](images/table-column.png)](images/table-column.png)

Display circular maps for a unique visual style:

```
MapColumn::make('location')
    ->height(72)
    ->zoom(5)
    ->pickMarker(fn(Marker $marker) => $marker->iconSize([14, 25]))
    ->circular()  // Optional: circular display
```

[![Table Column Example](images/circular-table-column.png)](images/circular-table-column.png)

### MapEntry (Infolist)

[](#mapentry-infolist)

Display read-only maps in Filament infolists:

```
use EduardoRibeiroDev\FilamentLeaflet\Infolists\MapEntry;
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;

MapEntry::make('location')
    ->height(284)
    ->zoom(10)
    ->pickMarker(fn(Marker $marker) => $marker->red())
    ->static()    // Disable interactions (enabled by default)
    ->columnSpanFull()
```

**Auto-recenter:** Maps automatically recenter after 3 seconds when users pan around. This provides a guided viewing experience while allowing temporary exploration:

```
    ->recenterTimeout(5000)  // Recenter after 5 seconds (null to disable)
```

[![Infolist Entry Example](images/infolist-entry.png)](images/infolist-entry.png)

Map Elements
------------

[](#map-elements)

### Markers

[](#markers)

#### Creating Markers

[](#creating-markers)

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Markers\Marker;
use Filament\Support\Colors\Color;

protected function getMarkers(): array
{
    return [
        Marker::make(-23.5505, -46.6333)->title('Simple marker'),
        Marker::make(-23.5212, -46.4243)->green()->title('Colored marker'),
        Marker::make(-23.5266, -46.5412)->icon('https://leafletjs.com/examples/custom-icons/leaf-red.png', [32, 72]),
        Marker::make(-23.5300, -46.6400)->violet()->icon('heroicon-user-circle')->title('Heroicon marker'),
    ];
}
```

[![Markers Example](images/markers.png)](images/markers.png)

#### Marker Icons

[](#marker-icons)

Markers support multiple icon types with different visual behaviors:

**Custom Icon URL** (replaces the entire marker):

```
Marker::make(-23.5505, -46.6333)
    ->icon('https://example.com/icon.png', [54, 54])
    ->title('Custom Icon Marker')
```

[![Custom Icon Marker Example](images/custom-marker.png)](images/custom-marker.png)

When using a custom icon URL, the entire marker is replaced with your custom image - the marker's color is ignored.

**Heroicons (Filament Icons)** (icon in center):

```
use Filament\Support\Icons\Heroicon;

Marker::make(-23.5505, -46.6333)
    ->icon(Heroicon::BuildingLibrary)        // Using Heroicon enum
    ->icon('heroicon-building-library')      // Using string
    ->heroicon('building-library')           // Using explicit heroicon() method
    ->iconSize([36, 54])
    ->title('Heroicon Marker')
    ->color(Color::Indigo);
```

[![Heroicon Marker Example](images/heroicon-marker.png)](images/heroicon-marker.png)

When using Heroicons, the marker keeps its default styled appearance with the color gradient, and the Heroicon is automatically rendered in the center in white. The marker's color settings (`.blue()`, `.red()`, etc.) are fully respected.

**Icon Appearance:**

- **Icon URL**: Fully replaces marker appearance (color is ignored)
- **Heroicon**: Keeps marker style (with color gradient) with the icon rendered in the center

#### Marker Colors

[](#marker-colors)

Markers accept colors in multiple formats:

**Using Color Class:**

```
Marker::make(-23.5505, -46.6333)
    ->color(Color::Blue)  // Color class constant
    ->blue()              // Convenience method
```

**Using string formats:**

```
Marker::make(-23.5505, -46.6333)
    ->color('#3388ff')           // Hex color
    ->color('rgb(51, 136, 255)') // RGB color
    ->color('oklch(59% 0.15 262)')  // OKLCH color
```

**Available convenience methods:** `blue()`, `red()`, `green()`, `orange()`, `yellow()`, `violet()`, `gray()`, `black()`, `gold()`, `randomColor()`

#### From Eloquent Models

[](#from-eloquent-models)

```
// Basic usage
Marker::fromRecord(
    record: $store,
    latColumn: 'latitude',
    lngColumn: 'longitude',
    titleColumn: 'name',
    popupFieldsColumns: ['address', 'phone'],
    color: Color::Blue,
);

// With JSON coordinates
Marker::fromRecord(
    record: $store,
    jsonColumn: 'coordinates',
    latColumn: 'lat',
    lngColumn: 'lng',
);

// With custom callback
Marker::fromRecord(
    record: $store,
    mapRecordCallback: function (Marker $marker, Model $record) {
        $marker->gold()->popupFields(['hours' => $record->hours]);
    }
);
```

### Layer Groups

[](#layer-groups)

Layer groups are a powerful way to organize and manage multiple layers on your map. They allow you to:

- **Toggle visibility** - Show/hide entire groups of layers at once
- **Organize layers** - Group related markers and shapes together
- **Improve performance** - Manage large datasets efficiently
- **Control layer management** - Add/remove layers from groups dynamically

#### Layer Group

[](#layer-group)

A simple container for organizing related layers. Perfect for grouping logically related markers and shapes without any automatic behavior:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\LayerGroup;

protected function getMarkers(): array
{
    return [
        LayerGroup::make([
            Marker::make(-23.5505, -46.6333)->title('Store 1'),
            Marker::make(-23.5515, -46.6343)->title('Store 2'),
            Marker::make(-23.5525, -46.6353)->title('Store 3'),
        ])
        ->name('Active Stores')
        ->id('active-stores'),
    ];
}
```

**Using the `group()` helper method (shorthand):**

Instead of wrapping layers in `LayerGroup::make()`, you can use the `group()` method on any layer to automatically group multiple layers:

```
protected function getMarkers(): array
{
    return [
        Marker::make(-23.5505, -46.6333)
            ->title('Store 1')
            ->group('Active Stores'),

        Marker::make(-23.5515, -46.6343)
            ->title('Store 2')
            ->group('Active Stores'),

        Marker::make(-23.5525, -46.6353)
            ->title('Store 3')
            ->group('Active Stores'),
    ];
}
```

[![Layer Group Example](images/layer-group.png)](images/layer-group.png)

#### Feature Group

[](#feature-group)

Creates a polygon envelope around all layers in the group. This is useful for visualizing the coverage area or boundary of a set of points:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\FeatureGroup;

protected function getMarkers(): array
{
    return [
        FeatureGroup::make([
            Marker::make(-23.5505, -46.6333)->title('Point 1'),
            Marker::make(-23.5515, -46.6343)->title('Point 2'),
            Marker::make(-23.5525, -46.6323)->title('Point 3'),
        ])
            ->name('Delivery Zone')
            ->blue()
            ->fillBlue()
            ->fillOpacity(0.3)
            ->weight(3)
            ->dashArray(5, 10),
    ];
}
```

[![Feature Group Example](images/feature-group.png)](images/feature-group.png)

#### Marker Cluster

[](#marker-cluster)

Groups nearby markers into clusters for better performance and visual clarity, especially with large datasets. Clusters automatically expand when zooming in:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Groups\MarkerCluster;

protected function getMarkers(): array
{
    return [
        MarkerCluster::make([
            Marker::make(-23.5505, -46.6333)->title('Location 1'),
            Marker::make(-23.5515, -46.6343)->title('Location 2'),
            Marker::make(-23.5525, -46.6353)->title('Location 3'),
        ])
        ->maxClusterRadius(80)
        ->showCoverageOnHover()
        ->name('Locations')
        ->spiderfyOnMaxZoom(),
    ];
}
```

[![Marker Cluster Example](images/marker-cluster.png)](images/marker-cluster.png)

**Cluster from Model:**

Create clusters directly from Eloquent models with powerful customization:

```
use App\Models\Store;

protected function getMarkers(): array
{
    return [
        MarkerCluster::fromModel(
            model: Store::class,
            latColumn: 'latitude',
            lngColumn: 'longitude',
            titleColumn: 'name',
            descriptionColumn: 'description',
            popupFieldsColumns: ['address', 'phone'],
            color: Color::Green,
        )
        ->maxClusterRadius(60)
        ->disableClusteringAtZoom(15),
    ];
}
```

**Cluster with Query Modification:**

Filter and customize the query used to load markers:

```
MarkerCluster::fromModel(
    model: Store::class,
    latColumn: 'latitude',
    lngColumn: 'longitude',
    modifyQueryCallback: function ($query) {
        return $query
            ->where('status', 'active')
            ->where('city', 'São Paulo')
            ->orderBy('name');
    },
    mapRecordCallback: function (Marker $marker, Model $record) {
        // Customize each marker based on record properties
        if ($record->isPremium()) {
            $marker->gold()->icon('/images/premium-icon.png');
        }

        // Add status-based styling
        match($record->status) {
            'open' => $marker->green(),
            'busy' => $marker->orange(),
            'closed' => $marker->red(),
            default => $marker->gray(),
        };

        // Add popup with custom fields
        $marker->popupFields([
            'manager' => $record->manager_name,
            'staff' => $record->staff_count . ' employees',
            'rating' => $record->rating . ' ⭐',
        ]);
    }
);
```

**Advanced cluster configuration:**

```
MarkerCluster::make($markers)
    ->maxClusterRadius(80)              // Cluster radius in pixels
    ->showCoverageOnHover(true)         // Highlight cluster area on hover
    ->zoomToBoundsOnClick(true)         // Zoom to cluster bounds when clicked
    ->spiderfyOnMaxZoom(true)           // Spread markers at max zoom
    ->removeOutsideVisibleBounds(true)  // Remove markers outside viewport for performance
    ->disableClusteringAtZoom(15)       // Stop clustering at zoom level 15+
    ->animate(true);                    // Animate cluster changes;
```

### Shapes

[](#shapes)

Draw various geometric shapes on your map:

#### Circles

[](#circles)

Circles with radius in various units:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Circle;

protected function getShapes(): array
{
    return [
        Circle::make(-23.5505, -46.6333)
            ->popupContent('10km radius coverage')
            ->title('Service Area')
            ->blue()
            ->fillBlue()
            ->fillOpacity(0.2)
            ->radius(10000)           // Radius in meters (default)
            ->radiusInKilometers(10)  // Radius in kilometers
            ->radiusInMiles(6.2)      // Radius in miles
            ->radiusInFeet(32808)     // Radius in feet
    ];
}
```

[![Circles Example](images/circle.png)](images/circle.png)

#### Circle Markers

[](#circle-markers)

Small circles with pixel-based radius (like markers but circular):

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\CircleMarker;

CircleMarker::make(-23.5505, -46.6333)
    ->radius(45) // Radius in pixels
    ->red()
    ->fillRed()
    ->weight(2)
    ->title('Point of Interest');
```

[![Circle Markers Example](images/circle-marker.png)](images/circle-marker.png)

#### Polygons

[](#polygons)

Draw custom polygons:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polygon;

// Define a polygon area
Polygon::make(
    [-23.5505, -46.6333],
    [-23.5515, -46.6343],
    [-23.5525, -46.6323],
    [-23.5505, -46.6333], // Close the polygon
)
->green()
->fillGreen()
->fillOpacity(0.3)
->title('Delivery Zone')
->popupContent('We deliver to this area');

// Or build point by point
Polygon::make()
    ->addPoint(-23.5505, -46.6333)
    ->addPoint(-23.5515, -46.6343)
    ->addPoint(-23.5525, -46.6323)
    ->addPoint(-23.5505, -46.6333)
    ->blue();
```

[![Polygons Example](images/polygon.png)](images/polygon.png)

#### Polylines

[](#polylines)

Draw lines connecting multiple points:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polyline;

// Route or path
Polyline::make(
    [-23.5505, -46.6333],
    [-23.5515, -46.6343],
    [-23.5525, -46.6353],
    [-23.5535, -46.6363],
)
->blue()
->weight(4)
->opacity(0.7)
->dashArray('10, 5')      // Dashed line
->smoothFactor(1.5)       // Smooth curves
->title('Delivery Route');

// Or build incrementally
Polyline::make()
    ->addPoint(-23.5505, -46.6333)
    ->addPoint(-23.5515, -46.6343)
    ->addPoint(-23.5525, -46.6353)
    ->red()
    ->weight(3);
```

[![Polylines Example](images/polyline.png)](images/polyline.png)

#### Rectangles

[](#rectangles)

Draw rectangular bounds:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Rectangle;

// Using corner coordinates
Rectangle::make(
    [-23.5505, -46.6333],  // Southwest corner
    [-23.5525, -46.6353]   // Northeast corner
)
->orange()
->fillOrange()
->fillOpacity(0.2)
->title('Restricted Area');

// Alternative syntax
Rectangle::makeFromCoordinates(
    -23.5505, -46.6333,    // Southwest lat, lng
    -23.5525, -46.6353     // Northeast lat, lng
)
->red();
```

[![Rectangles Example](images/rectangle.png)](images/rectangle.png)

#### Shapes from Eloquent Models

[](#shapes-from-eloquent-models)

All shape classes support `fromRecord()` factory methods for easy creation from database records:

```
use App\Models\DeliveryZone;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Circle;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polygon;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Polyline;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\Rectangle;
use EduardoRibeiroDev\FilamentLeaflet\Support\Shapes\CircleMarker;

protected function getShapes(): array
{
    return DeliveryZone::all()->map(function ($zone) {
        // Circle from record
        return Circle::fromRecord(
            record: $zone,
            latColumn: 'latitude',
            lngColumn: 'longitude',
            titleColumn: 'name',
            descriptionColumn: 'description',
            popupFieldsColumns: ['address', 'radius'],
        );
    })->all();
}
```

**Improved Support for JSON Columns:**

Shapes now intelligently handle JSON-stored coordinates:

```
Circle::fromRecord(
    record: $zone,
    jsonColumn: 'center_coordinates',  // Stores: { "lat": -23.5505, "lng": -46.6333 }
    latColumn: 'lat',                   // Keys within the JSON
    lngColumn: 'lng',
);
```

Each shape type supports the `fromRecord()` method with common parameters:

- `record`: The Eloquent model instance
- `latColumn`/`lngColumn`: Column names for coordinates (for Circle, CircleMarker)
- `radiusColumn`: Column name for radius (for Circle, CircleMarker)
- `jsonColumn`: Column name for JSON coordinates (case of storing lat/lng as JSON)
- `pointsColumn`: Column name for points array (for Polygon, Polyline)
- `boundsColumn`: Column name for bounds array (for Rectangle)
- `titleColumn`: Column name for the shape title
- `descriptionColumn`: Column name for the popup content
- `popupFieldsColumns`: Array of column names to include in popup
- `color`: Default color for the shape
- `mapRecordCallback`: Closure to customize the shape based on the record

##### Circle from Record

[](#circle-from-record)

```
Circle::fromRecord(
    record: $zone,
    latColumn: 'center_lat',
    lngColumn: 'center_lng',
    radiusColumn: 'coverage_radius_km',
    titleColumn: 'zone_name',
    descriptionColumn: 'zone_description',
    mapRecordCallback: fn(Circle $circle, $record) =>
        $circle->radiusInKilometers($record->coverage_radius_km)
);
```

##### Polygon from Record

[](#polygon-from-record)

```
// Assumes database structure: points => [[lat, lng], [lat, lng], ...]
Polygon::fromRecord(
    record: $zone,
    pointsColumn: 'boundary_points',
    titleColumn: 'zone_name',
    popupFieldsColumns: ['area_sqkm', 'population'],
);
```

##### Polyline from Record

[](#polyline-from-record)

```
// Store route as JSON array of [lat, lng] coordinates
Polyline::fromRecord(
    record: $deliveryRoute,
    pointsColumn: 'route_path',
    titleColumn: 'route_name',
    descriptionColumn: 'destination',
    mapRecordCallback: fn(Polyline $line, $record) =>
        $record->is_completed
            ? $line->green()->weight(2)
            : $line->red()->weight(3)
);
```

##### Rectangle from Record

[](#rectangle-from-record)

```
// Assumes database structure: bounds => [[lat1, lng1], [lat2, lng2]]
Rectangle::fromRecord(
    record: $territory,
    boundsColumn: 'service_bounds',
    titleColumn: 'territory_name',
    popupFieldsColumns: ['region', 'sales_person'],
);
```

#### Shape Styling

[](#shape-styling)

```
Circle::make(-23.5505, -46.6333)
    ->radius(5000)

    // Border styling
    ->color(Color::Blue)        // Border color
    ->weight(3)                 // Border width in pixels
    ->opacity(0.8)              // Border opacity (0-1)
    ->dashArray('5, 10')        // Dashed border pattern

    // Fill styling
    ->fillColor(Color::Green)   // Fill color
    ->fillOpacity(0.3)          // Fill opacity (0-1)
```

### Editable Layers

[](#editable-layers)

Make markers and shapes editable directly on the map by enabling the draw control:

```
class MyMapWidget extends MapWidget
{
    protected bool $hasEditLayersControl = true;

    protected function getMarkers(): array
    {
        return [
            Marker::make(-23.5505, -46.6333)
                ->title('Editable Marker')
                ->editable(),  // Make this marker editable

            Circle::make(-23.5505, -46.6333)
                ->radiusInKilometers(5)
                ->editable(),  // Make this circle editable
        ];
    }
}
```

You can also make all layers in a group editable:

```
LayerGroup::make([
    Marker::make(-23.5505, -46.6333)->title('Point 1'),
    Marker::make(-23.5515, -46.6343)->title('Point 2'),
    Marker::make(-23.5525, -46.6353)->title('Point 3'),
])
->name('Editable Points')
->editable(),  // All markers in the group are now editable
```

User Interactions
-----------------

[](#user-interactions)

### Popups and Tooltips

[](#popups-and-tooltips)

#### Tooltips (appear on hover)

[](#tooltips-appear-on-hover)

```
Marker::make(-23.5505, -46.6333)
    ->tooltip(
        content: 'São Paulo City',
        permanent: false,
        direction: 'top',
        options: ['offset' => [0, -20]]
    );

// Or using individual methods
Marker::make(-23.5505, -46.6333)
    ->tooltipContent('São Paulo')
    ->tooltipPermanent(true)
    ->tooltipDirection('top');
```

#### Popups (appear on click)

[](#popups-appear-on-click)

```
Marker::make(-23.5505, -46.6333)
    ->popupTitle('Store Location')
    ->popupContent('Visit our main store')
    ->popupFields([
        'address' => '123 Main Street',
        'phone' => '+55 11 1234-5678',
        'email' => 'contact@store.com',
    ])
    ->popupOptions(['maxWidth' => 300]);

// Or use shorthand
Marker::make(-23.5505, -46.6333)
    ->popup(
        content: 'Store description',
        fields: ['address' => '123 Main St', 'phone' => '+55 11 1234-5678'],
        options: ['maxWidth' => 300]
    );
```

**How popup fields work:** Keys are automatically converted to title case, underscores replaced with spaces, and translated using Laravel's `__()` helper. Both keys and values support translation keys.

#### Combined Usage

[](#combined-usage)

```
Marker::make(-23.5505, -46.6333)
    ->title('Pizza Palace')
    ->popupContent('Best pizza in town')
    ->popupFields([
        'address' => '123 Main St',
        'phone' => '+55 11 1234-5678',
        'rating' => '4.5 ⭐',
    ]);
```

### Click Actions

[](#click-actions)

#### Marker Click Handler

[](#marker-click-handler)

```
use Filament\Notifications\Notification;

Marker::make(-23.5505, -46.6333)
    ->title('Interactive Marker')
    ->action(function (Marker $marker) { // Or ->onClick()
        Notification::make()
            ->title('Marker Clicked!')
            ->body('ID: ' . $marker->getId())
            ->send();
    });
```

#### Shape Click Handler

[](#shape-click-handler)

```
Circle::make(-23.5505, -46.6333)
    ->radius(5000)
    ->action(function (Circle $circle) { // Or ->onClick()
        Notification::make()
            ->title('Circle clicked')
            ->send();
    });
```

#### From Model Records

[](#from-model-records)

```
protected function getMarkers(): array
{
    return Store::all()->map(function ($store) {
        return Marker::fromRecord(
            record: $store,
            latColumn: 'latitude',
            lngColumn: 'longitude',
        )->action(function (Marker $marker, Store $record) { // Or ->onClick()
            Notification::make()
                ->title("You clicked: {$record->name}")
                ->body("Address: {$record->address}")
                ->send();

            return redirect()->route('stores.show', $record);
        });
    })->all();
}
```

#### Map Click Handler

[](#map-click-handler)

Handle clicks on the map itself:

```
public function handleMapClick(float $latitude, float $longitude): void
{
    Notification::make()
        ->title('Map clicked')
        ->body("Coordinates: {$latitude}, {$longitude}")
        ->send();
}
```

#### Field/Entry/Column Click Handlers

[](#fieldentrycolumn-click-handlers)

Handle map clicks in form fields, infolists, or table columns:

```
use EduardoRibeiroDev\FilamentLeaflet\Fields\MapPicker;
use Filament\Notifications\Notification;

MapPicker::make('location')
    ->height(300)
    ->onMapClick(function (float $latitude, float $longitude) {
        Notification::make()
            ->title('Location Selected')
            ->body("Lat: {$latitude}, Lng: {$longitude}")
            ->send();
    });
```

Handle layer (marker/shape) clicks:

```
use EduardoRibeiroDev\FilamentLeaflet\Support\BaseLayer;

MapPicker::make('location')
    ->height(300)
    ->onLayerClick(function (BaseLayer $layer) {
        Notification::make()
            ->title('Layer Clicked')
            ->body("Layer ID: {$layer->getId()} - Type: {$layer->getType()}")
            ->send();
    });
```

Advanced Features
-----------------

[](#advanced-features)

### Model Integration &amp; CRUD Operations

[](#model-integration--crud-operations)

Enable creating markers directly from map clicks:

```
use App\Models\Location;

class LocationMapWidget extends MapWidget
{
    protected ?string $markerModel = Location::class;
    protected string $latitudeColumnName = 'latitude';
    protected string $longitudeColumnName = 'longitude';
    protected ?string $jsonCoordinatesColumnName = 'coordinates'; // For JSON storage

    protected function getFormComponents(): array
    {
        return [
            TextInput::make('name')->required(),
            ColorPicker::make('color')->rgb(),
            Textarea::make('description')->columnSpanFull(),
        ];
    }
}
```

When users click the map, a form modal opens to create a new marker. If coordinates are stored as JSON, the widget automatically converts latitude/longitude into the configured JSON column.

**Built-in Marker Actions:**

Define custom actions to execute when clicking markers with records using the `$markerClickAction` property:

```
use App\Models\Store;

class LocationMapWidget extends MapWidget
{
    protected ?string $markerModel = Store::class;
    protected string $latitudeColumnName = 'latitude';
    protected string $longitudeColumnName = 'longitude';

    // Define the action to execute when a marker is clicked
    protected ?string $markerClickAction = 'view'; // 'view', 'edit', 'delete' or null

    protected function getMarkers(): array
    {
        return Store::all()->map(fn($store) =>
            Marker::fromRecord(
                record: $store,
                latColumn: 'latitude',
                lngColumn: 'longitude',
                titleColumn: 'name',
            )
        )->all();
    }
}
```

**Available marker click actions:**

- `'view'` - Display a read-only modal with the marker's record details
- `'edit'` - Open an edit form for the marker's record (default)
- `'delete'` - Prompt to delete the marker's record
- `null` - No action

**Using a Resource Form:**

```
use App\Filament\Resources\Locations\LocationResource;

class LocationMapWidget extends MapWidget
{
    protected ?string $markerModel = Location::class;
    protected ?string $markerResource = LocationResource::class;
}
```

**Hooks:**

```
protected function afterMarkerCreated(Model $record): void
{
    Notification::make()
        ->title('Location created!')
        ->body("Created: {$record->name}")
        ->success()
        ->send();
}

protected function mutateFormDataBeforeCreate(array $data): array
{
    $data['user_id'] = auth()->id();
    $data['status'] = 'active';
    return parent::mutateFormDataBeforeCreate($data);
}
```

**Keep Maps in Sync with Tables:**

```
use EduardoRibeiroDev\FilamentLeaflet\Concerns\InteractsWithMap;

class ManageLocations extends ManageRecords
{
    use InteractsWithMap;
}
```

This automatically refreshes the map after create/edit/delete actions.

### GeoJSON Density Maps

[](#geojson-density-maps)

Display choropleth maps with custom density data:

```
class BrazilDensityWidget extends MapWidget
{
    protected ?string $geoJsonUrl = 'https://example.com/brazil-states.json';

    protected array $geoJsonColors = [
        '#FED976', '#FEB24C', '#FD8D3C', '#FC4E2A',
        '#E31A1C', '#BD0026', '#800026',
    ];

    protected function getGeoJsonData(): array
    {
        return [
            'SP' => 166.23,  // São Paulo
            'RJ' => 365.23,  // Rio de Janeiro
            'MG' => 33.41,   // Minas Gerais
        ];
    }

    protected function getGeoJsonTooltip(): string
    {
        return
