wm/map-multi-linestring
A Laravel package for working with multi-linestring map geometries, offering helpers to handle, convert and render collections of line strings so multi-segment routes and paths are easier to display in map-based applications.
Install and confirm compatibility first. This is a Nova field, so it will only work inside a Laravel app with Nova installed. Verify the Nova major version the package targets (composer show wm/map-multi-linestring for illuminate/* and laravel/nova constraints) before committing — adoption is very low, so you are the QA team.
Require at least ^1.0.4. The 1.0.4 patch fixes a crash where oldAttribute was null — i.e. the create path, not just edits. On older versions your first "new record" save can throw before you write a single line of integration code:
composer require wm/map-multi-linestring:^1.0.4
php artisan nova:publish # if the README mentions publishing field assets
php artisan view:clear && php artisan config:clear
Database column. Linestring collections are stored as GeoJSON, so you want a json (MySQL/Postgres) or text column, nullable:
$table->json('route')->nullable();
Register the field. The minimal resource usage is the standard Nova shape:
use Wm\MapMultiLinestring\MapMultiLinestring;
MapMultiLinestring::make('Route')
->onlyOnForms(), // index tables get ugly fast with a map per row
First use case to build: an admin editing a path-like geometry — delivery routes, pipeline/utility runs, fiber segments, hiking trails, catchment boundaries. You want an operator without GIS skills to click points on a map and have the model persist valid GeoJSON. Build that one screen end-to-end — including a genuine create (not just an edit of a seeded record) — before generalizing.
Where to look first: the package's src/ directory (field class, any withMeta defaults) and its compiled dist/js/field.js. There is no meaningful ecosystem around this package, so the source is the documentation. Read the field's resolve/fill methods before you decide how to cast the column — that interaction is where most of the friction lives. Note the 1.0.4 fix lives exactly there, in old-vs-new attribute comparison: the field does compare existing and incoming geometry, so it has opinions about what "unchanged" means.
Treat the column as GeoJSON, not as an array. Keep the raw JSON string on the model and parse on the way out. This avoids double-encoding when Nova's field serializes its value:
protected $casts = [
'route' => 'array', // only if the field's fill() tolerates it — verify first
];
Safer default: no cast at all, store the string, and add an accessor:
public function routeGeoJson(): ?array
{
return $this->route ? json_decode($this->route, true) : null;
}
This is doubly true given 1.0.4: the field compares oldAttribute against the incoming value. An array cast changes what the model hands back on the old side, and you do not want to discover mismatch semantics through a changelog entry. Leave the attribute raw and decode at the edges.
Expected payload shape is a FeatureCollection of LineString / MultiLineString features. Check against a real save before writing downstream code:
[
'type' => 'FeatureCollection',
'features' => [
['type' => 'Feature', 'geometry' => ['type' => 'LineString', 'coordinates' => [[lng, lat], ...]], 'properties' => []],
],
]
Map configuration via withMeta. Most of these Nova map fields expose their knobs through meta rather than fluent setters. Confirm the exact keys in the field source, but expect something like:
MapMultiLinestring::make('Route')
->withMeta([
'center' => [45.4383, 12.3266], // [lat, lng] for the map container
'zoom' => 13,
'height' => '420px',
])
->help('Click to add vertices. Double-click to finish a line.');
If you need per-record centering (e.g. fit to existing geometry), compute it in resolve via ->withMeta(['center' => ...]) on the resolved value — cheap, and it makes editing existing records far nicer.
Separate fields per geometry role. If a model has both a planned and an actual route, use two distinct columns and two fields. Do not try to multiplex one column with a discriminator; the field has no concept of a feature's properties, so filtering by type downstream becomes guesswork.
Validation. The submitted value is a JSON string. A plain ->rules('json') catches syntax but not topology. Add a custom rule if the geometry matters:
->rules(['nullable', 'json', new ValidLineStringCollection])
Inside the rule, json_decode, assert type === 'FeatureCollection', and assert each feature is a LineString/MultiLineString with at least two coordinate pairs. That's enough to prevent the "empty geometry saved as {}" class of bug.
Downstream consumption. Because everything is GeoJSON, you get free interop: feed it straight into a frontend Mapbox/Leaflet layer, into PostGIS via ST_GeomFromGeoJSON, or into MySQL 8 spatial functions. Keep a single conversion point (a resource class or toGeoJson() method on the model) so you can swap rendering libraries without touching controllers.
Testing. Don't try to drive Leaflet draw in feature tests. Test the field configuration and the persistence/validation layer with Nova::actingAs($user) and a direct payload post. Cover create and update explicitly — 1.0.4 is a create-path bug, which is exactly the path people forget to test. Reserve a single Dusk/Playwright smoke test for "the map renders and the save button round-trips a fixture GeoJSON." That covers the realistic failure mode (asset not loaded) without a flaky suite.
oldAttribute (fixed in 1.0.4). Creating a record — or any state where the field previously had no value — used to blow up while comparing geometries. If you see an exception from the field on first save, you are on 1.0.3 or below. Pin ^1.0.4 and keep a test that creates a record with route => null.[longitude, latitude]; most map APIs take [lat, lng]. This package's center/zoom meta almost certainly uses map-convention lat, lng while the stored geometry uses GeoJSON convention. Getting these crossed silently puts your routes off the coast of Somalia. Assert on one known fixture early.^1.0.4) and be prepared to fork. Don't put this field on a resource you can't afford to reimplement; the fallback is a Textarea with JSON validation plus a read-only map preview.composer update laravel/nova.FeatureCollection rather than null when a user clears the drawing. Normalize in a mutator so your "has geometry" checks (whereNotNull, if ($model->route)) behave predictably. Normalizing also removes a whole class of old-vs-new comparison surprises.invalidateSize() on the map instance.dist/, so no npm step — but that also means you cannot patch the JS without rebuilding. Confirm dist/ exists in vendor/; if not, you need the npm build, which is a much bigger commitment.fill()/cast/comparison); if it's absent, the JS never registered the draw event. And isolate create vs update immediately — they take different code paths through the field and failures rarely affect both.vendor:publish --tag=...) if you only need CSS/labels. For real behavior changes — custom toolbar, snap-to-road, per-vertex tooltips — fork and point a VCS repository at your fork; there is no plugin architecture here worth building against. A fork also lets you drop the old-vs-new comparison entirely if it fights your data model.->onlyOnForms() and show a human-readable summary (point count, bounding box) in the index instead.How can I help you explore Laravel packages today?