otrsw/leaflet-map
otrsw/leaflet-map is a lightweight Laravel package for adding interactive Leaflet maps to your views. It provides Blade components and helpers for tiles, markers, popups, and map configuration, making map integration simple without heavy JavaScript setup.
1.1 is a vendor rename, not a feature release. The only change is otrsw → imumz. The package name is now imumz/leaflet-map and the PSR-4 namespace is Imumz\LeafletMap. Nothing else in the release notes suggests new field methods, config, or assets — so assume the JS/PHP behaviour is byte-for-byte the same as the version you already know, and treat this as a migration, not an upgrade.
Fresh install:
composer require imumz/leaflet-map:^1.1
composer show imumz/leaflet-map # confirm version + autoload namespace
ls vendor/imumz/leaflet-map/src # confirm the field class name
Note that composer require otrsw/leaflet-map still resolves — to the old package. The two are separate Packagist entries and Composer will happily install both side by side (see Gotchas).
Existing install:
composer remove otrsw/leaflet-map
composer require imumz/leaflet-map:^1.1
grep -rn "Otrsw\\\\LeafletMap" app/ config/ resources/ database/
php artisan vendor:publish --provider="Imumz\LeafletMap\FieldServiceProvider" --force
php artisan nova:publish && php artisan optimize:clear
The rename is good news for one thing: the vendor/repo mismatch that made the old package confusing (otrsw/leaflet-map vs iMuMz/NovaLeafletMap) is gone. imumz/leaflet-map ↔ iMuMz/NovaLeafletMap now line up, so Imumz\LeafletMap\LeafletMap is a safe first guess — still verify with composer show.
Database columns come first. This is a coordinate picker, so the model needs somewhere to put two numbers. Use decimal, not float — float rounds and you'll chase phantom pin drift:
Schema::table('stores', function (Blueprint $table) {
$table->decimal('latitude', 10, 7)->nullable()->after('address');
$table->decimal('longitude', 10, 7)->nullable()->after('latitude');
});
Cast them on the model so Nova receives floats rather than strings:
protected $casts = [
'latitude' => 'float',
'longitude' => 'float',
];
First use case — a store/venue locator. Drop the field into the resource form and map it to those two columns:
use Imumz\LeafletMap\LeafletMap; // namespace changed in 1.1
public function fields(Request $request)
{
return [
Text::make('Name'),
Text::make('Address'),
LeafletMap::make('Location')
->latitude('latitude')
->longitude('longitude')
->defaultCenter(51.5074, -0.1278) // London; or your business HQ
->zoom(14)
->onlyOnForms(),
LeafletMap::make('Map')
->latitude('latitude')
->longitude('longitude')
->readonly()
->exceptOnForms(),
];
}
Load the resource, click the map, save, then dd($store->fresh()->latitude) to confirm the round trip before building anything on top of it.
Two fields, one column pair. The field renders a control in forms and a display on detail/index. Reusing one instance for both is the most common source of "the map won't drag" bug reports. Split them with onlyOnForms() / exceptOnForms() as above, or use resolveUsing() to swap the rendered mode.
Centralise the field — and now centralise the import too. Since 1.1 changes the class namespace, every use statement is a migration touchpoint. A single subclass means you grep one file next time:
// app/Nova/Fields/LocationPicker.php
namespace App\Nova\Fields;
use Imumz\LeafletMap\LeafletMap;
class LocationPicker extends LeafletMap
{
public function __construct($name = 'Location')
{
parent::__construct($name);
$this->latitude('latitude')
->longitude('longitude')
->defaultCenter(
config('maps.default_lat', 51.5074),
config('maps.default_lng', -0.1278),
)
->zoom(config('maps.default_zoom', 13));
}
}
Resources then reference LocationPicker::make() and never import the vendor class directly. This is the cheapest insurance against a 1.2 vendor rename, which the 1.1 history makes a live possibility.
Pair it with an address field and geocode on save. Let the user paste an address, geocode it once, then use the map purely as a correction tool for the pin.
// app/Observers/StoreObserver.php
public function saving(Store $store): void
{
// Only geocode when the address changed and no manual pin was set
if ($store->isDirty('address') && ! $store->isDirty('latitude')) {
$result = Geocoder::geocode($store->address)->get();
if ($result->isNotEmpty()) {
$store->latitude = $result->first()->getCoordinates()->getLatitude();
$store->longitude = $result->first()->getCoordinates()->getLongitude();
}
}
}
Add the inverse guard too — if the user drags the pin, don't clobber their manual placement on the next save.
Index views: don't. Rendering N Leaflet instances costs N tile sets and N DOM trees and will time out on a 100-row index. Show text instead and reserve the field for detail/edit:
Text::make('Coordinates', fn () => $this->latitude
? "{$this->latitude}, {$this->longitude}"
: '—')->onlyOnIndex()->asHtml(),
Validate the range, not just presence. The field won't stop a POST with latitude=999. Add explicit rules in StoreRequest:
public function rules(): array
{
return [
'latitude' => ['nullable', 'numeric', 'between:-90,90', 'required_with:longitude'],
'longitude' => ['nullable', 'numeric', 'between:-180,180', 'required_with:latitude'],
];
}
required_with is the one people forget — a half-set pin silently pins to (0,0) and shows up in the Gulf of Guinea.
Distance queries. Once coordinates are stored, a STORED generated POINT column plus a SPATIAL INDEX is the cheap upgrade:
$table->point('location')->stored()->nullable();
$table->spatialIndex('location');
Then whereRaw('ST_Distance_Sphere(location, POINT(?, ?)) < ?', [$lng, $lat, $meters]). The Leaflet field becomes the write side of a proper geo feature rather than a decorative widget.
Nova Actions for bulk pin fixes. An action that opens a single map and applies the dropped coordinate to all selected rows is where this package earns its place on a resource with a few hundred ungeocoded records.
The rename has no compatibility shim. Packagist treats imumz/leaflet-map as a brand-new package. There is no replace or provide for otrsw/leaflet-map unless the maintainer added one (the release notes don't say — check composer.json before assuming). Consequences:
composer update on an existing project will not move you to 1.1. You will sit on the old package forever, with no updates, and never know.otrsw/leaflet-map, you can end up with both installed. Two service providers register the same field name; two copies of the JS bundle into nova/app.js; vendor:publish writes to the same resources/views/vendor/leaflet-map/ path from whichever provider runs last. Symptom: duplicated map controls, or your customised view silently reverting after a composer update. Run composer show | grep -i leaflet and confirm exactly one entry.php artisan vendor:publish --provider="Otrsw\LeafletMap\FieldServiceProvider" now fails with a provider-not-found error that reads like a broken install. It isn't. The provider is Imumz\LeafletMap\FieldServiceProvider.Clear the compiled caches, in this order. bootstrap/cache/packages.php and config.php hold the old provider/nonce. After swapping packages:
composer dump-autoload
php artisan package:discover
php artisan config:clear && php artisan view:clear
php artisan nova:publish
Skipping package:discover is the usual cause of "class not found" on a package that is demonstrably in vendor/.
Still assume Nova 3 and verify on 4/5. 1.1 changes the vendor string and nothing else — no compatibility work is announced. All the Nova 4/5 breakage of the pre-rename code is still there to check: this.field.value vs this.field.displayedAs, missing FormField/DetailField mixins, and Nova's move away from global Vue components. Budget an afternoon for a fork. The rename makes a future release more likely, not less, but it does not by itself modernise the JS.
Re-publish views after the swap, and diff before --force. resources/views/vendor/leaflet-map/ is not namespaced by vendor, so the new package overwrites the same paths. If you had customised tiles or marker clustering, back it up first.
Tile provider is the hidden production dependency. Out of the box these fields hit tile.openstreetmap.org directly from the browser. OSM's tile usage policy forbids heavy/production traffic, and in the EU it's a GDPR problem (every admin page load ships user IPs to a third party). Fix before shipping:
{{-- resources/views/vendor/leaflet-map/partials/tiles.blade.php --}}
L.tileLayer('https://tiles.yourdomain.com/{z}/{x}/{y}.png', {
attribution: '© Your Co',
maxZoom: 19,
});
Self-host with tileserver-gl, or buy a Mapbox/MapTiler key and put it in config/services.php — never commit it into a published view.
invalidateSize() on tabs and modals. The classic Leaflet-in-Nova bug: the map renders grey or offset because the container had zero width at mounted(). Unchanged in 1.1. Patch the published Vue component:
mounted() {
this.map = L.map(this.$refs.map).setView([this.latitude, this.longitude], this.zoom);
setTimeout(() => this.map.invalidateSize(), 200);
}
and again inside any tab-change handler.
Coordinate precision vs. marker precision. decimal(8,6) loses ~11cm per step — fine for buildings, not survey data. decimal(10,7) gives ~1cm. Also confirm the field emits numbers; '' gets coerced to 0, silently dragging pins to Null Island.
No pin ≠ (0,0). Model the location as nullable and short-circuit the field when both values are null. A map centred on (0,0) with a marker at (0,0) is indistinguishable from a genuinely-empty record.
Publishing views is still your only extension point. There's no documented config file and no API beyond the field methods. resources/views/vendor/leaflet-map/ is yours — that's where you add marker clustering, a search box, or a "locate me" button. Prefer overriding the published view over forking the JS.
Testing is painful. The field is a Vue component inside Nova's SPA; php artisan test can only assert the field is present in the resource (assertTrue($resource->fields()[0] instanceof LeafletMap)) and that your lat/l ng columns persist. Anything visual needs Laravel Dusk with ->waitFor('.leaflet-container') and a stub tile server, otherwise CI hangs on network timeouts.
Debug order when it renders blank. 1) composer show | grep -i leaflet — are the old and new packages both installed? 2) php artisan package:discover && php artisan nova:publish, hard-refresh (stale app.js masks everything). 3) Browser console for L is not defined. 4) Network tab — tiles 404 or CSP-blocked? Check img-src/connect-src. 5) Zero-height container — that's invalidateSize, not the package.
Pin the version and own the source. Low-star, single-maintainer package; a vendor rename at 1.1 is a signal of life but also proof the install path is not stable. Commit composer.lock. If it's load-bearing, consider copying vendor/imumz/leaflet-map/src into app/Nova/Fields/ — it's a few hundred lines, and you have now been through one forced migration already.
How can I help you explore Laravel packages today?